fix(models): keep codex spark codex-only

This commit is contained in:
Peter Steinberger
2026-03-13 00:51:30 +00:00
parent d4f535b203
commit d5b3f2ed71
16 changed files with 339 additions and 4 deletions

View File

@@ -58,6 +58,16 @@ describe("pi embedded model e2e smoke", () => {
expect(result.model).toMatchObject(buildOpenAICodexForwardCompatExpectation("gpt-5.4"));
});
it("builds an openai-codex forward-compat fallback for gpt-5.3-codex-spark", () => {
mockOpenAICodexTemplateModel();
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject(
buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
);
});
it("keeps unknown-model errors for non-forward-compat IDs", () => {
const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent");
expect(result.model).toBeUndefined();

View File

@@ -35,15 +35,25 @@ export function mockOpenAICodexTemplateModel(): void {
export function buildOpenAICodexForwardCompatExpectation(
id: string = "gpt-5.3-codex",
): Partial<typeof OPENAI_CODEX_TEMPLATE_MODEL> & { provider: string; id: string } {
): Partial<ModelDefinitionConfig> & {
provider: string;
id: string;
api: string;
baseUrl: string;
} {
const isGpt54 = id === "gpt-5.4";
const isSpark = id === "gpt-5.3-codex-spark";
return {
provider: "openai-codex",
id,
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
contextWindow: isGpt54 ? 1_050_000 : 272000,
input: isSpark ? ["text"] : ["text", "image"],
cost: isSpark
? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
: OPENAI_CODEX_TEMPLATE_MODEL.cost,
contextWindow: isGpt54 ? 1_050_000 : isSpark ? 128_000 : 272000,
maxTokens: 128000,
};
}

View File

@@ -546,6 +546,60 @@ describe("resolveModel", () => {
expect(result.model).toMatchObject(buildOpenAICodexForwardCompatExpectation("gpt-5.4"));
});
it("builds an openai-codex fallback for gpt-5.3-codex-spark", () => {
mockOpenAICodexTemplateModel();
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject(
buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
);
});
it("keeps openai-codex gpt-5.3-codex-spark when discovery provides it", () => {
mockDiscoveredModel({
provider: "openai-codex",
modelId: "gpt-5.3-codex-spark",
templateModel: {
...buildOpenAICodexForwardCompatExpectation("gpt-5.3-codex-spark"),
name: "GPT-5.3 Codex Spark",
input: ["text"],
},
});
const result = resolveModel("openai-codex", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject({
provider: "openai-codex",
id: "gpt-5.3-codex-spark",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
});
});
it("rejects stale direct openai gpt-5.3-codex-spark discovery rows", () => {
mockDiscoveredModel({
provider: "openai",
modelId: "gpt-5.3-codex-spark",
templateModel: buildForwardCompatTemplate({
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
provider: "openai",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
}),
});
const result = resolveModel("openai", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.model).toBeUndefined();
expect(result.error).toBe(
"Unknown model: openai/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
);
});
it("applies provider overrides to openai gpt-5.4 forward-compat models", () => {
mockDiscoveredModel({
provider: "openai",
@@ -725,6 +779,24 @@ describe("resolveModel", () => {
expectUnknownModelError("openai-codex", "gpt-4.1-mini");
});
it("rejects direct openai gpt-5.3-codex-spark with a codex-only hint", () => {
const result = resolveModel("openai", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.model).toBeUndefined();
expect(result.error).toBe(
"Unknown model: openai/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
);
});
it("rejects azure openai gpt-5.3-codex-spark with a codex-only hint", () => {
const result = resolveModel("azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent");
expect(result.model).toBeUndefined();
expect(result.error).toBe(
"Unknown model: azure-openai-responses/gpt-5.3-codex-spark. gpt-5.3-codex-spark is only supported via openai-codex OAuth. Use openai-codex/gpt-5.3-codex-spark.",
);
});
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)

View File

@@ -8,6 +8,10 @@ import { buildModelAliasLines } from "../model-alias-lines.js";
import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js";
import { resolveForwardCompatModel } from "../model-forward-compat.js";
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
import {
buildSuppressedBuiltInModelError,
shouldSuppressBuiltInModel,
} from "../model-suppression.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
import { normalizeResolvedProviderModel } from "./model.provider-normalization.js";
@@ -159,6 +163,9 @@ export function resolveModelWithRegistry(params: {
cfg?: OpenClawConfig;
}): Model<Api> | undefined {
const { provider, modelId, modelRegistry, cfg } = params;
if (shouldSuppressBuiltInModel({ provider, id: modelId })) {
return undefined;
}
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
@@ -303,6 +310,10 @@ const LOCAL_PROVIDER_HINTS: Record<string, string> = {
};
function buildUnknownModelError(provider: string, modelId: string): string {
const suppressed = buildSuppressedBuiltInModelError({ provider, id: modelId });
if (suppressed) {
return suppressed;
}
const base = `Unknown model: ${provider}/${modelId}`;
const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()];
return hint ? `${base}. ${hint}` : base;