mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
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:
committed by
GitHub
parent
722e1ff48e
commit
72e67904be
+8
-3
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]) ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user