feat(ai): add input-based pricing tiers

This commit is contained in:
Armin Ronacher
2026-07-09 22:43:19 +02:00
parent 6c735db060
commit a9ecf301fb
14 changed files with 241 additions and 60 deletions
+54 -17
View File
@@ -194,6 +194,23 @@ const ANT_LING_RING_THINKING_LEVEL_MAP = {
} as const;
const MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS = new Set(["gpt-5.6"]);
const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272000;
const OPENAI_GPT_56_MODEL_IDS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
function withOpenAiLongContextPricing(cost: Model<Api>["cost"]): Model<Api>["cost"] {
return {
...cost,
tiers: [
{
inputTokensAbove: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
input: cost.input * 2,
output: cost.output * 1.5,
cacheRead: cost.cacheRead * 2,
cacheWrite: cost.cacheWrite * 2,
},
],
};
}
const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
"gpt-5.1",
@@ -1678,9 +1695,16 @@ async function generateModels() {
candidate.contextWindow = 272000;
candidate.maxTokens = 128000;
}
if (candidate.provider === "openai" && (candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5")) {
candidate.contextWindow = 272000;
// Keep direct OpenAI requests in the short-context pricing tier.
if (
candidate.provider === "openai" &&
(candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5" || OPENAI_GPT_56_MODEL_IDS.has(candidate.id))
) {
candidate.contextWindow = OPENAI_LONG_CONTEXT_INPUT_THRESHOLD;
candidate.maxTokens = 128000;
if (OPENAI_GPT_56_MODEL_IDS.has(candidate.id)) {
candidate.cost = withOpenAiLongContextPricing(candidate.cost);
}
}
// models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit);
// the actual max output is 128000. Also propagates to the derived Azure clone.
@@ -1724,8 +1748,8 @@ async function generateModels() {
provider: "openai",
reasoning: true,
input: ["text", "image"],
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
contextWindow: 1050000,
cost: withOpenAiLongContextPricing({ input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }),
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
},
{
@@ -1736,8 +1760,8 @@ async function generateModels() {
provider: "openai",
reasoning: true,
input: ["text", "image"],
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
contextWindow: 1050000,
cost: withOpenAiLongContextPricing({ input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 }),
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
},
{
@@ -1748,8 +1772,8 @@ async function generateModels() {
provider: "openai",
reasoning: true,
input: ["text", "image"],
cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 0 },
contextWindow: 1050000,
cost: withOpenAiLongContextPricing({ input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 }),
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
},
{
@@ -1899,9 +1923,10 @@ async function generateModels() {
// OpenAI Codex (ChatGPT OAuth) models
// NOTE: These are not fetched from models.dev; we keep a small, explicit list to avoid aliases.
// Context window is based on observed server limits (400s above ~272k), not marketing numbers.
// Older model limits are based on observed server behavior; GPT-5.6 follows Codex's 372k catalog limit.
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
const CODEX_CONTEXT = 272000;
const CODEX_GPT_56_CONTEXT = 372000;
const CODEX_SPARK_CONTEXT = 128000;
const CODEX_MAX_TOKENS = 128000;
const codexModels: Model<"openai-codex-responses">[] = [
@@ -1961,8 +1986,8 @@ async function generateModels() {
baseUrl: CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 0 },
contextWindow: CODEX_CONTEXT,
cost: withOpenAiLongContextPricing({ input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 }),
contextWindow: CODEX_GPT_56_CONTEXT,
maxTokens: CODEX_MAX_TOKENS,
},
{
@@ -1973,8 +1998,8 @@ async function generateModels() {
baseUrl: CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
contextWindow: CODEX_CONTEXT,
cost: withOpenAiLongContextPricing({ input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }),
contextWindow: CODEX_GPT_56_CONTEXT,
maxTokens: CODEX_MAX_TOKENS,
},
{
@@ -1985,8 +2010,8 @@ async function generateModels() {
baseUrl: CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
contextWindow: CODEX_CONTEXT,
cost: withOpenAiLongContextPricing({ input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 }),
contextWindow: CODEX_GPT_56_CONTEXT,
maxTokens: CODEX_MAX_TOKENS,
},
];
@@ -2112,11 +2137,14 @@ async function generateModels() {
});
}
// Azure Foundry deploys these with larger context windows than OpenAI's own API,
// which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs.
// Azure Foundry deploys these with larger context windows than OpenAI's own short-tier defaults.
// See models-sold-directly-by-azure docs.
const AZURE_CONTEXT_WINDOW_OVERRIDES: Record<string, number> = {
"gpt-5.4": 1050000,
"gpt-5.5": 1050000,
"gpt-5.6-luna": 1050000,
"gpt-5.6-sol": 1050000,
"gpt-5.6-terra": 1050000,
};
const azureOpenAiModels: Model<Api>[] = allModels
.filter((model) => model.provider === "openai" && model.api === "openai-responses")
@@ -2125,6 +2153,12 @@ async function generateModels() {
api: "azure-openai-responses",
provider: "azure-openai-responses",
baseUrl: "",
cost: {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite,
},
contextWindow: AZURE_CONTEXT_WINDOW_OVERRIDES[model.id] ?? model.contextWindow,
}));
allModels.push(...azureOpenAiModels);
@@ -2179,6 +2213,9 @@ async function generateModels() {
output += `${indent}\t\toutput: ${model.cost.output},\n`;
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
if (model.cost.tiers) {
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
}
output += `${indent}\t},\n`;
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;