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
@@ -360,13 +360,17 @@ export async function processResponsesStream<TApi extends Api>(
output.responseId = response.id;
}
if (response?.usage) {
const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
const inputDetails = response.usage.input_tokens_details as
| { cached_tokens?: number; cache_write_tokens?: number }
| undefined;
const cachedTokens = inputDetails?.cached_tokens || 0;
const cacheWriteTokens = inputDetails?.cache_write_tokens || 0;
output.usage = {
// OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
input: (response.usage.input_tokens || 0) - cachedTokens,
// OpenAI includes cached and cache-write tokens in input_tokens, so subtract both.
input: Math.max(0, (response.usage.input_tokens || 0) - cachedTokens - cacheWriteTokens),
output: response.usage.output_tokens || 0,
cacheRead: cachedTokens,
cacheWrite: 0,
cacheWrite: cacheWriteTokens,
reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0,
totalTokens: response.usage.total_tokens || 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
+15 -4
View File
@@ -10,6 +10,7 @@ import type {
AssistantMessageEventStream,
Context,
Model,
ModelCostRates,
ModelThinkingLevel,
ProviderHeaders,
ProviderStreams,
@@ -383,13 +384,23 @@ export function hasApi<TApi extends Api>(model: Model<Api>, api: TApi): model is
}
export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
let rates: ModelCostRates = model.cost;
let matchedThreshold = -1;
for (const tier of model.cost.tiers ?? []) {
if (inputTokens > tier.inputTokensAbove && tier.inputTokensAbove > matchedThreshold) {
rates = tier;
matchedThreshold = tier.inputTokensAbove;
}
}
// Anthropic charges 2x base input for 1h cache writes.
const longWrite = usage.cacheWrite1h ?? 0;
const shortWrite = usage.cacheWrite - longWrite;
usage.cost.input = (model.cost.input / 1000000) * usage.input;
usage.cost.output = (model.cost.output / 1000000) * usage.output;
usage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;
usage.cost.cacheWrite = (model.cost.cacheWrite * shortWrite + model.cost.input * 2 * longWrite) / 1000000;
usage.cost.input = (rates.input / 1000000) * usage.input;
usage.cost.output = (rates.output / 1000000) * usage.output;
usage.cost.cacheRead = (rates.cacheRead / 1000000) * usage.cacheRead;
usage.cost.cacheWrite = (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1000000;
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
return usage.cost;
}
@@ -89,9 +89,10 @@ export const OPENAI_CODEX_MODELS = {
input: 1,
output: 6,
cacheRead: 0.1,
cacheWrite: 0,
cacheWrite: 1.25,
tiers: [{"inputTokensAbove":272000,"input":2,"output":9,"cacheRead":0.2,"cacheWrite":2.5}],
},
contextWindow: 272000,
contextWindow: 372000,
maxTokens: 128000,
} satisfies Model<"openai-codex-responses">,
"gpt-5.6-sol": {
@@ -107,9 +108,10 @@ export const OPENAI_CODEX_MODELS = {
input: 5,
output: 30,
cacheRead: 0.5,
cacheWrite: 0,
cacheWrite: 6.25,
tiers: [{"inputTokensAbove":272000,"input":10,"output":45,"cacheRead":1,"cacheWrite":12.5}],
},
contextWindow: 272000,
contextWindow: 372000,
maxTokens: 128000,
} satisfies Model<"openai-codex-responses">,
"gpt-5.6-terra": {
@@ -125,9 +127,10 @@ export const OPENAI_CODEX_MODELS = {
input: 2.5,
output: 15,
cacheRead: 0.25,
cacheWrite: 0,
cacheWrite: 3.125,
tiers: [{"inputTokensAbove":272000,"input":5,"output":22.5,"cacheRead":0.5,"cacheWrite":6.25}],
},
contextWindow: 272000,
contextWindow: 372000,
maxTokens: 128000,
} satisfies Model<"openai-codex-responses">,
} as const;
+6 -3
View File
@@ -620,8 +620,9 @@ export const OPENAI_MODELS = {
output: 6,
cacheRead: 0.1,
cacheWrite: 1.25,
tiers: [{"inputTokensAbove":272000,"input":2,"output":9,"cacheRead":0.2,"cacheWrite":2.5}],
},
contextWindow: 1050000,
contextWindow: 272000,
maxTokens: 128000,
} satisfies Model<"openai-responses">,
"gpt-5.6-sol": {
@@ -638,8 +639,9 @@ export const OPENAI_MODELS = {
output: 30,
cacheRead: 0.5,
cacheWrite: 6.25,
tiers: [{"inputTokensAbove":272000,"input":10,"output":45,"cacheRead":1,"cacheWrite":12.5}],
},
contextWindow: 1050000,
contextWindow: 272000,
maxTokens: 128000,
} satisfies Model<"openai-responses">,
"gpt-5.6-terra": {
@@ -656,8 +658,9 @@ export const OPENAI_MODELS = {
output: 15,
cacheRead: 0.25,
cacheWrite: 3.125,
tiers: [{"inputTokensAbove":272000,"input":5,"output":22.5,"cacheRead":0.5,"cacheWrite":6.25}],
},
contextWindow: 1050000,
contextWindow: 272000,
maxTokens: 128000,
} satisfies Model<"openai-responses">,
"o1": {
+18 -6
View File
@@ -662,6 +662,23 @@ export interface VercelGatewayRouting {
order?: string[];
}
export interface ModelCostRates {
input: number; // $/million tokens
output: number; // $/million tokens
cacheRead: number; // $/million tokens
cacheWrite: number; // $/million tokens
}
export interface ModelCostTier extends ModelCostRates {
/** Use this tier for requests whose total input usage exceeds this token count. */
inputTokensAbove: number;
}
export interface ModelCost extends ModelCostRates {
/** Request-wide pricing tiers. The highest matching input threshold applies to the full request. */
tiers?: ModelCostTier[];
}
// Model interface for the unified model system
export interface Model<TApi extends Api> {
id: string;
@@ -676,12 +693,7 @@ export interface Model<TApi extends Api> {
*/
thinkingLevelMap?: ThinkingLevelMap;
input: ("text" | "image")[];
cost: {
input: number; // $/million tokens
output: number; // $/million tokens
cacheRead: number; // $/million tokens
cacheWrite: number; // $/million tokens
};
cost: ModelCost;
contextWindow: number;
maxTokens: number;
headers?: Record<string, string>;