Skip to content

Commit 501f634

Browse files
committed
fix(openai): route spark through codex runtime
1 parent dae2bcf commit 501f634

6 files changed

Lines changed: 122 additions & 5 deletions

File tree

extensions/openai/openai-provider.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,20 @@ describe("buildOpenAIProvider", () => {
849849
auth: "api-key",
850850
},
851851
} as never);
852+
const runtimeModel = provider.resolveDynamicModel?.({
853+
provider: "openai",
854+
modelId: "gpt-5.3-codex-spark",
855+
modelRegistry: { find: () => null },
856+
agentRuntimeId: "codex",
857+
} as never);
858+
const apiKeyRuntimeModel = provider.resolveDynamicModel?.({
859+
provider: "openai",
860+
modelId: "gpt-5.3-codex-spark",
861+
modelRegistry: { find: () => null },
862+
agentRuntimeId: "codex",
863+
authProfileId: "openai:api-key",
864+
authProfileMode: "api_key",
865+
} as never);
852866
const unknownModelHint = provider.buildUnknownModelHint?.({
853867
provider: "openai",
854868
modelId: "gpt-5.3-codex-spark",
@@ -864,7 +878,18 @@ describe("buildOpenAIProvider", () => {
864878
contextTokens: 128_000,
865879
maxTokens: 128_000,
866880
});
881+
expectFields(runtimeModel, {
882+
provider: "openai",
883+
id: "gpt-5.3-codex-spark",
884+
api: "openai-chatgpt-responses",
885+
baseUrl: "https://chatgpt.com/backend-api/codex",
886+
input: ["text"],
887+
contextWindow: 128_000,
888+
contextTokens: 128_000,
889+
maxTokens: 128_000,
890+
});
867891
expect(apiKeyModel).toBeUndefined();
892+
expect(apiKeyRuntimeModel).toBeUndefined();
868893
expect(unknownModelHint).toContain("ChatGPT/Codex OAuth");
869894
expect(unknownModelHint).toContain("OpenAI API-key auth cannot use this model");
870895
});

extensions/openai/openai-provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,11 @@ function shouldResolveDynamicModelThroughCodex(ctx: ProviderResolveDynamicModelC
515515
if (ctx.providerConfig?.baseUrl && !isOpenAIApiBaseUrl(ctx.providerConfig.baseUrl)) {
516516
return false;
517517
}
518-
return resolveConfiguredAuthTransport(ctx) === "codex";
518+
const authTransport = resolveConfiguredAuthTransport(ctx);
519+
if (authTransport) {
520+
return authTransport === "codex";
521+
}
522+
return ctx.agentRuntimeId === "codex";
519523
}
520524

521525
function buildOpenAIUnknownModelHint(modelId: string): string | undefined {

src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,58 @@ describe("resolveModel forward-compat errors and overrides", () => {
213213
);
214214
});
215215

