Models: address GPT-5.4 review feedback

This commit is contained in:
Vincent Koc
2026-03-05 23:46:29 -05:00
parent 48adb47ec3
commit 3b0779a75a
3 changed files with 125 additions and 101 deletions

View File

@@ -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);
}

View File

@@ -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<Api>)
);
},
});
return buildOpenAIGpt54FallbackModel(trimmedModelId, template);
}
function cloneFirstTemplateModel(params: {
@@ -138,6 +129,41 @@ function cloneSyntheticTemplateModel(params: {
return undefined;
}
function buildOpenAIGpt54FallbackModel(modelId: string, template?: Model<Api> | null): Model<Api> {
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<Api>);
}
function buildOpenAICodexSparkFallbackModel(template?: Model<Api> | null): Model<Api> {
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<Api>);
}
export function augmentKnownForwardCompatModels(models: Model<Api>[]): Model<Api>[] {
const next = [...models];
const existing = new Set(
@@ -158,64 +184,46 @@ export function augmentKnownForwardCompatModels(models: Model<Api>[]): Model<Api
pushIfMissing(
"openai",
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,
},
}) ??
normalizeModelCompat({
id: OPENAI_GPT_54_MODEL_ID,
name: OPENAI_GPT_54_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<Api>),
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<Api>),
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<Api>[]): Model<Api
pushIfMissing(
"openai-codex",
OPENAI_CODEX_GPT_53_SPARK_MODEL_ID,
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"],
},
}) ??
normalizeModelCompat({
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: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_TOKENS,
maxTokens: DEFAULT_CONTEXT_TOKENS,
} as Model<Api>),
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"],
},
}),
),
);
}

View File

@@ -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",