Skip to content

Commit 5cef288

Browse files
authored
fix(agents): resolve Codex runtime models first
* fix(agents): resolve Codex runtime models first * test(agents): align Codex runtime resolution fixtures
1 parent f3e2851 commit 5cef288

4 files changed

Lines changed: 196 additions & 110 deletions

File tree

src/agents/embedded-agent-runner.e2e.test.ts

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,24 @@ import {
1818
installEmbeddedRunnerFastRunE2eMocks,
1919
} from "./test-helpers/embedded-agent-runner-e2e-mocks.js";
2020

21+
type EmbeddedRunnerModelResolution =
22+
| ReturnType<typeof createResolvedEmbeddedRunnerModel>
23+
| {
24+
model?: undefined;
25+
error: string;
26+
authStorage: { setRuntimeApiKey: () => undefined };
27+
modelRegistry: Record<string, never>;
28+
};
29+
2130
const runEmbeddedAttemptMock = vi.fn();
2231
const disposeSessionMcpRuntimeMock = vi.fn<(sessionId: string) => Promise<void>>(async () => {
2332
return undefined;
2433
});
2534
const resolveSessionKeyForRequestMock = vi.fn();
2635
const resolveStoredSessionKeyForSessionIdMock = vi.fn();
27-
const resolveModelAsyncMock = vi.fn(async (provider: string, modelId: string) =>
28-
createResolvedEmbeddedRunnerModel(provider, modelId),
36+
const resolveModelAsyncMock = vi.fn(
37+
async (provider: string, modelId: string): Promise<EmbeddedRunnerModelResolution> =>
38+
createResolvedEmbeddedRunnerModel(provider, modelId),
2939
);
3040
const ensureOpenClawModelsJsonMock = vi.fn(async () => ({ wrote: false }));
3141
const loggerWarnMock = vi.fn();
@@ -400,20 +410,92 @@ describe("runEmbeddedAgent", () => {
400410

401411
expect(resolveModelAsyncMock).toHaveBeenNthCalledWith(
402412
1,
403-
"openai",
413+
"openai-codex",
404414
"mock-1",
405415
agentDir,
406416
cfg,
407417
expect.objectContaining({ skipAgentDiscovery: true }),
408418
);
419+
expect(resolveModelAsyncMock).toHaveBeenCalledTimes(1);
420+
expect(
421+
(firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider,
422+
).toBe("openai-codex");
423+
});
424+
425+
it("resolves transport-owned OpenAI Codex runs against the runtime provider first", async () => {
426+
const sessionFile = nextSessionFile();
427+
const baseConfig = createEmbeddedAgentRunnerOpenAiConfig([]);
428+
const openAIProvider = baseConfig.models?.providers?.openai;
429+
if (!openAIProvider) {
430+
throw new Error("expected OpenAI provider test config");
431+
}
432+
const cfg = {
433+
...baseConfig,
434+
models: {
435+
providers: {
436+
openai: {
437+
...openAIProvider,
438+
baseUrl: "https://api.openai.com/v1",
439+
models: [],
440+
},
441+
},
442+
},
443+
agents: {
444+
defaults: {
445+
models: {
446+
"openai/gpt-5.5": {
447+
agentRuntime: { id: "codex" },
448+
},
449+
},
450+
},
451+
},
452+
};
453+
resolveModelAsyncMock.mockImplementation(async (provider: string, modelId: string) => {
454+
if (provider === "openai-codex" && modelId === "gpt-5.5") {
455+
return createResolvedEmbeddedRunnerModel(provider, modelId);
456+
}
457+
return {
458+
error: `Unknown model: ${provider}/${modelId}`,
459+
authStorage: {
460+
setRuntimeApiKey: () => undefined,
461+
},
462+
modelRegistry: {},
463+
};
464+
});
465+
runEmbeddedAttemptMock.mockResolvedValueOnce(
466+
makeEmbeddedRunnerAttempt({
467+
assistantTexts: ["ok"],
468+
lastAssistant: buildEmbeddedRunnerAssistant({
469+
content: [{ type: "text", text: "ok" }],
470+
}),
471+
}),
472+
);
473+
474+
await runEmbeddedAgent({
475+
sessionId: "codex-runtime-model",
476+
sessionFile,
477+
workspaceDir,
478+
config: cfg,
479+
prompt: "hello",
480+
provider: "openai",
481+
model: "gpt-5.5",
482+
timeoutMs: 5_000,
483+
agentDir,
484+
agentHarnessId: "codex",
485+
runId: nextRunId("codex-runtime-model"),
486+
enqueue: immediateEnqueue,
487+
});
488+
409489
expect(resolveModelAsyncMock).toHaveBeenNthCalledWith(
410-
2,
490+
1,
411491
"openai-codex",
412-
"mock-1",
492+
"gpt-5.5",
413493
agentDir,
414494
cfg,
415495
expect.objectContaining({ skipAgentDiscovery: true }),
416496
);
497+
expect(resolveModelAsyncMock).toHaveBeenCalledTimes(1);
498+
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
417499
expect(
418500
(firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider,
419501
).toBe("openai-codex");

src/agents/embedded-agent-runner/run.overflow-compaction.test.ts

Lines changed: 33 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -974,29 +974,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
974974
runAttempt: pluginRunAttempt,
975975
});
976976
mockedEnsureAuthProfileStoreWithoutExternalProfiles.mockReturnValueOnce(codexAuthStore);
977-
mockedResolveModelAsync
978-
.mockResolvedValueOnce({
979-
model: {
980-
id: "gpt-5.5",
981-
provider: "openai",
982-
contextWindow: 200000,
983-
api: "openai-responses",
984-
},
985-
error: null,
986-
authStorage: { setRuntimeApiKey: vi.fn() },
987-
modelRegistry: {},
988-
})
989-
.mockResolvedValueOnce({
990-
model: {
991-
id: "gpt-5.5",
992-
provider: "openai-codex",
993-
contextWindow: 200000,
994-
api: "openai-codex-responses",
995-
},
996-
error: null,
997-
authStorage: codexAuthStorage,
998-
modelRegistry: {},
999-
});
977+
mockedResolveModelAsync.mockResolvedValueOnce({
978+
model: {
979+
id: "gpt-5.5",
980+
provider: "openai-codex",
981+
contextWindow: 200000,
982+
api: "openai-codex-responses",
983+
},
984+
error: null,
985+
authStorage: codexAuthStorage,
986+
modelRegistry: {},
987+
});
1000988
mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan);
1001989

1002990
try {
@@ -1101,29 +1089,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
11011089
? ["openai-codex:default"]
11021090
: [];
11031091
});
1104-
mockedResolveModelAsync
1105-
.mockResolvedValueOnce({
1106-
model: {
1107-
id: "gpt-5.5",
1108-
provider: "openai",
1109-
contextWindow: 200000,
1110-
api: "openai-responses",
1111-
},
1112-
error: null,
1113-
authStorage: { setRuntimeApiKey: vi.fn() },
1114-
modelRegistry: {},
1115-
})
1116-
.mockResolvedValueOnce({
1117-
model: {
1118-
id: "gpt-5.5",
1119-
provider: "openai-codex",
1120-
contextWindow: 200000,
1121-
api: "openai-codex-responses",
1122-
},
1123-
error: null,
1124-
authStorage: codexAuthStorage,
1125-
modelRegistry: {},
1126-
});
1092+
mockedResolveModelAsync.mockResolvedValueOnce({
1093+
model: {
1094+
id: "gpt-5.5",
1095+
provider: "openai-codex",
1096+
contextWindow: 200000,
1097+
api: "openai-codex-responses",
1098+
},
1099+
error: null,
1100+
authStorage: codexAuthStorage,
1101+
modelRegistry: {},
1102+
});
11271103
mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan);
11281104
mockedGetApiKeyForModel.mockImplementation(
11291105
async ({ profileId }: { profileId?: string } = {}) => {
@@ -1280,29 +1256,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
12801256
});
12811257
mockedEnsureAuthProfileStore.mockReturnValueOnce(codexAuthStore);
12821258
mockedResolveAuthProfileOrder.mockReturnValueOnce(["openai-codex:sub", "openai-codex:backup"]);
1283-
mockedResolveModelAsync
1284-
.mockResolvedValueOnce({
1285-
model: {
1286-
id: "gpt-5.5",
1287-
provider: "openai",
1288-
contextWindow: 200000,
1289-
api: "openai-responses",
1290-
},
1291-
error: null,
1292-
authStorage: { setRuntimeApiKey: vi.fn() },
1293-
modelRegistry: {},
1294-
})
1295-
.mockResolvedValueOnce({
1296-
model: {
1297-
id: "gpt-5.5",
1298-
provider: "openai-codex",
1299-
contextWindow: 200000,
1300-
api: "openai-codex-responses",
1301-
},
1302-
error: null,
1303-
authStorage: codexAuthStorage,
1304-
modelRegistry: {},
1305-
});
1259+
mockedResolveModelAsync.mockResolvedValueOnce({
1260+
model: {
1261+
id: "gpt-5.5",
1262+
provider: "openai-codex",
1263+
contextWindow: 200000,
1264+
api: "openai-codex-responses",
1265+
},
1266+
error: null,
1267+
authStorage: codexAuthStorage,
1268+
modelRegistry: {},
1269+
});
13061270
mockedBuildAgentRuntimePlan
13071271
.mockReturnValueOnce(firstRuntimePlan)
13081272
.mockReturnValueOnce(secondRuntimePlan);

