Model: add strict gpt-5.3-codex fallback for OpenAI Codex (fixes #9989) (#9995)

* Model: allow forward-compatible OpenAI Codex GPT-5 IDs

* Model: scope Codex fallback to gpt-5.3-codex

* fix: reorder codex fallback before providerCfg, add ordering test, changelog (#9989) (thanks @w1kke)

---------

Co-authored-by: Robin <4robinlehmann@gmail.com>
This commit is contained in:
Tyler Yust
2026-02-05 16:23:18 -08:00
committed by GitHub
parent 6f4665dda3
commit 370bbcd89b
3 changed files with 134 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../pi-model-discovery.js", () => ({
discoverAuthStorage: vi.fn(() => ({ mocked: true })),
@@ -6,6 +6,7 @@ vi.mock("../pi-model-discovery.js", () => ({
}));
import type { OpenClawConfig } from "../../config/config.js";
import { discoverModels } from "../pi-model-discovery.js";
import { buildInlineProviderModels, resolveModel } from "./model.js";
const makeModel = (id: string) => ({
@@ -18,6 +19,12 @@ const makeModel = (id: string) => ({
maxTokens: 1,
});
beforeEach(() => {
vi.mocked(discoverModels).mockReturnValue({
find: vi.fn(() => null),
} as unknown as ReturnType<typeof discoverModels>);
});
describe("buildInlineProviderModels", () => {
it("attaches provider ids to inline models", () => {
const providers = {
@@ -127,4 +134,74 @@ describe("resolveModel", () => {
expect(result.model?.provider).toBe("custom");
expect(result.model?.id).toBe("missing-model");
});
it("builds an openai-codex fallback for gpt-5.3-codex", () => {
const templateModel = {
id: "gpt-5.2-codex",
name: "GPT-5.2 Codex",
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text", "image"] as const,
cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
contextWindow: 272000,
maxTokens: 128000,
};
vi.mocked(discoverModels).mockReturnValue({
find: vi.fn((provider: string, modelId: string) => {
if (provider === "openai-codex" && modelId === "gpt-5.2-codex") {
return templateModel;
}
return null;
}),
} as unknown as ReturnType<typeof discoverModels>);
const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject({
provider: "openai-codex",
id: "gpt-5.3-codex",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
contextWindow: 272000,
maxTokens: 128000,
});
});
it("keeps unknown-model errors for non-gpt-5 openai-codex ids", () => {
const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent");
expect(result.model).toBeUndefined();
expect(result.error).toBe("Unknown model: openai-codex/gpt-4.1-mini");
});
it("uses codex fallback even when openai-codex provider is configured", () => {
// This test verifies the ordering: codex fallback must fire BEFORE the generic providerCfg fallback.
// If ordering is wrong, the generic fallback would use api: "openai-responses" (the default)
// instead of "openai-codex-responses".
const cfg: OpenClawConfig = {
models: {
providers: {
"openai-codex": {
baseUrl: "https://custom.example.com",
// No models array, or models without gpt-5.3-codex
},
},
},
} as OpenClawConfig;
vi.mocked(discoverModels).mockReturnValue({
find: vi.fn(() => null),
} as unknown as ReturnType<typeof discoverModels>);
const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent", cfg);
expect(result.error).toBeUndefined();
expect(result.model?.api).toBe("openai-codex-responses");
expect(result.model?.id).toBe("gpt-5.3-codex");
expect(result.model?.provider).toBe("openai-codex");
});
});