fix(cli): allow agent selection for direct inference (#125143)

* fix(cli): inherit inference agent options

* fix(cli): route inference model auth agents

* fix(cli): route video describe agent

* fix(cli): route audio transcription agent

* fix(cli): preserve inherited agent selectors

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
This commit is contained in:
Omar Shahine
2026-08-19 21:27:09 -07:00
committed by GitHub
parent 3fe833dfa4
commit 641ccba793
10 changed files with 562 additions and 59 deletions
+15 -2
View File
@@ -1,5 +1,6 @@
import path from "node:path";
import type { Command } from "commander";
import { resolveAgentDir } from "../../agents/agent-scope.js";
import { getRuntimeConfig } from "../../config/config.js";
import { inspectLocalAudioSelection } from "../../media-understanding/local-audio.js";
import { buildMediaUnderstandingRegistry } from "../../media-understanding/provider-registry.js";
@@ -16,6 +17,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityAgentOption,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
@@ -25,15 +27,21 @@ async function runAudioTranscribe(params: {
language?: string;
model?: string;
prompt?: string;
agent?: string;
}) {
const cfg = await resolveLocalCapabilityRuntimeConfig({
commandName: "infer audio transcribe",
targetIds: getModelsCommandSecretTargetIds(),
});
const agentDir = resolveAgentDir(
cfg,
resolveCapabilityProviderAgentId(cfg, params.agent, "infer audio transcribe"),
);
const activeModel = requireProviderModelOverride(params.model);
const result = await transcribeAudioFile({
filePath: path.resolve(params.file),
cfg,
agentDir,
language: params.language,
activeModel,
prompt: params.prompt,
@@ -56,20 +64,25 @@ async function runAudioTranscribe(params: {
}
export function registerAudioCapabilityCommands(capability: Command): void {
const audio = capability.command("audio").description("Audio transcription");
const audio = capability
.command("audio")
.description("Audio transcription")
.option("--agent <id>", "Agent whose model and auth state should be used");
audio
.command("transcribe")
.description("Transcribe one audio file")
.requiredOption("--file <path>", "Audio file")
.option("--agent <id>", "Agent whose model and auth state should be used")
.option("--language <code>", "Language hint")
.option("--prompt <text>", "Prompt hint")
.option("--model <provider/model>", "Model override")
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runAudioTranscribe({
file: String(opts.file),
agent: resolveCapabilityAgentOption(command, opts.agent),
language: opts.language as string | undefined,
model: opts.model as string | undefined,
prompt: opts.prompt as string | undefined,
+12 -5
View File
@@ -17,6 +17,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityAgentOption,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
@@ -102,7 +103,10 @@ async function runMemoryEmbeddingCreate(params: {
}
export function registerEmbeddingCapabilityCommands(capability: Command): void {
const embedding = capability.command("embedding").description("Embedding providers");
const embedding = capability
.command("embedding")
.description("Embedding providers")
.option("--agent <id>", "Agent whose model and auth state should be used");
embedding
.command("create")
@@ -115,13 +119,13 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
"Agent whose saved provider auth is used (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runMemoryEmbeddingCreate({
texts: opts.text as string[],
agent: resolveCapabilityAgentOption(command, opts.agent),
provider: opts.provider as string | undefined,
model: opts.model as string | undefined,
agent: typeof opts.agent === "string" ? opts.agent : undefined,
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -132,10 +136,13 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
.description("List embedding providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const agentId = resolveCapabilityProviderAgentId(
cfg,
resolveCapabilityAgentOption(command, opts.agent),
);
const resolvedMemory = resolveMemorySearchConfig(cfg, agentId);
const selectedProvider = resolvedMemory?.provider;
const providers = new Map(
+20 -13
View File
@@ -40,6 +40,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityAgentOption,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
@@ -313,8 +314,9 @@ function addImageGenerationOptions(command: Command): Command {
.option("--json", "Output JSON", false);
}
function resolveImageGenerationOptions(opts: Record<string, unknown>) {
function resolveImageGenerationOptions(opts: Record<string, unknown>, command: Command) {
return {
agent: resolveCapabilityAgentOption(command, opts.agent),
model: opts.model as string | undefined,
count: parseOptionalPositiveInteger(opts.count, "--count"),
size: opts.size as string | undefined,
@@ -330,24 +332,26 @@ function resolveImageGenerationOptions(opts: Record<string, unknown>) {
quality: normalizeImageQuality(opts.quality as string | undefined),
timeoutMs: parseOptionalTimeoutMs(opts.timeoutMs as string | number | undefined),
output: opts.output as string | undefined,
agent: typeof opts.agent === "string" ? opts.agent : undefined,
};
}
export function registerImageCapabilityCommands(capability: Command): void {
const image = capability.command("image").description("Image generation and description");
const image = capability
.command("image")
.description("Image generation and description")
.option("--agent <id>", "Agent whose model and auth state should be used");
addImageGenerationOptions(
image
.command("generate")
.description("Generate images")
.requiredOption("--prompt <text>", "Prompt text"),
).action(async (opts) => {
).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runImageGenerate({
capability: "image.generate",
prompt: String(opts.prompt),
...resolveImageGenerationOptions(opts),
...resolveImageGenerationOptions(opts, command),
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -359,14 +363,14 @@ export function registerImageCapabilityCommands(capability: Command): void {
.description("Edit images with one or more input files")
.requiredOption("--file <path>", "Input file", collectOption, [])
.requiredOption("--prompt <text>", "Prompt text"),
).action(async (opts) => {
).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const files = Array.isArray(opts.file) ? (opts.file as string[]) : [String(opts.file)];
const result = await runImageGenerate({
capability: "image.edit",
prompt: String(opts.prompt),
file: files,
...resolveImageGenerationOptions(opts),
...resolveImageGenerationOptions(opts, command),
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -384,7 +388,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
"Agent whose saved provider auth is used (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runImageDescribe({
capability: "image.describe",
@@ -392,7 +396,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
model: opts.model as string | undefined,
prompt: opts.prompt as string | undefined,
timeoutMs: parseOptionalTimeoutMs(opts.timeoutMs),
agent: typeof opts.agent === "string" ? opts.agent : undefined,
agent: resolveCapabilityAgentOption(command, opts.agent),
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -410,7 +414,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
"Agent whose saved provider auth is used (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runImageDescribe({
capability: "image.describe-many",
@@ -418,7 +422,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
model: opts.model as string | undefined,
prompt: opts.prompt as string | undefined,
timeoutMs: parseOptionalTimeoutMs(opts.timeoutMs),
agent: typeof opts.agent === "string" ? opts.agent : undefined,
agent: resolveCapabilityAgentOption(command, opts.agent),
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -429,10 +433,13 @@ export function registerImageCapabilityCommands(capability: Command): void {
.description("List image generation providers")
.option("--agent <id>", "Agent whose provider state should be inspected")
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const agentId = resolveCapabilityProviderAgentId(
cfg,
resolveCapabilityAgentOption(command, opts.agent),
);
const selectedProvider = resolveSelectedProviderFromModelRef(
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.mediaModels?.image),
);
+4 -4
View File
@@ -63,7 +63,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "model.auth.login",
description: "Run the existing provider auth login flow.",
transports: ["local"],
flags: ["--provider", "--method"],
flags: ["--provider", "--method", "--agent"],
resultShape: "interactive auth result",
},
{
@@ -77,7 +77,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "model.auth.status",
description: "Show configured model auth state.",
transports: ["local"],
flags: ["--json"],
flags: ["--agent", "--json"],
resultShape: "model status summary",
},
{
@@ -152,7 +152,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "audio.transcribe",
description: "Transcribe one audio file.",
transports: ["local"],
flags: ["--file", "--language", "--prompt", "--model", "--json"],
flags: ["--file", "--agent", "--language", "--prompt", "--model", "--json"],
resultShape: "normalized text output",
},
{
@@ -259,7 +259,7 @@ export const CAPABILITY_METADATA: CapabilityMetadata[] = [
id: "video.describe",
description: "Describe one video file through media-understanding providers.",
transports: ["local"],
flags: ["--file", "--model", "--json"],
flags: ["--file", "--agent", "--model", "--json"],
resultShape: "normalized text output",
},
{
+33 -16
View File
@@ -45,6 +45,7 @@ import {
providerHasGenericConfig,
providerSummaryText,
requireProviderModelOverride,
resolveCapabilityAgentOption,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
@@ -384,11 +385,11 @@ async function buildModelProviders(rawAgentId?: string) {
return [...grouped.values()].toSorted((a, b) => a.provider.localeCompare(b.provider));
}
async function runModelAuthStatus() {
async function runModelAuthStatus(agent: string) {
const captured: string[] = [];
const { modelsStatusCommand } = await import("../../commands/models/list.status-command.js");
await modelsStatusCommand(
{ json: true },
{ json: true, agent },
{
log: (...args) => captured.push(args.join(" ")),
error: (message) => {
@@ -403,10 +404,9 @@ async function runModelAuthStatus() {
return raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
}
async function runModelAuthLogout(provider: string, agent?: string) {
async function runModelAuthLogout(provider: string, agent: string) {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, agent, "infer model auth logout");
const agentDir = resolveAgentDir(cfg, agentId);
const agentDir = resolveAgentDir(cfg, agent);
const store = loadAuthProfileStoreForRuntime(agentDir);
const profileIds = listProfilesForProvider(store, provider);
const updated = await updateAuthProfileStoreWithLock({
@@ -446,7 +446,8 @@ async function runModelAuthLogout(provider: string, agent?: string) {
export function registerModelCapabilityCommands(capability: Command): void {
const model = capability
.command("model")
.description("Text inference and model catalog commands");
.description("Text inference and model catalog commands")
.option("--agent <id>", "Agent whose model and auth state should be used");
model
.command("run")
@@ -462,7 +463,7 @@ export function registerModelCapabilityCommands(capability: Command): void {
"Agent whose model and credentials own the run (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const prompt = requireModelRunPrompt(opts.prompt);
const thinking = normalizeModelRunThinking(opts.thinking);
@@ -474,11 +475,11 @@ export function registerModelCapabilityCommands(capability: Command): void {
});
const result = await runModelRun({
prompt,
agent: resolveCapabilityAgentOption(command, opts.agent),
files: opts.file as string[] | undefined,
model: opts.model as string | undefined,
thinking,
transport,
agent: typeof opts.agent === "string" ? opts.agent : undefined,
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -521,27 +522,40 @@ export function registerModelCapabilityCommands(capability: Command): void {
.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) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await buildModelProviders(opts.agent as string | undefined);
const result = await buildModelProviders(resolveCapabilityAgentOption(command, opts.agent));
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, providerSummaryText);
});
});
const modelAuth = model.command("auth").description("Provider auth helpers");
const modelAuth = model
.command("auth")
.description("Provider auth helpers")
.option("--agent <id>", "Agent id (default: configured default agent)");
const resolveModelAuthAgent = (command: Command, rawAgentId: unknown, surface: string) =>
resolveCapabilityProviderAgentId(
getRuntimeConfig(),
resolveCapabilityAgentOption(command, rawAgentId),
surface,
);
modelAuth
.command("login")
.description("Run provider auth login")
.requiredOption("--provider <id>", "Provider id")
.option("--method <id>", "Provider auth method id")
.action(async (opts) => {
.option("--agent <id>", "Agent id (default: configured default agent)")
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const agent = resolveModelAuthAgent(command, opts.agent, "infer model auth login");
const { modelsAuthLoginCommand } = await import("../../commands/models/auth.js");
await modelsAuthLoginCommand(
{
provider: String(opts.provider),
method: opts.method ? String(opts.method) : undefined,
agent,
},
defaultRuntime,
);
@@ -557,11 +571,11 @@ export function registerModelCapabilityCommands(capability: Command): void {
"Agent id (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runModelAuthLogout(
String(opts.provider),
typeof opts.agent === "string" ? opts.agent : undefined,
resolveModelAuthAgent(command, opts.agent, "infer model auth logout"),
);
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) =>
JSON.stringify(value, null, 2),
@@ -572,10 +586,13 @@ export function registerModelCapabilityCommands(capability: Command): void {
modelAuth
.command("status")
.description("Show configured auth state")
.option("--agent <id>", "Agent id (default: configured default agent)")
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runModelAuthStatus();
const result = await runModelAuthStatus(
resolveModelAuthAgent(command, opts.agent, "infer model auth status"),
);
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) =>
JSON.stringify(value, null, 2),
);
+10
View File
@@ -2,6 +2,7 @@ import {
parseStrictFiniteNumber,
parseStrictPositiveInteger,
} from "@openclaw/normalization-core/number-coercion";
import type { Command } from "commander";
import { listAgentIds, resolveAgentOperationAgentId } from "../../agents/agent-scope-config.js";
import { resolveAgentDir } from "../../agents/agent-scope.js";
import {
@@ -17,6 +18,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { writeRuntimeJson, defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
import { resolveCommandConfigWithSecrets } from "../command-config-resolution.js";
import { inheritOptionFromParent } from "../command-options.js";
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
import type { CapabilityEnvelope, CapabilityTransport } from "./metadata.js";
@@ -123,6 +125,14 @@ export function resolveCapabilityProviderAgentId(
return agentId;
}
export function resolveCapabilityAgentOption(
command: Command | undefined,
rawAgentId: unknown,
): string | undefined {
return typeof rawAgentId === "string"
? rawAgentId
: inheritOptionFromParent<string>(command, "agent");
}
function getAuthProfileIdsForProvider(
cfg: OpenClawConfig,
providerId: string,
+21 -7
View File
@@ -39,6 +39,7 @@ import {
parseOptionalTimeoutMs,
providerHasGenericConfig,
requireProviderModelOverride,
resolveCapabilityAgentOption,
resolveCapabilityProviderAgentId,
resolveLocalCapabilityRuntimeConfig,
resolveSelectedProviderFromModelRef,
@@ -234,15 +235,20 @@ async function runVideoGenerate(params: {
} satisfies CapabilityEnvelope;
}
async function runVideoDescribe(params: { file: string; model?: string }) {
async function runVideoDescribe(params: { file: string; model?: string; agent?: string }) {
const cfg = await resolveLocalCapabilityRuntimeConfig({
commandName: "infer video.describe",
targetIds: getModelsCommandSecretTargetIds(),
});
const agentDir = resolveAgentDir(
cfg,
resolveCapabilityProviderAgentId(cfg, params.agent, "infer video describe"),
);
const activeModel = requireProviderModelOverride(params.model);
const result = await describeVideoFile({
filePath: path.resolve(params.file),
cfg,
agentDir,
activeModel,
});
if (!result.text) {
@@ -260,7 +266,10 @@ async function runVideoDescribe(params: { file: string; model?: string }) {
}
export function registerVideoCapabilityCommands(capability: Command): void {
const video = capability.command("video").description("Video generation and description");
const video = capability
.command("video")
.description("Video generation and description")
.option("--agent <id>", "Agent whose model and auth state should be used");
video
.command("generate")
@@ -280,10 +289,11 @@ export function registerVideoCapabilityCommands(capability: Command): void {
"Agent whose saved provider auth is used (default: agents.defaults.systemAgent.agentId, then the sole agent)",
)
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runVideoGenerate({
prompt: String(opts.prompt),
agent: resolveCapabilityAgentOption(command, opts.agent),
model: opts.model as string | undefined,
output: opts.output as string | undefined,
size: opts.size as string | undefined,
@@ -293,7 +303,6 @@ export function registerVideoCapabilityCommands(capability: Command): void {
audio: opts.audio === true ? true : undefined,
watermark: opts.watermark === true ? true : undefined,
timeoutMs: parseOptionalTimeoutMs(opts.timeoutMs),
agent: typeof opts.agent === "string" ? opts.agent : undefined,
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
@@ -303,12 +312,14 @@ export function registerVideoCapabilityCommands(capability: Command): void {
.command("describe")
.description("Describe one video file")
.requiredOption("--file <path>", "Video file")
.option("--agent <id>", "Agent whose model and auth state should be used")
.option("--model <provider/model>", "Model override")
.option("--json", "Output JSON", false)
.action(async (opts) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runVideoDescribe({
file: String(opts.file),
agent: resolveCapabilityAgentOption(command, opts.agent),
model: opts.model as string | undefined,
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
@@ -320,10 +331,13 @@ export function registerVideoCapabilityCommands(capability: Command): void {
.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) => {
.action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, opts.agent as string | undefined);
const agentId = resolveCapabilityProviderAgentId(
cfg,
resolveCapabilityAgentOption(command, opts.agent),
);
const selectedGenerationProvider = resolveSelectedProviderFromModelRef(
resolveAgentModelPrimaryValue(cfg.agents?.defaults?.mediaModels?.video),
);