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
+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,