232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import type { BetaUsage as Usage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
|
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js'
|
|
import { logEvent } from 'src/services/analytics/index.js'
|
|
import { setHasUnknownModelCost } from '../bootstrap/state.js'
|
|
import { isFastModeEnabled } from './fastMode.js'
|
|
import {
|
|
CLAUDE_3_5_HAIKU_CONFIG,
|
|
CLAUDE_3_5_V2_SONNET_CONFIG,
|
|
CLAUDE_3_7_SONNET_CONFIG,
|
|
CLAUDE_HAIKU_4_5_CONFIG,
|
|
CLAUDE_OPUS_4_1_CONFIG,
|
|
CLAUDE_OPUS_4_5_CONFIG,
|
|
CLAUDE_OPUS_4_6_CONFIG,
|
|
CLAUDE_OPUS_4_CONFIG,
|
|
CLAUDE_SONNET_4_5_CONFIG,
|
|
CLAUDE_SONNET_4_6_CONFIG,
|
|
CLAUDE_SONNET_4_CONFIG,
|
|
} from './model/configs.js'
|
|
import {
|
|
firstPartyNameToCanonical,
|
|
getCanonicalName,
|
|
getDefaultMainLoopModelSetting,
|
|
type ModelShortName,
|
|
} from './model/model.js'
|
|
|
|
// @see https://platform.claude.com/docs/en/about-claude/pricing
|
|
export type ModelCosts = {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
promptCacheWriteTokens: number
|
|
promptCacheReadTokens: number
|
|
webSearchRequests: number
|
|
}
|
|
|
|
// Standard pricing tier for Sonnet models: $3 input / $15 output per Mtok
|
|
export const COST_TIER_3_15 = {
|
|
inputTokens: 3,
|
|
outputTokens: 15,
|
|
promptCacheWriteTokens: 3.75,
|
|
promptCacheReadTokens: 0.3,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
// Pricing tier for Opus 4/4.1: $15 input / $75 output per Mtok
|
|
export const COST_TIER_15_75 = {
|
|
inputTokens: 15,
|
|
outputTokens: 75,
|
|
promptCacheWriteTokens: 18.75,
|
|
promptCacheReadTokens: 1.5,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
// Pricing tier for Opus 4.5: $5 input / $25 output per Mtok
|
|
export const COST_TIER_5_25 = {
|
|
inputTokens: 5,
|
|
outputTokens: 25,
|
|
promptCacheWriteTokens: 6.25,
|
|
promptCacheReadTokens: 0.5,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
// Fast mode pricing for Opus 4.6: $30 input / $150 output per Mtok
|
|
export const COST_TIER_30_150 = {
|
|
inputTokens: 30,
|
|
outputTokens: 150,
|
|
promptCacheWriteTokens: 37.5,
|
|
promptCacheReadTokens: 3,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
// Pricing for Haiku 3.5: $0.80 input / $4 output per Mtok
|
|
export const COST_HAIKU_35 = {
|
|
inputTokens: 0.8,
|
|
outputTokens: 4,
|
|
promptCacheWriteTokens: 1,
|
|
promptCacheReadTokens: 0.08,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
// Pricing for Haiku 4.5: $1 input / $5 output per Mtok
|
|
export const COST_HAIKU_45 = {
|
|
inputTokens: 1,
|
|
outputTokens: 5,
|
|
promptCacheWriteTokens: 1.25,
|
|
promptCacheReadTokens: 0.1,
|
|
webSearchRequests: 0.01,
|
|
} as const satisfies ModelCosts
|
|
|
|
const DEFAULT_UNKNOWN_MODEL_COST = COST_TIER_5_25
|
|
|
|
/**
|
|
* Get the cost tier for Opus 4.6 based on fast mode.
|
|
*/
|
|
export function getOpus46CostTier(fastMode: boolean): ModelCosts {
|
|
if (isFastModeEnabled() && fastMode) {
|
|
return COST_TIER_30_150
|
|
}
|
|
return COST_TIER_5_25
|
|
}
|
|
|
|
// @[MODEL LAUNCH]: Add a pricing entry for the new model below.
|
|
// Costs from https://platform.claude.com/docs/en/about-claude/pricing
|
|
// Web search cost: $10 per 1000 requests = $0.01 per request
|
|
export const MODEL_COSTS: Record<ModelShortName, ModelCosts> = {
|
|
[firstPartyNameToCanonical(CLAUDE_3_5_HAIKU_CONFIG.firstParty)]:
|
|
COST_HAIKU_35,
|
|
[firstPartyNameToCanonical(CLAUDE_HAIKU_4_5_CONFIG.firstParty)]:
|
|
COST_HAIKU_45,
|
|
[firstPartyNameToCanonical(CLAUDE_3_5_V2_SONNET_CONFIG.firstParty)]:
|
|
COST_TIER_3_15,
|
|
[firstPartyNameToCanonical(CLAUDE_3_7_SONNET_CONFIG.firstParty)]:
|
|
COST_TIER_3_15,
|
|
[firstPartyNameToCanonical(CLAUDE_SONNET_4_CONFIG.firstParty)]:
|
|
COST_TIER_3_15,
|
|
[firstPartyNameToCanonical(CLAUDE_SONNET_4_5_CONFIG.firstParty)]:
|
|
COST_TIER_3_15,
|
|
[firstPartyNameToCanonical(CLAUDE_SONNET_4_6_CONFIG.firstParty)]:
|
|
COST_TIER_3_15,
|
|
[firstPartyNameToCanonical(CLAUDE_OPUS_4_CONFIG.firstParty)]: COST_TIER_15_75,
|
|
[firstPartyNameToCanonical(CLAUDE_OPUS_4_1_CONFIG.firstParty)]:
|
|
COST_TIER_15_75,
|
|
[firstPartyNameToCanonical(CLAUDE_OPUS_4_5_CONFIG.firstParty)]:
|
|
COST_TIER_5_25,
|
|
[firstPartyNameToCanonical(CLAUDE_OPUS_4_6_CONFIG.firstParty)]:
|
|
COST_TIER_5_25,
|
|
}
|
|
|
|
/**
|
|
* Calculates the USD cost based on token usage and model cost configuration
|
|
*/
|
|
function tokensToUSDCost(modelCosts: ModelCosts, usage: Usage): number {
|
|
return (
|
|
(usage.input_tokens / 1_000_000) * modelCosts.inputTokens +
|
|
(usage.output_tokens / 1_000_000) * modelCosts.outputTokens +
|
|
((usage.cache_read_input_tokens ?? 0) / 1_000_000) *
|
|
modelCosts.promptCacheReadTokens +
|
|
((usage.cache_creation_input_tokens ?? 0) / 1_000_000) *
|
|
modelCosts.promptCacheWriteTokens +
|
|
(usage.server_tool_use?.web_search_requests ?? 0) *
|
|
modelCosts.webSearchRequests
|
|
)
|
|
}
|
|
|
|
export function getModelCosts(model: string, usage: Usage): ModelCosts {
|
|
const shortName = getCanonicalName(model)
|
|
|
|
// Check if this is an Opus 4.6 model with fast mode active.
|
|
if (
|
|
shortName === firstPartyNameToCanonical(CLAUDE_OPUS_4_6_CONFIG.firstParty)
|
|
) {
|
|
const isFastMode = usage.speed === 'fast'
|
|
return getOpus46CostTier(isFastMode)
|
|
}
|
|
|
|
const costs = MODEL_COSTS[shortName]
|
|
if (!costs) {
|
|
trackUnknownModelCost(model, shortName)
|
|
return (
|
|
MODEL_COSTS[getCanonicalName(getDefaultMainLoopModelSetting())] ??
|
|
DEFAULT_UNKNOWN_MODEL_COST
|
|
)
|
|
}
|
|
return costs
|
|
}
|
|
|
|
function trackUnknownModelCost(model: string, shortName: ModelShortName): void {
|
|
logEvent('tengu_unknown_model_cost', {
|
|
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
shortName:
|
|
shortName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
})
|
|
setHasUnknownModelCost()
|
|
}
|
|
|
|
// Calculate the cost of a query in US dollars.
|
|
// If the model's costs are not found, use the default model's costs.
|
|
export function calculateUSDCost(resolvedModel: string, usage: Usage): number {
|
|
const modelCosts = getModelCosts(resolvedModel, usage)
|
|
return tokensToUSDCost(modelCosts, usage)
|
|
}
|
|
|
|
/**
|
|
* Calculate cost from raw token counts without requiring a full BetaUsage object.
|
|
* Useful for side queries (e.g. classifier) that track token counts independently.
|
|
*/
|
|
export function calculateCostFromTokens(
|
|
model: string,
|
|
tokens: {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadInputTokens: number
|
|
cacheCreationInputTokens: number
|
|
},
|
|
): number {
|
|
const usage: Usage = {
|
|
input_tokens: tokens.inputTokens,
|
|
output_tokens: tokens.outputTokens,
|
|
cache_read_input_tokens: tokens.cacheReadInputTokens,
|
|
cache_creation_input_tokens: tokens.cacheCreationInputTokens,
|
|
} as Usage
|
|
return calculateUSDCost(model, usage)
|
|
}
|
|
|
|
function formatPrice(price: number): string {
|
|
// Format price: integers without decimals, others with 2 decimal places
|
|
// e.g., 3 -> "$3", 0.8 -> "$0.80", 22.5 -> "$22.50"
|
|
if (Number.isInteger(price)) {
|
|
return `$${price}`
|
|
}
|
|
return `$${price.toFixed(2)}`
|
|
}
|
|
|
|
/**
|
|
* Format model costs as a pricing string for display
|
|
* e.g., "$3/$15 per Mtok"
|
|
*/
|
|
export function formatModelPricing(costs: ModelCosts): string {
|
|
return `${formatPrice(costs.inputTokens)}/${formatPrice(costs.outputTokens)} per Mtok`
|
|
}
|
|
|
|
/**
|
|
* Get formatted pricing string for a model
|
|
* Accepts either a short name or full model name
|
|
* Returns undefined if model is not found
|
|
*/
|
|
export function getModelPricingString(model: string): string | undefined {
|
|
const shortName = getCanonicalName(model)
|
|
const costs = MODEL_COSTS[shortName]
|
|
if (!costs) return undefined
|
|
return formatModelPricing(costs)
|
|
}
|