src/agents/embedded-agent-runner/run.ts

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -639,46 +639,68 @@ export async function runEmbeddedAgent(
639639
config: params.config,
640640
workspaceDir: resolvedWorkspace,
641641
});
642-
const dynamicModelResolution = await resolveModelAsync(
643-
provider,
644-
modelId,
645-
agentDir,
646-
params.config,
647-
{
648-
// Plugin dynamic model hooks can resolve explicit model refs without
649-
// first generating OpenClaw models.json. This keeps one-shot model runs from
650-
// blocking on unrelated provider discovery.
651-
skipAgentDiscovery: true,
652-
workspaceDir: resolvedWorkspace,
653-
},
654-
);
655-
let modelResolution =
656-
dynamicModelResolution.model || pluginHarnessOwnsTransport
657-
? dynamicModelResolution
658-
: await (async () => {
659-
await ensureOpenClawModelsJson(params.config, agentDir, {
660-
workspaceDir: resolvedWorkspace,
661-
});
662-
return await resolveModelAsync(provider, modelId, agentDir, params.config, {
663-
workspaceDir: resolvedWorkspace,
664-
});
665-
})();
666-
if (selectedRuntimeProvider !== provider && modelResolution.model) {
667-
const runtimeModelResolution = await resolveModelAsync(
668-
selectedRuntimeProvider,
642+
const modelResolutionProviders =
643+
selectedRuntimeProvider !== provider ? [selectedRuntimeProvider, provider] : [provider];
644+
let resolvedModelProvider = provider;
645+
let firstModelResolution: Awaited<ReturnType<typeof resolveModelAsync>> | undefined;
646+
let modelResolution: Awaited<ReturnType<typeof resolveModelAsync>> | undefined;
647+
for (const candidateProvider of modelResolutionProviders) {
648+
const candidateResolution = await resolveModelAsync(
649+
candidateProvider,
669650
modelId,
670651
agentDir,
671652
params.config,
672653
{
654+
// Plugin dynamic model hooks can resolve explicit model refs without
655+
// first generating OpenClaw models.json. This keeps one-shot model runs from
656+
// blocking on unrelated provider discovery.
673657
skipAgentDiscovery: true,
674658
workspaceDir: resolvedWorkspace,
675659
},
676660
);
677-
if (runtimeModelResolution.model) {
678-
provider = selectedRuntimeProvider;
679-
modelResolution = runtimeModelResolution;
661+
firstModelResolution ??= candidateResolution;
662+
if (candidateResolution.model) {
663+
resolvedModelProvider = candidateProvider;
664+
modelResolution = candidateResolution;
665+
break;
666+
}
667+
}
668+
if (!modelResolution && pluginHarnessOwnsTransport) {
669+
modelResolution = firstModelResolution;
670+
}
671+
if (!modelResolution) {
672+
await ensureOpenClawModelsJson(params.config, agentDir, {
673+
workspaceDir: resolvedWorkspace,
674+
});
675+
for (const candidateProvider of modelResolutionProviders) {
676+
const candidateResolution = await resolveModelAsync(
677+
candidateProvider,
678+
modelId,
679+
agentDir,
680+
params.config,
681+
{
682+
workspaceDir: resolvedWorkspace,
683+
},
684+
);
685+
firstModelResolution ??= candidateResolution;
686+
if (candidateResolution.model) {
687+
resolvedModelProvider = candidateProvider;
688+
modelResolution = candidateResolution;
689+
break;
690+
}
680691
}
681692
}
693+
modelResolution ??= firstModelResolution;
694+
if (!modelResolution) {
695+
throw new FailoverError(`Unknown model: ${provider}/${modelId}`, {
696+
reason: "model_not_found",
697+
provider,
698+
model: modelId,
699+
sessionId: params.sessionId,
700+
lane: globalLane,
701+
});
702+
}
703+
provider = resolvedModelProvider;
682704
const { model, error, authStorage, modelRegistry } = modelResolution;
683705
if (!model) {
684706
throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, {

src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,18 @@ export function installEmbeddedRunnerFastRunE2eMocks(
5454
options: EmbeddedRunnerFastRunMockOptions,
5555
): void {
5656
vi.doMock("../harness/selection.js", () => ({
57-
selectAgentHarness: vi.fn((params: { provider?: string }) => ({
58-
id: params.provider === "codex-cli" ? "codex" : "openclaw",
59-
label: "Mock agent harness",
60-
supports: vi.fn(() => ({ supported: false })),
61-
runAttempt: vi.fn(),
62-
})),
57+
selectAgentHarness: vi.fn(
58+
(params: {
59+
provider?: string;
60+
agentHarnessId?: string;
61+
agentHarnessRuntimeOverride?: string;
62+
}) => ({
63+
id: resolveMockHarnessId(params),
64+
label: "Mock agent harness",
65+
supports: vi.fn(() => ({ supported: false })),
66+
runAttempt: vi.fn(),
67+
}),
68+
),
6369
resolveAgentHarnessPolicy: vi.fn(() => ({ runtime: "openclaw" })),
6470
runAgentHarnessAttempt: (params: unknown) => options.runEmbeddedAttempt(params),
6571
}));
@@ -152,6 +158,18 @@ export function installEmbeddedRunnerFastRunE2eMocks(
152158
}));
153159
}
154160

161+
function resolveMockHarnessId(params: {
162+
provider?: string;
163+
agentHarnessId?: string;
164+
agentHarnessRuntimeOverride?: string;
165+
}): "codex" | "openclaw" {
166+
return params.provider === "codex-cli" ||
167+
params.agentHarnessId === "codex" ||
168+
params.agentHarnessRuntimeOverride === "codex"
169+
? "codex"
170+
: "openclaw";
171+
}
172+
155173
export function installEmbeddedRunnerBackoffE2eMocks(
156174
options: EmbeddedRunnerBackoffMockOptions,
157175
): void {

0 commit comments

Comments
 (0)