diff --git a/src/agents/command/attempt-execution.cli.test.ts b/src/agents/command/attempt-execution.cli.test.ts index 4b56dc3cac6..b7e13c70921 100644 --- a/src/agents/command/attempt-execution.cli.test.ts +++ b/src/agents/command/attempt-execution.cli.test.ts @@ -562,6 +562,61 @@ describe("CLI attempt execution", () => { ); }); + it("routes canonical OpenAI models through the configured Codex CLI runtime", async () => { + const sessionKey = "agent:main:direct:canonical-codex-cli"; + const sessionEntry: SessionEntry = { + sessionId: "openclaw-session-canonical-codex-cli", + updatedAt: Date.now(), + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + runCliAgentMock.mockResolvedValueOnce(makeCliResult("canonical codex cli")); + + await runAgentAttempt({ + providerOverride: "openai", + originalProvider: "openai", + modelOverride: "gpt-5.4", + cfg: { + agents: { + defaults: { + agentRuntime: { id: "codex-cli", fallback: "none" }, + }, + }, + } as OpenClawConfig, + sessionEntry, + sessionId: sessionEntry.sessionId, + sessionKey, + sessionAgentId: "main", + sessionFile: path.join(tmpDir, "session.jsonl"), + workspaceDir: tmpDir, + body: "route this", + isFallbackRetry: false, + resolvedThinkLevel: "medium", + timeoutMs: 1_000, + runId: "run-canonical-codex-cli", + opts: { senderIsOwner: false } as Parameters[0]["opts"], + runContext: {} as Parameters[0]["runContext"], + spawnedBy: undefined, + messageChannel: "telegram", + skillsSnapshot: undefined, + resolvedVerboseLevel: undefined, + agentDir: tmpDir, + onAgentEvent: vi.fn(), + authProfileProvider: "openai", + sessionStore, + storePath, + sessionHasHistory: false, + }); + + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runCliAgentMock).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "codex-cli", + model: "gpt-5.4", + }), + ); + }); + it("keeps one-shot model runs on the raw embedded provider path", async () => { const sessionKey = "agent:main:direct:model-run-raw"; const sessionEntry: SessionEntry = { diff --git a/src/gateway/gateway-cli-backend.live-helpers.test.ts b/src/gateway/gateway-cli-backend.live-helpers.test.ts index fa246caa573..13972561be8 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.test.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.test.ts @@ -115,6 +115,38 @@ describe("gateway cli backend live helpers", () => { expect(shouldRunCliModelSwitchProbe("codex-cli", "codex-cli/gpt-5.5")).toBe(false); }); + it("configures legacy CLI model refs as canonical provider models plus CLI runtime", async () => { + const { resolveCliBackendLiveModelSelection } = + await import("./gateway-cli-backend.live-helpers.js"); + + expect( + resolveCliBackendLiveModelSelection({ + rawModel: "codex-cli/gpt-5.4", + defaultProvider: "claude-cli", + }), + ).toEqual({ + providerId: "codex-cli", + cliModelKey: "codex-cli/gpt-5.4", + configModelKey: "openai/gpt-5.4", + configModelSwitchTarget: undefined, + agentRuntime: { id: "codex-cli", fallback: "none" }, + }); + + expect( + resolveCliBackendLiveModelSelection({ + rawModel: "claude-cli/claude-sonnet-4-6", + defaultProvider: "claude-cli", + modelSwitchTarget: "claude-cli/claude-opus-4-6", + }), + ).toEqual({ + providerId: "claude-cli", + cliModelKey: "claude-cli/claude-sonnet-4-6", + configModelKey: "anthropic/claude-sonnet-4-6", + configModelSwitchTarget: "anthropic/claude-opus-4-6", + agentRuntime: { id: "claude-cli", fallback: "none" }, + }); + }); + it("lets env disable the model switch probe", async () => { const { shouldRunCliModelSwitchProbe } = await import("./gateway-cli-backend.live-helpers.js"); diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index 629070a6692..7c2932dd2d0 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { resolveCliBackendLiveTest } from "../agents/cli-backends.js"; +import { migrateLegacyRuntimeModelRef } from "../agents/model-runtime-aliases.js"; +import { parseModelRef } from "../agents/model-selection.js"; import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, @@ -33,6 +35,14 @@ export type SystemPromptReport = { injectedWorkspaceFiles?: Array<{ name?: string }>; }; +export type CliBackendLiveModelSelection = { + providerId: string; + cliModelKey: string; + configModelKey: string; + configModelSwitchTarget: string | undefined; + agentRuntime: { id: string; fallback: "pi" | "none" }; +}; + export type CliBackendLiveEnvSnapshot = { configPath?: string; stateDir?: string; @@ -49,6 +59,41 @@ export type CliBackendLiveEnvSnapshot = { anthropicApiKeyOld?: string; }; +export function resolveCliBackendLiveModelSelection(params: { + rawModel: string; + defaultProvider: string; + modelSwitchTarget?: string; +}): CliBackendLiveModelSelection { + const parsed = parseModelRef(params.rawModel, params.defaultProvider); + if (!parsed) { + throw new Error( + `OPENCLAW_LIVE_CLI_BACKEND_MODEL must resolve to a CLI backend model. Got: ${params.rawModel}`, + ); + } + + const migrated = migrateLegacyRuntimeModelRef(params.rawModel); + if (migrated?.cli) { + return { + providerId: migrated.runtime, + cliModelKey: `${migrated.runtime}/${migrated.model}`, + configModelKey: migrated.ref, + configModelSwitchTarget: params.modelSwitchTarget + ? (migrateLegacyRuntimeModelRef(params.modelSwitchTarget)?.ref ?? params.modelSwitchTarget) + : undefined, + agentRuntime: { id: migrated.runtime, fallback: "none" }, + }; + } + + const modelKey = `${parsed.provider}/${parsed.model}`; + return { + providerId: parsed.provider, + cliModelKey: modelKey, + configModelKey: modelKey, + configModelSwitchTarget: params.modelSwitchTarget, + agentRuntime: { id: "pi", fallback: "pi" }, + }; +} + export function parseJsonStringArray(name: string, raw?: string): string[] | undefined { const trimmed = raw?.trim(); if (!trimmed) { diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index b79e8ed224a..af7b91a8d14 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -17,6 +17,7 @@ import { parseImageMode, resolveCliModelSwitchProbeTarget, resolveCliBackendLiveArgs, + resolveCliBackendLiveModelSelection, parseJsonStringArray, restoreCliBackendLiveEnv, shouldRunCliImageProbe, @@ -204,25 +205,34 @@ describeLive("gateway live (cli backend)", () => { logCliBackendLiveStep("env-ready", { port }); const rawModel = process.env.OPENCLAW_LIVE_CLI_BACKEND_MODEL ?? DEFAULT_MODEL; - const parsed = parseModelRef(rawModel, "claude-cli"); - if (!parsed) { - throw new Error( - `OPENCLAW_LIVE_CLI_BACKEND_MODEL must resolve to a CLI backend model. Got: ${rawModel}`, - ); - } - - const providerId = parsed.provider; - const modelKey = `${providerId}/${parsed.model}`; + const initialParsed = parseModelRef(rawModel, "claude-cli"); + const initialProviderId = initialParsed?.provider ?? ""; + const initialModelKey = initialParsed + ? `${initialProviderId}/${initialParsed.model}` + : rawModel; + const initialModelSwitchTarget = resolveCliModelSwitchProbeTarget( + initialProviderId, + initialModelKey, + ); + const modelSelection = resolveCliBackendLiveModelSelection({ + rawModel, + defaultProvider: "claude-cli", + modelSwitchTarget: initialModelSwitchTarget, + }); + const providerId = modelSelection.providerId; + const modelKey = modelSelection.cliModelKey; + const configModelKey = modelSelection.configModelKey; const backendResolved = resolveCliBackendConfig(providerId); const enableCliImageProbe = shouldRunCliImageProbe(providerId); const enableCliMcpProbe = shouldRunCliMcpProbe(providerId); const enableCliModelSwitchProbe = shouldRunCliModelSwitchProbe(providerId, modelKey); const modelSwitchTarget = enableCliModelSwitchProbe - ? resolveCliModelSwitchProbeTarget(providerId, modelKey) + ? modelSelection.configModelSwitchTarget : undefined; logCliBackendLiveStep("model-selected", { providerId, modelKey, + configModelKey, enableCliImageProbe, enableCliMcpProbe, enableCliModelSwitchProbe, @@ -328,7 +338,7 @@ describeLive("gateway live (cli backend)", () => { providers: { ...cfg.models?.providers, openai: { - ...openAiProviderConfigForCodexCli(modelKey), + ...openAiProviderConfigForCodexCli(configModelKey), ...cfg.models?.providers?.openai, }, }, @@ -347,12 +357,12 @@ describeLive("gateway live (cli backend)", () => { defaults: { ...cfg.agents?.defaults, ...(bootstrapWorkspace ? { workspace: bootstrapWorkspace.workspaceRootDir } : {}), - model: { primary: modelKey }, + model: { primary: configModelKey }, models: { - [modelKey]: {}, + [configModelKey]: {}, ...(modelSwitchTarget ? { [modelSwitchTarget]: {} } : {}), }, - agentRuntime: { id: "pi", fallback: "pi" }, + agentRuntime: modelSelection.agentRuntime, cliBackends: { ...existingBackends, [providerId]: {