mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 16:00:25 +00:00
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:
committed by
GitHub
parent
f3e285126a
commit
5cef288d65
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user