feat(ai): add input-based pricing tiers
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user