From 5cef288d651127fc959f8206dc6bbf2fc22708d9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 27 May 2026 23:23:22 +0100 Subject: [PATCH] fix(agents): resolve Codex runtime models first * fix(agents): resolve Codex runtime models first * test(agents): align Codex runtime resolution fixtures --- src/agents/embedded-agent-runner.e2e.test.ts | 102 ++++++++++++++++-- .../run.overflow-compaction.test.ts | 102 ++++++------------ src/agents/embedded-agent-runner/run.ts | 82 ++++++++------ .../embedded-agent-runner-e2e-mocks.ts | 30 ++++-- 4 files changed, 201 insertions(+), 115 deletions(-) diff --git a/src/agents/embedded-agent-runner.e2e.test.ts b/src/agents/embedded-agent-runner.e2e.test.ts index 00310e45d64..0ca9a4829ca 100644 --- a/src/agents/embedded-agent-runner.e2e.test.ts +++ b/src/agents/embedded-agent-runner.e2e.test.ts @@ -18,14 +18,24 @@ import { installEmbeddedRunnerFastRunE2eMocks, } from "./test-helpers/embedded-agent-runner-e2e-mocks.js"; +type EmbeddedRunnerModelResolution = + | ReturnType + | { + model?: undefined; + error: string; + authStorage: { setRuntimeApiKey: () => undefined }; + modelRegistry: Record; + }; + const runEmbeddedAttemptMock = vi.fn(); const disposeSessionMcpRuntimeMock = vi.fn<(sessionId: string) => Promise>(async () => { return undefined; }); const resolveSessionKeyForRequestMock = vi.fn(); const resolveStoredSessionKeyForSessionIdMock = vi.fn(); -const resolveModelAsyncMock = vi.fn(async (provider: string, modelId: string) => - createResolvedEmbeddedRunnerModel(provider, modelId), +const resolveModelAsyncMock = vi.fn( + async (provider: string, modelId: string): Promise => + createResolvedEmbeddedRunnerModel(provider, modelId), ); const ensureOpenClawModelsJsonMock = vi.fn(async () => ({ wrote: false })); const loggerWarnMock = vi.fn(); @@ -400,20 +410,92 @@ describe("runEmbeddedAgent", () => { expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( 1, - "openai", - "mock-1", - agentDir, - cfg, - expect.objectContaining({ skipAgentDiscovery: true }), - ); - expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( - 2, "openai-codex", "mock-1", agentDir, cfg, expect.objectContaining({ skipAgentDiscovery: true }), ); + expect(resolveModelAsyncMock).toHaveBeenCalledTimes(1); + expect( + (firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider, + ).toBe("openai-codex"); + }); + + it("resolves transport-owned OpenAI Codex runs against the runtime provider first", async () => { + const sessionFile = nextSessionFile(); + const baseConfig = createEmbeddedAgentRunnerOpenAiConfig([]); + const openAIProvider = baseConfig.models?.providers?.openai; + if (!openAIProvider) { + throw new Error("expected OpenAI provider test config"); + } + const cfg = { + ...baseConfig, + models: { + providers: { + openai: { + ...openAIProvider, + baseUrl: "https://api.openai.com/v1", + models: [], + }, + }, + }, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { id: "codex" }, + }, + }, + }, + }, + }; + resolveModelAsyncMock.mockImplementation(async (provider: string, modelId: string) => { + if (provider === "openai-codex" && modelId === "gpt-5.5") { + return createResolvedEmbeddedRunnerModel(provider, modelId); + } + return { + error: `Unknown model: ${provider}/${modelId}`, + authStorage: { + setRuntimeApiKey: () => undefined, + }, + modelRegistry: {}, + }; + }); + runEmbeddedAttemptMock.mockResolvedValueOnce( + makeEmbeddedRunnerAttempt({ + assistantTexts: ["ok"], + lastAssistant: buildEmbeddedRunnerAssistant({ + content: [{ type: "text", text: "ok" }], + }), + }), + ); + + await runEmbeddedAgent({ + sessionId: "codex-runtime-model", + sessionFile, + workspaceDir, + config: cfg, + prompt: "hello", + provider: "openai", + model: "gpt-5.5", + timeoutMs: 5_000, + agentDir, + agentHarnessId: "codex", + runId: nextRunId("codex-runtime-model"), + enqueue: immediateEnqueue, + }); + + expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( + 1, + "openai-codex", + "gpt-5.5", + agentDir, + cfg, + expect.objectContaining({ skipAgentDiscovery: true }), + ); + expect(resolveModelAsyncMock).toHaveBeenCalledTimes(1); + expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); expect( (firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider, ).toBe("openai-codex"); diff --git a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts index 69be4bf2bfa..be445a1ea55 100644 --- a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts @@ -974,29 +974,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { runAttempt: pluginRunAttempt, }); mockedEnsureAuthProfileStoreWithoutExternalProfiles.mockReturnValueOnce(codexAuthStore); - mockedResolveModelAsync - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai", - contextWindow: 200000, - api: "openai-responses", - }, - error: null, - authStorage: { setRuntimeApiKey: vi.fn() }, - modelRegistry: {}, - }) - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai-codex", - contextWindow: 200000, - api: "openai-codex-responses", - }, - error: null, - authStorage: codexAuthStorage, - modelRegistry: {}, - }); + mockedResolveModelAsync.mockResolvedValueOnce({ + model: { + id: "gpt-5.5", + provider: "openai-codex", + contextWindow: 200000, + api: "openai-codex-responses", + }, + error: null, + authStorage: codexAuthStorage, + modelRegistry: {}, + }); mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan); try { @@ -1101,29 +1089,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { ? ["openai-codex:default"] : []; }); - mockedResolveModelAsync - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai", - contextWindow: 200000, - api: "openai-responses", - }, - error: null, - authStorage: { setRuntimeApiKey: vi.fn() }, - modelRegistry: {}, - }) - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai-codex", - contextWindow: 200000, - api: "openai-codex-responses", - }, - error: null, - authStorage: codexAuthStorage, - modelRegistry: {}, - }); + mockedResolveModelAsync.mockResolvedValueOnce({ + model: { + id: "gpt-5.5", + provider: "openai-codex", + contextWindow: 200000, + api: "openai-codex-responses", + }, + error: null, + authStorage: codexAuthStorage, + modelRegistry: {}, + }); mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan); mockedGetApiKeyForModel.mockImplementation( async ({ profileId }: { profileId?: string } = {}) => { @@ -1280,29 +1256,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }); mockedEnsureAuthProfileStore.mockReturnValueOnce(codexAuthStore); mockedResolveAuthProfileOrder.mockReturnValueOnce(["openai-codex:sub", "openai-codex:backup"]); - mockedResolveModelAsync - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai", - contextWindow: 200000, - api: "openai-responses", - }, - error: null, - authStorage: { setRuntimeApiKey: vi.fn() }, - modelRegistry: {}, - }) - .mockResolvedValueOnce({ - model: { - id: "gpt-5.5", - provider: "openai-codex", - contextWindow: 200000, - api: "openai-codex-responses", - }, - error: null, - authStorage: codexAuthStorage, - modelRegistry: {}, - }); + mockedResolveModelAsync.mockResolvedValueOnce({ + model: { + id: "gpt-5.5", + provider: "openai-codex", + contextWindow: 200000, + api: "openai-codex-responses", + }, + error: null, + authStorage: codexAuthStorage, + modelRegistry: {}, + }); mockedBuildAgentRuntimePlan .mockReturnValueOnce(firstRuntimePlan) .mockReturnValueOnce(secondRuntimePlan); diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 877c1aa87c1..14675727629 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -639,46 +639,68 @@ export async function runEmbeddedAgent( config: params.config, workspaceDir: resolvedWorkspace, }); - const dynamicModelResolution = await resolveModelAsync( - provider, - modelId, - agentDir, - params.config, - { - // Plugin dynamic model hooks can resolve explicit model refs without - // first generating OpenClaw models.json. This keeps one-shot model runs from - // blocking on unrelated provider discovery. - skipAgentDiscovery: true, - workspaceDir: resolvedWorkspace, - }, - ); - let modelResolution = - dynamicModelResolution.model || pluginHarnessOwnsTransport - ? dynamicModelResolution - : await (async () => { - await ensureOpenClawModelsJson(params.config, agentDir, { - workspaceDir: resolvedWorkspace, - }); - return await resolveModelAsync(provider, modelId, agentDir, params.config, { - workspaceDir: resolvedWorkspace, - }); - })(); - if (selectedRuntimeProvider !== provider && modelResolution.model) { - const runtimeModelResolution = await resolveModelAsync( - selectedRuntimeProvider, + const modelResolutionProviders = + selectedRuntimeProvider !== provider ? [selectedRuntimeProvider, provider] : [provider]; + let resolvedModelProvider = provider; + let firstModelResolution: Awaited> | undefined; + let modelResolution: Awaited> | undefined; + for (const candidateProvider of modelResolutionProviders) { + const candidateResolution = await resolveModelAsync( + candidateProvider, modelId, agentDir, params.config, { + // Plugin dynamic model hooks can resolve explicit model refs without + // first generating OpenClaw models.json. This keeps one-shot model runs from + // blocking on unrelated provider discovery. skipAgentDiscovery: true, workspaceDir: resolvedWorkspace, }, ); - if (runtimeModelResolution.model) { - provider = selectedRuntimeProvider; - modelResolution = runtimeModelResolution; + firstModelResolution ??= candidateResolution; + if (candidateResolution.model) { + resolvedModelProvider = candidateProvider; + modelResolution = candidateResolution; + break; } } + if (!modelResolution && pluginHarnessOwnsTransport) { + modelResolution = firstModelResolution; + } + if (!modelResolution) { + await ensureOpenClawModelsJson(params.config, agentDir, { + workspaceDir: resolvedWorkspace, + }); + for (const candidateProvider of modelResolutionProviders) { + const candidateResolution = await resolveModelAsync( + candidateProvider, + modelId, + agentDir, + params.config, + { + workspaceDir: resolvedWorkspace, + }, + ); + firstModelResolution ??= candidateResolution; + if (candidateResolution.model) { + resolvedModelProvider = candidateProvider; + modelResolution = candidateResolution; + break; + } + } + } + modelResolution ??= firstModelResolution; + if (!modelResolution) { + throw new FailoverError(`Unknown model: ${provider}/${modelId}`, { + reason: "model_not_found", + provider, + model: modelId, + sessionId: params.sessionId, + lane: globalLane, + }); + } + provider = resolvedModelProvider; const { model, error, authStorage, modelRegistry } = modelResolution; if (!model) { throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, { diff --git a/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts index 0d7627720e9..f0dcf3f7c89 100644 --- a/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts +++ b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts @@ -54,12 +54,18 @@ export function installEmbeddedRunnerFastRunE2eMocks( options: EmbeddedRunnerFastRunMockOptions, ): void { vi.doMock("../harness/selection.js", () => ({ - selectAgentHarness: vi.fn((params: { provider?: string }) => ({ - id: params.provider === "codex-cli" ? "codex" : "openclaw", - label: "Mock agent harness", - supports: vi.fn(() => ({ supported: false })), - runAttempt: vi.fn(), - })), + selectAgentHarness: vi.fn( + (params: { + provider?: string; + agentHarnessId?: string; + agentHarnessRuntimeOverride?: string; + }) => ({ + id: resolveMockHarnessId(params), + label: "Mock agent harness", + supports: vi.fn(() => ({ supported: false })), + runAttempt: vi.fn(), + }), + ), resolveAgentHarnessPolicy: vi.fn(() => ({ runtime: "openclaw" })), runAgentHarnessAttempt: (params: unknown) => options.runEmbeddedAttempt(params), })); @@ -152,6 +158,18 @@ export function installEmbeddedRunnerFastRunE2eMocks( })); } +function resolveMockHarnessId(params: { + provider?: string; + agentHarnessId?: string; + agentHarnessRuntimeOverride?: string; +}): "codex" | "openclaw" { + return params.provider === "codex-cli" || + params.agentHarnessId === "codex" || + params.agentHarnessRuntimeOverride === "codex" + ? "codex" + : "openclaw"; +} + export function installEmbeddedRunnerBackoffE2eMocks( options: EmbeddedRunnerBackoffMockOptions, ): void {