fix(cli): infer provider lists respect selected agent (#123884)

* fix(cli): require inference provider owners

* test(cli): type provider owner fixtures

* chore: leave infer release note to release
This commit is contained in:
Peter Steinberger
2026-08-14 16:45:37 -07:00
committed by GitHub
parent 722e1ff48e
commit 72e67904be
14 changed files with 264 additions and 52 deletions
+8 -3
View File
@@ -106,6 +106,11 @@ A good infer-based skill maps common user intents to the right subcommand, inclu
- For `image describe`, `--file` accepts local paths and HTTP(S) URLs; remote URLs go through the normal media-fetch SSRF policy.
- Stateless execution commands (`model run`, `image *`, `audio *`, `video *`, `web *`, `embedding *`) default to local. Gateway-managed state commands (`tts status`) default to gateway.
- The local path never requires the gateway to be running.
- Provider inventory commands whose `configured` state can come from saved agent auth accept
`--agent <id>`. Without it, they use `agents.defaults.systemAgent.agentId` or the sole configured
agent; explicit multi-agent fleets with no system owner must pass `--agent`. The provider catalog
remains aggregate; `--agent` scopes saved-auth and per-agent selection facts. Gateway-owned TTS
provider state remains Gateway-global, so `tts providers --gateway` does not accept `--agent`.
- Generated image and video `--output` files are staged beside the destination and replace it only after the complete buffer is written; a failed write leaves an existing destination unchanged.
- Local `model run` is a lean one-shot provider completion: it resolves the configured agent model and auth but does not start a chat-agent turn, load tools, or open bundled MCP servers.
- `model run --file` attaches image files (auto-detected MIME type) to the prompt; repeat `--file` for multiple images. Non-image files are rejected — use `infer audio transcribe` or `infer video describe` instead.
@@ -121,7 +126,7 @@ openclaw infer model run --prompt "Reply with exactly: smoke-ok" --json
openclaw infer model run --prompt "Summarize this changelog entry" --model openai/gpt-5.4 --json
openclaw infer model run --prompt "Describe this image in one sentence" --file ./photo.jpg --model google/gemini-2.5-flash --json
openclaw infer model run --prompt "Use more reasoning here" --thinking high --json
openclaw infer model providers --json
openclaw infer model providers --agent <id> --json
openclaw infer model inspect --model gpt-5.6-sol --json
```
@@ -254,7 +259,7 @@ Search and fetch.
openclaw infer web search --query "OpenClaw docs" --json
openclaw infer web search --query "OpenClaw infer web providers" --json
openclaw infer web fetch --url https://docs.openclaw.ai/cli/infer --json
openclaw infer web providers --json
openclaw infer web providers --agent <id> --json
```
`web providers` lists available, configured, and selected providers for search and fetch.
@@ -266,7 +271,7 @@ Vector creation and embedding-provider inspection.
```bash
openclaw infer embedding create --text "friendly lobster" --json
openclaw infer embedding create --text "customer support ticket: delayed shipment" --model openai/text-embedding-3-large --json
openclaw infer embedding providers --json
openclaw infer embedding providers --agent <id> --json
```
## JSON output
+8 -4
View File
@@ -223,6 +223,7 @@ export function tryResolveLegacyCompatibilityAgentId(cfg: OpenClawConfig): strin
export function resolveSystemAgentTargetAgentId(
cfg: OpenClawConfig,
requestedAgentId?: string,
context?: AgentSelectionContext,
): string {
const configuredAgentId =
normalizeOptionalString(requestedAgentId) ??
@@ -231,10 +232,13 @@ export function resolveSystemAgentTargetAgentId(
return normalizeAgentId(configuredAgentId);
}
return normalizeAgentId(
resolveSoleAgentId(cfg, {
surface: "system-agent consult routing",
hint: "Set agents.defaults.systemAgent.agentId or pass an explicit consult agent id.",
}),
resolveSoleAgentId(
cfg,
context ?? {
surface: "system-agent consult routing",
hint: "Set agents.defaults.systemAgent.agentId or pass an explicit consult agent id.",
},
),
);
}
+145 -7
View File
@@ -46,8 +46,12 @@ const mocks = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({})),
getRuntimeConfigSourceSnapshot: vi.fn(() => null),
setRuntimeConfigSnapshot: vi.fn(),
loadAuthProfileStoreForRuntime: vi.fn(() => ({ profiles: {}, order: {} })),
listProfilesForProvider: vi.fn(() => []),
loadAuthProfileStoreForRuntime: vi.fn<
typeof import("../agents/auth-profiles.js").loadAuthProfileStoreForRuntime
>(() => ({ version: 1, profiles: {}, order: {} })),
listProfilesForProvider: vi.fn<
typeof import("../agents/auth-profiles.js").listProfilesForProvider
>(() => []),
resolveApiKeyForProviderCore: vi.fn(),
loadManifestMetadataSnapshot: vi.fn(() => ({ manifestRegistry: { plugins: [] } })),
planEffectiveModelCatalogRows: vi.fn<
@@ -67,7 +71,9 @@ const mocks = vi.hoisted(() => ({
return store;
},
),
resolveMemorySearchConfig: vi.fn(() => null),
resolveMemorySearchConfig: vi.fn<
typeof import("../agents/memory-search.js").resolveMemorySearchConfig
>(() => null),
loadModelCatalog: vi.fn(async () => []),
prepareSimpleCompletionModelForAgent: vi.fn(async () => ({
selection: {
@@ -184,7 +190,12 @@ const mocks = vi.hoisted(() => ({
entries: [],
})),
convertHeicToJpeg: vi.fn(async () => Buffer.from("jpeg-normalized")),
isWebSearchProviderConfigured: vi.fn(() => false),
listWebSearchProviders: vi.fn<typeof import("../web-search/runtime.js").listWebSearchProviders>(
() => [],
),
isWebSearchProviderConfigured: vi.fn<
typeof import("../web-search/runtime.js").isWebSearchProviderConfigured
>(() => false),
isWebFetchProviderConfigured: vi.fn(() => false),
getModelsCommandSecretTargetIds: vi.fn(() => new Set(["models.providers.*.apiKey"])),
getMemoryEmbeddingCommandSecretTargetIds: vi.fn(() => new Set(["models.providers.*.apiKey"])),
@@ -279,7 +290,15 @@ vi.mock("../agents/agent-scope.js", () => ({
resolveDefaultAgentId: () => "main",
resolveAgentDir: mocks.resolveAgentDir,
resolveAgentConfig: () => ({}),
resolveAgentEffectiveModelPrimary: () => undefined,
resolveAgentEffectiveModelPrimary: (
cfg: {
agents?: {
defaults?: { model?: string };
entries?: Record<string, { model?: string }>;
};
},
agentId: string,
) => cfg.agents?.entries?.[agentId]?.model ?? cfg.agents?.defaults?.model,
resolveAgentModelFallbacksOverride: () => [],
}));
@@ -422,7 +441,7 @@ vi.mock("../tts/provider-registry.js", () => ({
}));
vi.mock("../web-search/runtime.js", () => ({
listWebSearchProviders: vi.fn(() => []),
listWebSearchProviders: mocks.listWebSearchProviders,
isWebSearchProviderConfigured:
mocks.isWebSearchProviderConfigured as typeof import("../web-search/runtime.js").isWebSearchProviderConfigured,
runWebSearch: vi.fn(),
@@ -520,13 +539,16 @@ describe("capability cli", () => {
beforeEach(() => {
vi.stubEnv("OPENAI_API_KEY", "");
mocks.loadConfig.mockReset().mockReturnValue({});
mocks.runtime.log.mockClear();
mocks.runtime.error.mockClear();
mocks.runtime.writeJson.mockClear();
mocks.loadModelCatalog
.mockReset()
.mockResolvedValue([{ id: "gpt-5.4", provider: "openai", name: "GPT-5.4" }] as never);
mocks.loadAuthProfileStoreForRuntime.mockReset().mockReturnValue({ profiles: {}, order: {} });
mocks.loadAuthProfileStoreForRuntime
.mockReset()
.mockReturnValue({ version: 1, profiles: {}, order: {} });
mocks.listProfilesForProvider.mockReset().mockReturnValue([]);
mocks.resolveApiKeyForProviderCore.mockReset().mockRejectedValue(new Error("no auth profile"));
mocks.loadManifestMetadataSnapshot
@@ -607,6 +629,7 @@ describe("capability cli", () => {
{ id: "openai", defaultModel: "text-embedding-3-small", transport: "remote" },
]);
mocks.listEmbeddingProviders.mockReset().mockReturnValue([]);
mocks.listWebSearchProviders.mockReset().mockReturnValue([]);
mocks.isWebSearchProviderConfigured.mockReset().mockReturnValue(false);
mocks.isWebFetchProviderConfigured.mockReset().mockReturnValue(false);
mocks.getModelsCommandSecretTargetIds.mockClear();
@@ -795,6 +818,7 @@ describe("capability cli", () => {
expect(mocks.loadModelCatalog).toHaveBeenCalledWith({
config: mocks.loadConfig(),
...(command === "providers" ? { agentId: "main" } : {}),
readOnly: true,
});
});
@@ -814,6 +838,120 @@ describe("capability cli", () => {
expect(mocks.getProviderEnvVars).toHaveBeenCalledWith("openai");
});
it("scopes provider state and model selection to an explicit agent", async () => {
const cfg = {
agents: {
ownership: "explicit" as const,
defaults: { systemAgent: { agentId: "beta" } },
entries: {
alpha: { model: "anthropic/claude-sonnet-4-6" },
beta: { model: "openai/gpt-5.4" },
},
},
};
mocks.loadConfig.mockReturnValue(cfg);
mocks.loadModelCatalog.mockResolvedValueOnce([
{ id: "claude-sonnet-4-6", provider: "anthropic", name: "Claude" },
{ id: "gpt-5.4", provider: "openai", name: "GPT" },
] as never);
mocks.loadAuthProfileStoreForRuntime.mockImplementation(
(agentDir) =>
({
profiles:
agentDir === "/tmp/agent-alpha"
? { "anthropic:alpha": { provider: "anthropic" } }
: { "openai:beta": { provider: "openai" } },
order: {},
}) as never,
);
mocks.listProfilesForProvider.mockImplementation((store, provider) =>
Object.entries(store.profiles as Record<string, { provider: string }>)
.filter(([, profile]) => profile.provider === provider)
.map(([id]) => id),
);
await runCapability("model", "providers", "--agent", "alpha", "--json");
expect(mocks.loadModelCatalog).toHaveBeenCalledWith({
config: cfg,
agentId: "alpha",
readOnly: true,
});
expect(mocks.resolveAgentDir.mock.calls.map((call) => call[1])).toEqual(["alpha", "alpha"]);
expect(firstJsonOutput()).toEqual(
expect.arrayContaining([
expect.objectContaining({ provider: "anthropic", configured: true, selected: true }),
expect.objectContaining({ provider: "openai", configured: false, selected: false }),
]),
);
});
it("uses the configured system agent for embedding provider state", async () => {
const cfg = {
agents: {
ownership: "explicit" as const,
defaults: { systemAgent: { agentId: "beta" } },
entries: { alpha: {}, beta: {} },
},
};
mocks.loadConfig.mockReturnValue(cfg);
mocks.resolveMemorySearchConfig.mockImplementation(
(_cfg, agentId) =>
(agentId === "beta"
? { provider: "openai", model: "text-embedding-3-small" }
: null) as never,
);
await runCapability("embedding", "providers", "--json");
expect(mocks.resolveMemorySearchConfig).toHaveBeenCalledWith(cfg, "beta");
expect(mocks.resolveAgentDir.mock.calls.every((call) => call[1] === "beta")).toBe(true);
expect(firstJsonOutput()).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "openai", configured: true, selected: true }),
]),
);
});
it("requires an explicit owner for agent-backed provider state in explicit fleets", async () => {
mocks.loadConfig.mockReturnValue({
agents: {
ownership: "explicit",
entries: { alpha: {}, beta: {} },
},
});
await expect(runCapability("audio", "providers", "--json")).rejects.toThrow("exit 1");
expectRuntimeErrorContains("inference provider inspection has no explicit owner");
expectRuntimeErrorContains("Pass --agent <id> or set agents.defaults.systemAgent.agentId");
expect(mocks.loadAuthProfileStoreForRuntime).not.toHaveBeenCalled();
});
it("scopes web search auth inspection to the selected agent", async () => {
mocks.loadConfig.mockReturnValue({
agents: {
ownership: "explicit",
entries: { alpha: {}, beta: {} },
},
});
const provider = {
id: "openai",
envVars: ["OPENAI_API_KEY"],
requiresCredential: true,
};
mocks.listWebSearchProviders.mockReturnValue([provider] as never);
await runCapability("web", "providers", "--agent", "beta", "--json");
expect(firstJsonOutput()).toMatchObject({ search: [{ id: "openai" }], fetch: [] });
expect(mocks.isWebSearchProviderConfigured).toHaveBeenCalledWith({
provider,
config: mocks.loadConfig(),
agentDir: "/tmp/agent-beta",
});
});
it("inspects runtime-declared manifest models without live discovery", async () => {
mocks.loadModelCatalog.mockResolvedValueOnce([] as never);
mocks.planEffectiveModelCatalogRows.mockReturnValueOnce({
+4
View File
@@ -16,6 +16,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
@@ -80,10 +81,12 @@ export function registerAudioCapabilityCommands(capability: Command): void {
audio
.command("providers")
.description("List audio transcription providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const remoteProviders = [...buildMediaUnderstandingRegistry(undefined, cfg).values()]
.filter((provider) => provider.capabilities?.includes("audio"))
.map((provider) => ({
@@ -91,6 +94,7 @@ export function registerAudioCapabilityCommands(capability: Command): void {
configured: providerHasGenericConfig({
cfg,
providerId: provider.id,
agentId,
envVars: getProviderEnvVars(provider.id, {
config: cfg,
includeUntrustedWorkspacePlugins: false,
+5 -2
View File
@@ -17,6 +17,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
@@ -122,11 +123,12 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
embedding
.command("providers")
.description("List embedding providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveDefaultAgentId(cfg);
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const resolvedMemory = resolveMemorySearchConfig(cfg, agentId);
const selectedProvider = resolvedMemory?.provider;
const providers = new Map(
@@ -155,7 +157,7 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
providers.set(selectedProvider, {
id: selectedProvider,
defaultModel: resolvedMemory?.model || undefined,
transport: providerHasGenericConfig({ cfg, providerId: selectedProvider })
transport: providerHasGenericConfig({ cfg, providerId: selectedProvider, agentId })
? "remote"
: undefined,
autoSelectPriority: undefined,
@@ -168,6 +170,7 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
providerHasGenericConfig({
cfg,
providerId: provider.id,
agentId,
}),
selected: provider.id === selectedProvider,
id: provider.id,
+4 -1
View File
@@ -40,6 +40,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
} from "./shared.js";
@@ -407,10 +408,12 @@ export function registerImageCapabilityCommands(capability: Command): void {
image
.command("providers")
.description("List image generation providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const selectedProvider = resolveSelectedProviderFromModelRef(
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.mediaModels?.image),
);
@@ -418,7 +421,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
available: true,
configured:
selectedProvider === provider.id ||
providerHasGenericConfig({ cfg, providerId: provider.id }),
providerHasGenericConfig({ cfg, providerId: provider.id, agentId }),
selected: selectedProvider === provider.id,
id: provider.id,
label: provider.label,
+7 -7
View File
@@ -47,7 +47,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "model.providers",
description: "List model providers discovered from the catalog.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids with counts and defaults",
},
{
@@ -134,7 +134,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "image.providers",
description: "List image generation providers.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids and defaults",
},
{
@@ -148,7 +148,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "audio.providers",
description: "List audio transcription providers.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids and capabilities",
},
{
@@ -179,7 +179,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "tts.providers",
description: "List speech providers.",
transports: ["local", "gateway"],
flags: ["--local", "--gateway", "--json"],
flags: ["--agent", "--local", "--gateway", "--json"],
resultShape: "provider ids, configured state, models, voices",
},
{
@@ -254,7 +254,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "video.providers",
description: "List video generation and description providers.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids and defaults",
},
{
@@ -275,7 +275,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "web.providers",
description: "List web search and fetch providers.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids grouped by family",
},
{
@@ -289,7 +289,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "embedding.providers",
description: "List embedding providers.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "provider ids and default models",
},
];
+15 -8
View File
@@ -11,7 +11,11 @@ import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../../packages/gateway-protocol/src/client-info.js";
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import {
resolveAgentDir,
resolveAgentEffectiveModelPrimary,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import {
listProfilesForProvider,
loadAuthProfileStoreForRuntime,
@@ -27,7 +31,6 @@ import {
} from "../../agents/simple-completion-runtime.js";
import { normalizeThinkLevel, type ThinkLevel } from "../../auto-reply/thinking.js";
import { getRuntimeConfig } from "../../config/config.js";
import { resolveAgentModelPrimaryValue } from "../../config/model-input.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { callGateway, randomIdempotencyKey } from "../../gateway/call.js";
import { ADMIN_SCOPE } from "../../gateway/operator-scopes.js";
@@ -46,6 +49,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
resolveTransport,
@@ -54,8 +58,8 @@ import {
const LOCAL_MODEL_RUN_SYSTEM_PROMPT = "You are a personal assistant running inside OpenClaw.";
const HEIC_MODEL_RUN_MIMES = new Set(["image/heic", "image/heif"]);
async function loadModelCatalogForInspection(cfg: OpenClawConfig) {
const prepared = await loadPreparedModelCatalog({ config: cfg, readOnly: true });
async function loadModelCatalogForInspection(cfg: OpenClawConfig, agentId?: string) {
const prepared = await loadPreparedModelCatalog({ config: cfg, agentId, readOnly: true });
const metadataSnapshot = loadManifestMetadataSnapshot({ config: cfg, env: process.env });
const manifest = planEffectiveModelCatalogRows({
registry: metadataSnapshot.manifestRegistry,
@@ -342,11 +346,12 @@ async function runModelRun(params: {
} satisfies CapabilityEnvelope;
}
async function buildModelProviders() {
async function buildModelProviders(rawAgentId?: string) {
const cfg = getRuntimeConfig();
const catalog = await loadModelCatalogForInspection(cfg);
const agentId = resolveCapabilityProviderAgentId(cfg, rawAgentId);
const catalog = await loadModelCatalogForInspection(cfg, agentId);
const selectedProvider = resolveSelectedProviderFromModelRef(
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model),
resolveAgentEffectiveModelPrimary(cfg, agentId),
);
const grouped = new Map<
string,
@@ -368,6 +373,7 @@ async function buildModelProviders() {
configured: providerHasGenericConfig({
cfg,
providerId: entry.provider,
agentId,
envVars: getProviderEnvVars(entry.provider),
}),
selected: selectedProvider === entry.provider,
@@ -511,10 +517,11 @@ export function registerModelCapabilityCommands(capability: Command): void {
model
.command("providers")
.description("List model providers from the catalog")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await buildModelProviders();
const result = await buildModelProviders(opts.agent as string | undefined);
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, providerSummaryText);
});
});
+33 -4
View File
@@ -2,7 +2,8 @@ import {
parseStrictFiniteNumber,
parseStrictPositiveInteger,
} from "@openclaw/normalization-core/number-coercion";
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { listAgentIds, resolveSystemAgentTargetAgentId } from "../../agents/agent-scope-config.js";
import { resolveAgentDir } from "../../agents/agent-scope.js";
import {
listProfilesForProvider,
loadAuthProfileStoreForRuntime,
@@ -101,8 +102,32 @@ export function resolveSelectedProviderFromModelRef(
return resolveModelRefOverride(modelRef).provider;
}
function getAuthProfileIdsForProvider(cfg: OpenClawConfig, providerId: string): string[] {
const agentDir = resolveAgentDir(cfg, resolveDefaultAgentId(cfg));
export function resolveCapabilityProviderAgentId(
cfg: OpenClawConfig,
rawAgentId: string | undefined,
): string {
const requestedAgentId = rawAgentId?.trim();
if (rawAgentId !== undefined && !requestedAgentId) {
throw new Error("--agent must not be blank");
}
const agentId = resolveSystemAgentTargetAgentId(cfg, requestedAgentId, {
surface: "inference provider inspection",
hint: "Pass --agent <id> or set agents.defaults.systemAgent.agentId.",
});
if (!listAgentIds(cfg).includes(agentId)) {
throw new Error(
`Unknown agent id "${agentId}". Run \`openclaw agents list\` to see configured agents.`,
);
}
return agentId;
}
function getAuthProfileIdsForProvider(
cfg: OpenClawConfig,
providerId: string,
agentId: string,
): string[] {
const agentDir = resolveAgentDir(cfg, agentId);
const store = loadAuthProfileStoreForRuntime(agentDir);
return listProfilesForProvider(store, providerId);
}
@@ -110,6 +135,8 @@ function getAuthProfileIdsForProvider(cfg: OpenClawConfig, providerId: string):
export function providerHasGenericConfig(params: {
cfg: OpenClawConfig;
providerId: string;
/** Omit only for aggregate/global callers that intentionally exclude agent auth stores. */
agentId?: string;
envVars?: string[];
}): boolean {
const modelsProviders = (params.cfg.models?.providers ?? {}) as Record<string, unknown>;
@@ -123,7 +150,9 @@ export function providerHasGenericConfig(params: {
});
const envConfigured = envVars.some((envVar) => Boolean(process.env[envVar]?.trim()));
return (
getAuthProfileIdsForProvider(params.cfg, params.providerId).length > 0 ||
(params.agentId
? getAuthProfileIdsForProvider(params.cfg, params.providerId, params.agentId).length > 0
: false) ||
hasOwnKeys(modelsProviders[params.providerId]) ||
hasOwnKeys(pluginEntries[params.providerId]?.config) ||
hasOwnKeys(ttsProviders[params.providerId]) ||
+8 -2
View File
@@ -31,6 +31,7 @@ import type { CapabilityEnvelope, CapabilityTransport } from "./metadata.js";
import {
pinRuntimeConfigSnapshot,
providerHasGenericConfig,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
} from "./shared.js";
@@ -413,9 +414,12 @@ function resolvedTtsConfigHasProviderApiKey(config: unknown, providerId: string)
return ttsProviderConfigHasApiKey(config.providerConfigs[providerId]);
}
export async function runTtsProviders(transport: CapabilityTransport) {
export async function runTtsProviders(transport: CapabilityTransport, rawAgentId?: string) {
const cfg = getRuntimeConfig();
if (transport === "gateway") {
if (rawAgentId !== undefined) {
throw new Error("--agent is only supported with local TTS provider inspection.");
}
const payload: {
providers?: Array<Record<string, unknown>>;
active?: string;
@@ -441,6 +445,7 @@ export async function runTtsProviders(transport: CapabilityTransport) {
}),
};
}
const agentId = resolveCapabilityProviderAgentId(cfg, rawAgentId);
const config = resolveTtsConfig(cfg);
const prefsPath = resolveTtsPrefsPath(config);
const active = getTtsProvider(config, prefsPath);
@@ -448,7 +453,8 @@ export async function runTtsProviders(transport: CapabilityTransport) {
providers: listSpeechProviders(cfg).map((provider) => ({
available: true,
configured:
active === provider.id || providerHasGenericConfig({ cfg, providerId: provider.id }),
active === provider.id ||
providerHasGenericConfig({ cfg, providerId: provider.id, agentId }),
selected: active === provider.id,
id: provider.id,
name: provider.label,
+14 -10
View File
@@ -95,16 +95,20 @@ export function registerTtsCapabilityCommands(capability: Command): void {
});
});
for (const [name, description, run] of [
["providers", "List speech providers", runTtsProviders],
["personas", "List TTS personas", runTtsPersonas],
] as const) {
registerTransportTtsCommand(
tts.command(name).description(description),
"local",
(_, transport) => run(transport),
);
}
registerTransportTtsCommand(
tts
.command("providers")
.description("List speech providers")
.option("--agent <id>", "Agent whose provider state should be inspected"),
"local",
(opts, transport) => runTtsProviders(transport, opts.agent as string | undefined),
);
registerTransportTtsCommand(
tts.command("personas").description("List TTS personas"),
"local",
(_, transport) => runTtsPersonas(transport),
);
tts
.command("status")
+5 -2
View File
@@ -39,6 +39,7 @@ import {
parseOptionalTimeoutMs,
providerHasGenericConfig,
requireProviderModelOverride,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
} from "./shared.js";
@@ -310,10 +311,12 @@ export function registerVideoCapabilityCommands(capability: Command): void {
video
.command("providers")
.description("List video generation and description providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const selectedGenerationProvider = resolveSelectedProviderFromModelRef(
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.mediaModels?.video),
);
@@ -322,7 +325,7 @@ export function registerVideoCapabilityCommands(capability: Command): void {
available: true,
configured:
selectedGenerationProvider === provider.id ||
providerHasGenericConfig({ cfg, providerId: provider.id }),
providerHasGenericConfig({ cfg, providerId: provider.id, agentId }),
selected: selectedGenerationProvider === provider.id,
id: provider.id,
label: provider.label,
@@ -334,7 +337,7 @@ export function registerVideoCapabilityCommands(capability: Command): void {
.filter((provider) => provider.capabilities?.includes("video"))
.map((provider) => ({
available: true,
configured: providerHasGenericConfig({ cfg, providerId: provider.id }),
configured: providerHasGenericConfig({ cfg, providerId: provider.id, agentId }),
selected: false,
id: provider.id,
capabilities: provider.capabilities,
+6 -1
View File
@@ -1,5 +1,6 @@
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { Command } from "commander";
import { resolveAgentDir } from "../../agents/agent-scope.js";
import { getRuntimeConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js";
import {
@@ -22,6 +23,7 @@ import {
emitJsonOrText,
formatEnvelopeForText,
parseOptionalPositiveInteger,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
@@ -178,10 +180,13 @@ export function registerWebCapabilityCommands(capability: Command): void {
web
.command("providers")
.description("List web providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const agentDir = resolveAgentDir(cfg, agentId);
const selectedSearchProvider =
typeof cfg.tools?.web?.search?.provider === "string"
? normalizeLowercaseStringOrEmpty(cfg.tools.web.search.provider)
@@ -193,7 +198,7 @@ export function registerWebCapabilityCommands(capability: Command): void {
const result = {
search: listWebSearchProviders({ config: cfg }).map((provider) => ({
available: true,
configured: isWebSearchProviderConfigured({ provider, config: cfg }),
configured: isWebSearchProviderConfigured({ provider, config: cfg, agentDir }),
selected: provider.id === selectedSearchProvider,
id: provider.id,
envVars: provider.envVars,
+2 -1
View File
@@ -137,9 +137,10 @@ export function isWebSearchProviderConfigured(params: {
| "requiresCredential"
>;
config?: OpenClawConfig;
agentDir?: string;
}): boolean {
const config = resolveWebSearchRuntimeConfig({ config: params.config });
return hasEntryCredential(params.provider, config, resolveSearchConfig(config));
return hasEntryCredential(params.provider, config, resolveSearchConfig(config), params.agentDir);
}
/** Lists runtime web_search providers after applying runtime config snapshots. */