fix(agents): resolve Codex runtime models first

* fix(agents): resolve Codex runtime models first

* test(agents): align Codex runtime resolution fixtures
This commit is contained in:
Peter Steinberger
2026-05-27 23:23:22 +01:00
committed by GitHub
parent f3e285126a
commit 5cef288d65
4 changed files with 201 additions and 115 deletions

View File

@@ -18,14 +18,24 @@ import {
installEmbeddedRunnerFastRunE2eMocks,
} from "./test-helpers/embedded-agent-runner-e2e-mocks.js";
type EmbeddedRunnerModelResolution =
| ReturnType<typeof createResolvedEmbeddedRunnerModel>
| {
model?: undefined;
error: string;
authStorage: { setRuntimeApiKey: () => undefined };
modelRegistry: Record<string, never>;
};
const runEmbeddedAttemptMock = vi.fn();
const disposeSessionMcpRuntimeMock = vi.fn<(sessionId: string) => Promise<void>>(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<EmbeddedRunnerModelResolution> =>
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");

View File

@@ -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);

View File

@@ -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<ReturnType<typeof resolveModelAsync>> | undefined;
let modelResolution: Awaited<ReturnType<typeof resolveModelAsync>> | 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}`, {

View File

@@ -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 {