Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion docs/onboarding/ai-observability/_snippets/generation-event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,20 @@ export const GenerationEvent = (): JSX.Element => {
</td>
<td>
<p>
<em>(Optional)</em> Price per cached token write
<em>(Optional)</em> Price per cached token write. For custom Anthropic pricing, this
applies to both cache TTLs unless <code>$ai_cache_write_1h_token_price</code> is
set.
</p>
</td>
</tr>
<tr>
<td style={propertyColumnStyle}>
<code>$ai_cache_write_1h_token_price</code>
</td>
<td>
<p>
<em>(Optional)</em> Price per token written to Anthropic's 1-hour cache. Takes
precedence over <code>$ai_cache_write_token_price</code> for 1-hour writes.
</p>
</td>
</tr>
Expand Down Expand Up @@ -501,6 +514,30 @@ export const GenerationEvent = (): JSX.Element => {
<td>
<p>
<em>(Optional)</em> Number of tokens written to cache (Anthropic-specific)
<br />
When both TTL-specific counts are present, PostHog uses them instead of this
aggregate. The aggregate should equal their sum; if either count is missing, PostHog
uses the aggregate.
</p>
</td>
</tr>
<tr>
<td style={propertyColumnStyle}>
<code>$ai_cache_creation_5m_input_tokens</code>
</td>
<td>
<p>
<em>(Optional)</em> Number of tokens written to Anthropic's 5-minute cache
</p>
</td>
</tr>
<tr>
<td style={propertyColumnStyle}>
<code>$ai_cache_creation_1h_input_tokens</code>
</td>
<td>
<p>
<em>(Optional)</em> Number of tokens written to Anthropic's 1-hour cache
</p>
</td>
</tr>
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/taxonomy/core-filter-definitions-by-group.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const TOKEN_PROPERTIES = [
'$ai_reasoning_tokens',
'$ai_cache_read_input_tokens',
'$ai_cache_creation_input_tokens',
'$ai_cache_creation_5m_input_tokens',
'$ai_cache_creation_1h_input_tokens',
] as const

const AI_EVENT_TYPES = ['$ai_generation', '$ai_embedding', '$ai_span', '$ai_trace', '$ai_metric', '$ai_feedback']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const TOKEN_PROPERTIES = [
'$ai_reasoning_tokens',
'$ai_cache_read_input_tokens',
'$ai_cache_creation_input_tokens',
'$ai_cache_creation_5m_input_tokens',
'$ai_cache_creation_1h_input_tokens',
] as const

/**
Expand Down
1 change: 1 addition & 0 deletions nodejs/src/ingestion/pipelines/ai/costs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const processCost = (event: EventWithProperties): EventWithProperties =>
completion_token: event.properties['$ai_output_token_price'],
cache_read_token: event.properties['$ai_cache_read_token_price'],
cache_write_token: event.properties['$ai_cache_write_token_price'],
cache_write_1h_token: event.properties['$ai_cache_write_1h_token_price'],
request: event.properties['$ai_request_price'],
web_search: event.properties['$ai_web_search_price'],
},
Expand Down
87 changes: 87 additions & 0 deletions nodejs/src/ingestion/pipelines/ai/costs/input-costs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,93 @@ describe('calculateInputCost()', () => {
expectCostToBeCloseTo(result, 0.004275)
})

it.each([
{
name: '5-minute',
cacheCreationTokens: 100,
ttlProperties: {
$ai_cache_creation_5m_input_tokens: 100,
$ai_cache_creation_1h_input_tokens: 0,
},
expectedCost: '0.003375',
},
{
name: '1-hour',
cacheCreationTokens: 100,
ttlProperties: {
$ai_cache_creation_5m_input_tokens: 0,
$ai_cache_creation_1h_input_tokens: 100,
},
expectedCost: '0.0036',
},
{
name: 'mixed-TTL',
cacheCreationTokens: 300,
ttlProperties: {
$ai_cache_creation_5m_input_tokens: 100,
$ai_cache_creation_1h_input_tokens: 200,
},
expectedCost: '0.004575',
},
{
name: 'mixed-TTL numeric strings',
cacheCreationTokens: 300,
ttlProperties: {
$ai_cache_creation_5m_input_tokens: '100',
$ai_cache_creation_1h_input_tokens: '200',
},
expectedCost: '0.004575',
},
{
name: 'legacy aggregate-only',
cacheCreationTokens: 300,
ttlProperties: {},
expectedCost: '0.004125',
},
])('prices $name cache creation tokens', ({ cacheCreationTokens, ttlProperties, expectedCost }) => {
const event = createAnthropicTestEvent(1000, undefined, cacheCreationTokens, ttlProperties)

const result = calculateInputCost(event, ANTHROPIC_MODEL)

expect(result).toBe(expectedCost)
})

it.each([
{
name: '5-minute count only',
ttlProperties: { $ai_cache_creation_5m_input_tokens: 100 },
},
{
name: '1-hour count only',
ttlProperties: { $ai_cache_creation_1h_input_tokens: 200 },
},
])('uses the aggregate when the TTL breakdown has $name', ({ ttlProperties }) => {
const event = createAnthropicTestEvent(1000, undefined, 300, ttlProperties)

const result = calculateInputCost(event, ANTHROPIC_MODEL)

expect(result).toBe('0.004125')
})

it('uses the generic custom cache-write rate for both TTLs when no 1-hour rate is set', () => {
const customModel = createTestModel({
provider: 'custom',
cost: {
prompt_token: 0.000003,
completion_token: 0.000015,
cache_write_token: 0.000004,
},
})
const event = createAnthropicTestEvent(1000, undefined, 300, {
$ai_cache_creation_5m_input_tokens: 100,
$ai_cache_creation_1h_input_tokens: 200,
})

const result = calculateInputCost(event, customModel)

expect(result).toBe('0.0042')
})

it('uses 1.25x multiplier fallback for cache write when not defined', () => {
const modelWithoutCacheWrite = createTestModel({
model: 'claude-2',
Expand Down
36 changes: 30 additions & 6 deletions nodejs/src/ingestion/pipelines/ai/costs/input-costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ const usesInclusiveAnthropicInputTokens = (event: PluginEvent): boolean => {
return provider === 'gateway' && framework === 'vercel'
}

const hasNumericProperty = (event: PluginEvent, key: string): boolean => {
const value = event.properties?.[key]
return (
(typeof value === 'number' && Number.isFinite(value)) ||
(typeof value === 'string' && value.length > 0 && Number.isFinite(Number(value)))
)
}

export const resolveCacheReportingExclusive = (event: PluginEvent): boolean => {
if (!event.properties) {
return false
Expand Down Expand Up @@ -177,12 +185,28 @@ export const calculateInputCost = (event: PluginEvent, cost: ResolvedModelCost):
const cachedTextTokens = cacheReadTokens - cachedAudioInputTokens

if (matchProvider(event, 'anthropic')) {
const cacheWriteTokens = numericProperty(event, '$ai_cache_creation_input_tokens')

const writeCost =
cost.cost.cache_write_token !== undefined
? bigDecimal.multiply(cost.cost.cache_write_token, cacheWriteTokens)
: bigDecimal.multiply(bigDecimal.multiply(cost.cost.prompt_token, 1.25), cacheWriteTokens)
const aggregateCacheWriteTokens = numericProperty(event, '$ai_cache_creation_input_tokens')
const cacheWrite5mTokens = numericProperty(event, '$ai_cache_creation_5m_input_tokens')
const cacheWrite1hTokens = numericProperty(event, '$ai_cache_creation_1h_input_tokens')
const hasCacheWriteBreakdown =
hasNumericProperty(event, '$ai_cache_creation_5m_input_tokens') &&
hasNumericProperty(event, '$ai_cache_creation_1h_input_tokens')
const cacheWriteTokens = hasCacheWriteBreakdown
? cacheWrite5mTokens + cacheWrite1hTokens
: aggregateCacheWriteTokens

const cacheWrite5mRate = cost.cost.cache_write_token ?? bigDecimal.multiply(cost.cost.prompt_token, 1.25)
const cacheWrite1hRate =
cost.cost.cache_write_1h_token ??
(cost.provider === 'custom' && cost.cost.cache_write_token !== undefined
? cost.cost.cache_write_token
: bigDecimal.multiply(cost.cost.prompt_token, 2))
const writeCost = hasCacheWriteBreakdown
? bigDecimal.add(
bigDecimal.multiply(cacheWrite5mRate, cacheWrite5mTokens),
bigDecimal.multiply(cacheWrite1hRate, cacheWrite1hTokens)
)
: bigDecimal.multiply(cacheWrite5mRate, cacheWriteTokens)

const cacheReadCost =
cost.cost.cache_read_token !== undefined
Expand Down
Loading
Loading