216+
it("resolves suppressed openai gpt-5.3-codex-spark through model-scoped Codex runtime", () => {
217+
mockOpenAICodexTemplateModel(discoverModels);
218+
219+
const cfg: OpenClawConfig = {
220+
agents: {
221+
defaults: {
222+
models: {
223+
"openai/gpt-5.3-codex-spark": {
224+
agentRuntime: { id: "codex" },
225+
},
226+
},
227+
},
228+
},
229+
};
230+
const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg);
231+
232+
expect(result.error).toBeUndefined();
233+
expect(result.model).toMatchObject(
234+
buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
235+
);
236+
});
237+
238+
it("keeps model-scoped Codex runtime blocked for explicit OpenAI API-key provider config", () => {
239+
mockOpenAICodexTemplateModel(discoverModels);
240+
241+
const cfg: OpenClawConfig = {
242+
agents: {
243+
defaults: {
244+
models: {
245+
"openai/gpt-5.3-codex-spark": {
246+
agentRuntime: { id: "codex" },
247+
},
248+
},
249+
},
250+
},
251+
models: {
252+
providers: {
253+
openai: {
254+
auth: "api-key",
255+
api: "openai-responses",
256+
baseUrl: "https://api.openai.com/v1",
257+
models: [],
258+
},
259+
},
260+
},
261+
};
262+
const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg);
263+
264+
expect(result.model).toBeUndefined();
265+
expect(result.error).toContain("OpenAI API-key auth cannot use this model");
266+
});
267+
216268
it("keeps suppressed stale direct openai gpt-5.3-codex-spark catalog rows blocked", () => {
217269
mockDiscoveredModel(discoverModels, {
218270
provider: "openai",

src/agents/embedded-agent-runner/model.provider-runtime.test-support.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ type DynamicModelContext = {
3939
provider: string;
4040
modelId: string;
4141
modelRegistry: ModelRegistryLike;
42-
providerConfig?: { api?: string | null; baseUrl?: string };
42+
agentRuntimeId?: string;
43+
authProfileMode?: "api_key" | "aws-sdk" | "oauth" | "token";
44+
providerConfig?: {
45+
api?: string | null;
46+
auth?: "api-key" | "aws-sdk" | "oauth" | "token";
47+
baseUrl?: string;
48+
};
4349
};
4450

4551
type ResolvedModelLike = Record<string, unknown>;
@@ -296,9 +302,22 @@ function buildDynamicModel(
296302
const isLegacyGpt54Alias = lower === "gpt-5.4-codex";
297303
const isSparkModel = lower === "gpt-5.3-codex-spark";
298304
const exactModel = params.modelRegistry.find("openai", modelId) as ResolvedModelLike | null;
305+
const explicitResponsesAuth =
306+
params.authProfileMode === "api_key" ||
307+
params.authProfileMode === "aws-sdk" ||
308+
params.providerConfig?.auth === "api-key" ||
309+
params.providerConfig?.auth === "aws-sdk";
310+
const explicitCodexAuth =
311+
params.authProfileMode === "oauth" ||
312+
params.authProfileMode === "token" ||
313+
params.providerConfig?.auth === "oauth" ||
314+
params.providerConfig?.auth === "token";
299315
const providerConfigSelectsChatGpt =
300-
params.providerConfig?.api === "openai-chatgpt-responses" ||
301-
isNativeOpenAICodexBaseUrl(params.providerConfig?.baseUrl);
316+
!explicitResponsesAuth &&
317+
(explicitCodexAuth ||
318+
params.providerConfig?.api === "openai-chatgpt-responses" ||
319+
isNativeOpenAICodexBaseUrl(params.providerConfig?.baseUrl) ||
320+
params.agentRuntimeId === "codex");
302321
if (
303322
lower === "gpt-5.5" &&
304323
(providerConfigSelectsChatGpt || isOpenAIChatGptModelTemplate(exactModel))
@@ -695,7 +714,13 @@ export function createProviderRuntimeTestMock(options: ProviderRuntimeTestMockOp
695714
context: {
696715
modelId: string;
697716
modelRegistry: ModelRegistryLike;
698-
providerConfig?: { api?: string | null; baseUrl?: string };
717+
agentRuntimeId?: string;
718+
authProfileMode?: "api_key" | "aws-sdk" | "oauth" | "token";
719+
providerConfig?: {
720+
api?: string | null;
721+
auth?: "api-key" | "aws-sdk" | "oauth" | "token";
722+
baseUrl?: string;
723+
};
699724
};
700725
}) =>
701726
handledDynamicProviders.has(params.provider)
@@ -704,6 +729,8 @@ export function createProviderRuntimeTestMock(options: ProviderRuntimeTestMockOp
704729
provider: params.provider,
705730
modelId: params.context.modelId,
706731
modelRegistry: params.context.modelRegistry,
732+
agentRuntimeId: params.context.agentRuntimeId,
733+
authProfileMode: params.context.authProfileMode,
707734
providerConfig: params.context.providerConfig,
708735
},
709736
{

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { resolveDefaultAgentDir } from "../agent-scope.js";
2222
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../auth-profiles.js";
2323
import type { AuthProfileCredential } from "../auth-profiles/types.js";
2424
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
25+
import { resolveAgentHarnessPolicy } from "../harness/policy.js";
2526
import { buildModelAliasLines } from "../model-alias-lines.js";
2627
import { resolveModelWorkspaceDir } from "../model-discovery-context.js";
2728
import { modelKey, normalizeStaticProviderModelId } from "../model-ref-shared.js";
@@ -1134,6 +1135,12 @@ function resolvePluginDynamicModelWithRegistry(params: {
11341135
const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir } = params;
11351136
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
11361137
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
1138+
const agentHarnessPolicy = resolveAgentHarnessPolicy({ provider, modelId, config: cfg });
1139+
const agentRuntimeId =
1140+
agentHarnessPolicy.runtimeSource !== "implicit" ||
1141+
cfg?.plugins?.entries?.codex?.enabled === true
1142+
? agentHarnessPolicy.runtime
1143+
: undefined;
11371144
const authProfile = resolveDynamicModelAuthProfile({
11381145
provider,
11391146
cfg,
@@ -1157,6 +1164,7 @@ function resolvePluginDynamicModelWithRegistry(params: {
11571164
config: cfg,
11581165
agentDir,
11591166
workspaceDir,
1167+
...(agentRuntimeId ? { agentRuntimeId } : {}),
11601168
provider,
11611169
modelId,
11621170
modelRegistry,

src/plugins/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ export type ProviderResolveDynamicModelContext = {
510510
config?: OpenClawConfig;
511511
agentDir?: string;
512512
workspaceDir?: string;
513+
agentRuntimeId?: string;
513514
provider: string;
514515
modelId: string;
515516
modelRegistry: ModelRegistry;

0 commit comments

Comments
 (0)