diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 03de7d772cc..48218d41ff7 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -46,6 +46,8 @@ export function isModernModelRef(ref: ModelRef): boolean { } if (provider === "openai") { + // Keep the broader prefix match for GPT-5.x families so live tests keep opting into + // fresh OpenAI minor variants before the forward-compat catalog learns each exact ID. return matchesExactOrPrefix(id, OPENAI_MODELS); } diff --git a/src/agents/model-forward-compat.ts b/src/agents/model-forward-compat.ts index 630730dcd47..95d13832cc7 100644 --- a/src/agents/model-forward-compat.ts +++ b/src/agents/model-forward-compat.ts @@ -10,6 +10,9 @@ const OPENAI_GPT_54_CONTEXT_TOKENS = 1_050_000; const OPENAI_GPT_54_MAX_TOKENS = 128_000; const OPENAI_GPT_54_TEMPLATE_MODEL_IDS = ["gpt-5.2"] as const; const OPENAI_GPT_54_PRO_TEMPLATE_MODEL_IDS = ["gpt-5.2-pro", "gpt-5.2"] as const; +const OPENAI_GPT_54_COST = { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 } as const; +// OpenAI currently publishes no cached-input price for GPT-5.4 Pro. +const OPENAI_GPT_54_PRO_COST = { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 } as const; const OPENAI_CODEX_GPT_54_MODEL_ID = "gpt-5.4"; const OPENAI_CODEX_GPT_54_TEMPLATE_MODEL_IDS = ["gpt-5.3-codex", "gpt-5.2-codex"] as const; @@ -56,35 +59,23 @@ function resolveOpenAIGpt54ForwardCompatModel( return undefined; } - return ( - cloneFirstTemplateModel({ - normalizedProvider, - trimmedModelId, - templateIds: [...templateIds], - modelRegistry, - patch: { - api: "openai-responses", - provider: normalizedProvider, - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, - maxTokens: OPENAI_GPT_54_MAX_TOKENS, - }, - }) ?? - normalizeModelCompat({ - id: trimmedModelId, - name: trimmedModelId, + const template = cloneFirstTemplateModel({ + normalizedProvider, + trimmedModelId, + templateIds: [...templateIds], + modelRegistry, + patch: { api: "openai-responses", provider: normalizedProvider, baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, maxTokens: OPENAI_GPT_54_MAX_TOKENS, - } as Model) - ); + }, + }); + + return buildOpenAIGpt54FallbackModel(trimmedModelId, template); } function cloneFirstTemplateModel(params: { @@ -138,6 +129,41 @@ function cloneSyntheticTemplateModel(params: { return undefined; } +function buildOpenAIGpt54FallbackModel(modelId: string, template?: Model | null): Model { + return normalizeModelCompat({ + ...template, + id: modelId, + name: modelId, + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: + modelId.toLowerCase() === OPENAI_GPT_54_PRO_MODEL_ID + ? OPENAI_GPT_54_PRO_COST + : OPENAI_GPT_54_COST, + contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, + maxTokens: OPENAI_GPT_54_MAX_TOKENS, + } as Model); +} + +function buildOpenAICodexSparkFallbackModel(template?: Model | null): Model { + return normalizeModelCompat({ + ...template, + id: OPENAI_CODEX_GPT_53_SPARK_MODEL_ID, + name: OPENAI_CODEX_GPT_53_SPARK_MODEL_ID, + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text", "image"], + cost: template?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: template?.contextWindow ?? DEFAULT_CONTEXT_TOKENS, + maxTokens: template?.maxTokens ?? DEFAULT_CONTEXT_TOKENS, + } as Model); +} + export function augmentKnownForwardCompatModels(models: Model[]): Model[] { const next = [...models]; const existing = new Set( @@ -158,64 +184,46 @@ export function augmentKnownForwardCompatModels(models: Model[]): Model), + buildOpenAIGpt54FallbackModel( + OPENAI_GPT_54_MODEL_ID, + cloneSyntheticTemplateModel({ + models: next, + normalizedProvider: "openai", + trimmedModelId: OPENAI_GPT_54_MODEL_ID, + templateIds: OPENAI_GPT_54_TEMPLATE_MODEL_IDS, + patch: { + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, + maxTokens: OPENAI_GPT_54_MAX_TOKENS, + }, + }), + ), ); pushIfMissing( "openai", OPENAI_GPT_54_PRO_MODEL_ID, - cloneSyntheticTemplateModel({ - models: next, - normalizedProvider: "openai", - trimmedModelId: OPENAI_GPT_54_PRO_MODEL_ID, - templateIds: OPENAI_GPT_54_PRO_TEMPLATE_MODEL_IDS, - patch: { - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, - maxTokens: OPENAI_GPT_54_MAX_TOKENS, - }, - }) ?? - normalizeModelCompat({ - id: OPENAI_GPT_54_PRO_MODEL_ID, - name: OPENAI_GPT_54_PRO_MODEL_ID, - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, - maxTokens: OPENAI_GPT_54_MAX_TOKENS, - } as Model), + buildOpenAIGpt54FallbackModel( + OPENAI_GPT_54_PRO_MODEL_ID, + cloneSyntheticTemplateModel({ + models: next, + normalizedProvider: "openai", + trimmedModelId: OPENAI_GPT_54_PRO_MODEL_ID, + templateIds: OPENAI_GPT_54_PRO_TEMPLATE_MODEL_IDS, + patch: { + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + contextWindow: OPENAI_GPT_54_CONTEXT_TOKENS, + maxTokens: OPENAI_GPT_54_MAX_TOKENS, + }, + }), + ), ); } @@ -245,31 +253,21 @@ export function augmentKnownForwardCompatModels(models: Model[]): Model), + buildOpenAICodexSparkFallbackModel( + cloneSyntheticTemplateModel({ + models: next, + normalizedProvider: "openai-codex", + trimmedModelId: OPENAI_CODEX_GPT_53_SPARK_MODEL_ID, + templateIds: [OPENAI_CODEX_GPT_53_MODEL_ID, ...OPENAI_CODEX_TEMPLATE_MODEL_IDS], + patch: { + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text", "image"], + }, + }), + ), ); } diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index d23b68d32b6..d3bfbf6f308 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -446,6 +446,30 @@ describe("resolveModel", () => { }); }); + it("uses GPT-5.4 Pro pricing when cloning an older openai template", () => { + mockDiscoveredModel({ + provider: "openai", + modelId: "gpt-5.2", + templateModel: buildForwardCompatTemplate({ + id: "gpt-5.2", + name: "GPT-5.2", + provider: "openai", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + }), + }); + + const result = resolveModel("openai", "gpt-5.4-pro", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model?.cost).toEqual({ + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }); + }); + it("builds an anthropic forward-compat fallback for claude-opus-4-6", () => { mockDiscoveredModel({ provider: "anthropic",