diff --git a/docs/onboarding/ai-observability/_snippets/generation-event.tsx b/docs/onboarding/ai-observability/_snippets/generation-event.tsx
index 5ff96d23d936..c7717b69c031 100644
--- a/docs/onboarding/ai-observability/_snippets/generation-event.tsx
+++ b/docs/onboarding/ai-observability/_snippets/generation-event.tsx
@@ -424,7 +424,20 @@ export const GenerationEvent = (): JSX.Element => {
- (Optional) Price per cached token write
+ (Optional) Price per cached token write. For custom Anthropic pricing, this
+ applies to both cache TTLs unless $ai_cache_write_1h_token_price is
+ set.
+
+ |
+
+
+
+ $ai_cache_write_1h_token_price
+ |
+
+
+ (Optional) Price per token written to Anthropic's 1-hour cache. Takes
+ precedence over $ai_cache_write_token_price for 1-hour writes.
|
@@ -501,6 +514,30 @@ export const GenerationEvent = (): JSX.Element => {
(Optional) Number of tokens written to cache (Anthropic-specific)
+
+ 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.
+
+ |
+
+
+
+ $ai_cache_creation_5m_input_tokens
+ |
+
+
+ (Optional) Number of tokens written to Anthropic's 5-minute cache
+
+ |
+
+
+
+ $ai_cache_creation_1h_input_tokens
+ |
+
+
+ (Optional) Number of tokens written to Anthropic's 1-hour cache
|
diff --git a/frontend/src/taxonomy/core-filter-definitions-by-group.json b/frontend/src/taxonomy/core-filter-definitions-by-group.json
index fa937da48b4b..26bff736d078 100644
--- a/frontend/src/taxonomy/core-filter-definitions-by-group.json
+++ b/frontend/src/taxonomy/core-filter-definitions-by-group.json
@@ -151,6 +151,22 @@
"label": "AI billable (LLM)",
"system": true
},
+ "$ai_cache_creation_1h_input_tokens": {
+ "description": "The number of tokens created in the 1-hour prompt cache (Anthropic only).",
+ "examples": [
+ 23
+ ],
+ "label": "AI 1-hour cache creation input tokens (LLM)",
+ "type": "Numeric"
+ },
+ "$ai_cache_creation_5m_input_tokens": {
+ "description": "The number of tokens created in the 5-minute prompt cache (Anthropic only).",
+ "examples": [
+ 23
+ ],
+ "label": "AI 5-minute cache creation input tokens (LLM)",
+ "type": "Numeric"
+ },
"$ai_cache_creation_input_tokens": {
"description": "The number of tokens created in the cache for the input prompt (anthropic only).",
"examples": [
@@ -188,6 +204,14 @@
],
"label": "AI cache reporting exclusive (LLM)"
},
+ "$ai_cache_write_1h_token_price": {
+ "description": "The price per token written to the 1-hour prompt cache. Set this to override PostHog's cost calculation for 1-hour cache writes.",
+ "examples": [
+ 6e-06
+ ],
+ "label": "AI 1-hour cache write token price (LLM)",
+ "type": "Numeric"
+ },
"$ai_cache_write_token_price": {
"description": "The price per token written to the prompt cache.",
"examples": [
@@ -4509,6 +4533,22 @@
"label": "AI billable (LLM)",
"system": true
},
+ "$ai_cache_creation_1h_input_tokens": {
+ "description": "The number of tokens created in the 1-hour prompt cache (Anthropic only).",
+ "examples": [
+ 23
+ ],
+ "label": "AI 1-hour cache creation input tokens (LLM)",
+ "type": "Numeric"
+ },
+ "$ai_cache_creation_5m_input_tokens": {
+ "description": "The number of tokens created in the 5-minute prompt cache (Anthropic only).",
+ "examples": [
+ 23
+ ],
+ "label": "AI 5-minute cache creation input tokens (LLM)",
+ "type": "Numeric"
+ },
"$ai_cache_creation_input_tokens": {
"description": "The number of tokens created in the cache for the input prompt (anthropic only).",
"examples": [
@@ -4546,6 +4586,14 @@
],
"label": "AI cache reporting exclusive (LLM)"
},
+ "$ai_cache_write_1h_token_price": {
+ "description": "The price per token written to the 1-hour prompt cache. Set this to override PostHog's cost calculation for 1-hour cache writes.",
+ "examples": [
+ 6e-06
+ ],
+ "label": "AI 1-hour cache write token price (LLM)",
+ "type": "Numeric"
+ },
"$ai_cache_write_token_price": {
"description": "The price per token written to the prompt cache.",
"examples": [
diff --git a/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.test.ts b/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.test.ts
index 8fe89f6191ab..f52db61d3a7a 100644
--- a/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.test.ts
+++ b/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.test.ts
@@ -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']
diff --git a/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.ts b/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.ts
index c7f72aa925a3..6a846ca5e816 100644
--- a/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.ts
+++ b/nodejs/src/ingestion/common/steps/event-preprocessing/validate-ai-event-tokens.ts
@@ -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
/**
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/index.ts b/nodejs/src/ingestion/pipelines/ai/costs/index.ts
index b4dd2c105492..4641f64db11e 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/index.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/index.ts
@@ -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'],
},
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/input-costs.test.ts b/nodejs/src/ingestion/pipelines/ai/costs/input-costs.test.ts
index daf8e3fa8e09..6d9709fe793e 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/input-costs.test.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/input-costs.test.ts
@@ -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',
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/input-costs.ts b/nodejs/src/ingestion/pipelines/ai/costs/input-costs.ts
index 8a017377c16c..6863d9648a88 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/input-costs.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/input-costs.ts
@@ -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
@@ -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
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.test.ts b/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.test.ts
index 869b1482c34c..3d6c627024f6 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.test.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.test.ts
@@ -432,6 +432,116 @@ describe('extractModalityTokens()', () => {
})
describe('cache modality extraction', () => {
+ const anthropicUsage = {
+ input_tokens: 2048,
+ cache_creation_input_tokens: 248,
+ cache_creation: {
+ ephemeral_5m_input_tokens: 148,
+ ephemeral_1h_input_tokens: 100,
+ },
+ }
+ const vercelBedrockUsage = {
+ cacheDetails: [
+ { ttl: 'T5M', inputTokens: 148 },
+ { ttl: 'T1H', inputTokens: 100 },
+ ],
+ }
+
+ it.each([
+ {
+ name: 'direct Anthropic usage',
+ usage: anthropicUsage,
+ expected5mTokens: 148,
+ expected1hTokens: 100,
+ },
+ {
+ name: 'Vercel usage.raw',
+ usage: {
+ usage: {
+ raw: anthropicUsage,
+ },
+ },
+ expected5mTokens: 148,
+ expected1hTokens: 100,
+ },
+ {
+ name: 'Vercel providerMetadata.anthropic.usage',
+ usage: {
+ providerMetadata: {
+ anthropic: {
+ usage: anthropicUsage,
+ },
+ },
+ },
+ expected5mTokens: 148,
+ expected1hTokens: 100,
+ },
+ {
+ name: 'Vercel providerMetadata.amazonBedrock.usage with only one-hour writes',
+ usage: {
+ providerMetadata: {
+ amazonBedrock: {
+ usage: { cacheDetails: [{ ttl: 'T1H', inputTokens: 100 }] },
+ },
+ },
+ },
+ expected5mTokens: 0,
+ expected1hTokens: 100,
+ },
+ {
+ name: 'Vercel rawUsage.providerMetadata.amazonBedrock.usage',
+ usage: {
+ rawUsage: {
+ providerMetadata: {
+ amazonBedrock: { usage: vercelBedrockUsage },
+ },
+ },
+ },
+ expected5mTokens: 148,
+ expected1hTokens: 100,
+ },
+ {
+ name: 'legacy providerMetadata.bedrock.usage with only five-minute writes',
+ usage: {
+ providerMetadata: {
+ bedrock: {
+ usage: { cacheDetails: [{ ttl: '5m', inputTokens: 148 }] },
+ },
+ },
+ },
+ expected5mTokens: 148,
+ expected1hTokens: 0,
+ },
+ {
+ name: 'Vercel providerMetadata with both Bedrock aliases',
+ usage: {
+ providerMetadata: {
+ amazonBedrock: { usage: vercelBedrockUsage },
+ bedrock: {
+ usage: {
+ cacheDetails: [
+ { ttl: '5m', inputTokens: 999 },
+ { ttl: '1h', inputTokens: 999 },
+ ],
+ },
+ },
+ },
+ },
+ expected5mTokens: 148,
+ expected1hTokens: 100,
+ },
+ ])('extracts cache creation tokens by TTL from $name', ({ usage, expected5mTokens, expected1hTokens }) => {
+ const event = createAIEvent({
+ $ai_usage: usage,
+ })
+
+ const result = extractModalityTokens(event)
+
+ expect(result.properties['$ai_cache_creation_5m_input_tokens']).toBe(expected5mTokens)
+ expect(result.properties['$ai_cache_creation_1h_input_tokens']).toBe(expected1hTokens)
+ expect(result.properties['$ai_usage']).toBeUndefined()
+ })
+
it('extracts cached audio tokens from Gemini cacheTokensDetails array format', () => {
const event = createAIEvent({
$ai_usage: {
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.ts b/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.ts
index 060eeb7426a6..d78a023d1754 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/modality-tokens.ts
@@ -35,14 +35,20 @@ const modalityOf = (detail: Record): string | null => {
return typeof modality === 'string' ? modality.toLowerCase() : null
}
-type ExtractionSource = 'gemini_input' | 'gemini_output' | 'gemini_cache' | 'openai_input' | 'openai_cache'
+type ExtractionSource =
+ | 'anthropic_cache'
+ | 'bedrock_cache'
+ | 'gemini_input'
+ | 'gemini_output'
+ | 'gemini_cache'
+ | 'openai_input'
+ | 'openai_cache'
/**
- * Extract modality-specific token counts from raw provider usage metadata.
- * Supports Gemini's promptTokensDetails (input modality), candidatesTokensDetails
- * (output modality), and cacheTokensDetails (cache modality), plus the OpenAI
- * equivalents under prompt_tokens_details. Removes $ai_usage from properties
- * after extraction so it does not get persisted to ClickHouse.
+ * Extract token details used by cost calculation from raw provider usage metadata.
+ * Supports Anthropic's cache-creation TTL breakdown, Gemini's input/output/cache
+ * modality details, and OpenAI's prompt_tokens_details. Removes $ai_usage from
+ * properties after extraction so it does not get persisted to ClickHouse.
*/
export const extractModalityTokens = (event: EventWithProperties): EventWithProperties => {
const usage = event.properties['$ai_usage']
@@ -202,6 +208,67 @@ export const extractModalityTokens = (event: EventWithProperties): EventWithProp
}
}
+ const extractAnthropicCacheCreation = (metadata: Record): void => {
+ const cacheCreation = metadata['cache_creation']
+ if (!isObject(cacheCreation)) {
+ return
+ }
+
+ const fiveMinuteTokens = cacheCreation['ephemeral_5m_input_tokens']
+ const oneHourTokens = cacheCreation['ephemeral_1h_input_tokens']
+ if (typeof fiveMinuteTokens === 'number' && fiveMinuteTokens >= 0) {
+ event.properties['$ai_cache_creation_5m_input_tokens'] = fiveMinuteTokens
+ extractedSources.add('anthropic_cache')
+ }
+ if (typeof oneHourTokens === 'number' && oneHourTokens >= 0) {
+ event.properties['$ai_cache_creation_1h_input_tokens'] = oneHourTokens
+ extractedSources.add('anthropic_cache')
+ }
+ }
+
+ const extractBedrockCacheCreation = (metadata: Record): void => {
+ const amazonBedrockMetadata = metadata['amazonBedrock']
+ const bedrockMetadata = isObject(amazonBedrockMetadata) ? amazonBedrockMetadata : metadata['bedrock']
+ if (!isObject(bedrockMetadata) || !isObject(bedrockMetadata['usage'])) {
+ return
+ }
+
+ const cacheDetails = bedrockMetadata['usage']['cacheDetails']
+ if (!Array.isArray(cacheDetails)) {
+ return
+ }
+
+ let fiveMinuteTokens = 0
+ let oneHourTokens = 0
+ let foundCacheDetails = false
+
+ for (const detail of cacheDetails) {
+ if (!isObject(detail)) {
+ continue
+ }
+
+ const inputTokens = detail['inputTokens']
+ const ttl = typeof detail['ttl'] === 'string' ? detail['ttl'].toLowerCase() : null
+ if (typeof inputTokens !== 'number' || !Number.isFinite(inputTokens) || inputTokens < 0) {
+ continue
+ }
+
+ if (ttl === 't5m' || ttl === '5m') {
+ fiveMinuteTokens += inputTokens
+ foundCacheDetails = true
+ } else if (ttl === 't1h' || ttl === '1h') {
+ oneHourTokens += inputTokens
+ foundCacheDetails = true
+ }
+ }
+
+ if (foundCacheDetails) {
+ event.properties['$ai_cache_creation_5m_input_tokens'] = fiveMinuteTokens
+ event.properties['$ai_cache_creation_1h_input_tokens'] = oneHourTokens
+ extractedSources.add('bedrock_cache')
+ }
+ }
+
// Walk each `usage`-shaped metadata object encountered across the SDK
// wrapper variants and pull every modality breakdown it exposes.
// Gemini metadata arrives camelCased through the Vercel AI SDK and
@@ -210,6 +277,7 @@ export const extractModalityTokens = (event: EventWithProperties): EventWithProp
if (!isObject(metadata)) {
return
}
+ extractAnthropicCacheCreation(metadata)
extractInputModality(pick(metadata, 'promptTokensDetails', 'prompt_tokens_details'))
extractOutputModality(
pick(metadata, 'candidatesTokensDetails', 'candidates_tokens_details', 'outputTokenDetails')
@@ -221,6 +289,12 @@ export const extractModalityTokens = (event: EventWithProperties): EventWithProp
extractFromMetadata(usage)
+ // @posthog/ai preserves raw Vercel usage under usage.raw.
+ const usageDetails = (usage as Record)['usage']
+ if (isObject(usageDetails)) {
+ extractFromMetadata(usageDetails['raw'])
+ }
+
// Vercel AI SDK with rawResponse at top level: { rawResponse: { usageMetadata: {...} } }
const topLevelRawResponse = (usage as Record)['rawResponse']
if (isObject(topLevelRawResponse)) {
@@ -231,6 +305,13 @@ export const extractModalityTokens = (event: EventWithProperties): EventWithProp
const providerMetadata = (usage as Record)['providerMetadata']
if (isObject(providerMetadata)) {
extractFromMetadata(providerMetadata['google'])
+ extractBedrockCacheCreation(providerMetadata)
+
+ // Anthropic's original usage is also available in provider metadata.
+ const anthropicProviderMetadata = providerMetadata['anthropic']
+ if (isObject(anthropicProviderMetadata)) {
+ extractFromMetadata(anthropicProviderMetadata['usage'])
+ }
}
// Vercel AI SDK V3 / nested rawUsage variants:
@@ -242,6 +323,7 @@ export const extractModalityTokens = (event: EventWithProperties): EventWithProp
const rawProviderMetadata = rawUsage['providerMetadata']
if (isObject(rawProviderMetadata)) {
extractFromMetadata(rawProviderMetadata['google'])
+ extractBedrockCacheCreation(rawProviderMetadata)
}
const rawUsageUsage = rawUsage['usage']
diff --git a/nodejs/src/ingestion/pipelines/ai/costs/providers/types.ts b/nodejs/src/ingestion/pipelines/ai/costs/providers/types.ts
index 36eac4abb26f..f27095973148 100644
--- a/nodejs/src/ingestion/pipelines/ai/costs/providers/types.ts
+++ b/nodejs/src/ingestion/pipelines/ai/costs/providers/types.ts
@@ -3,6 +3,7 @@ export interface ModelCost {
completion_token: number
cache_read_token?: number
cache_write_token?: number
+ cache_write_1h_token?: number
request?: number
web_search?: number
image?: number
diff --git a/nodejs/src/ingestion/pipelines/ai/process-ai-event.test.ts b/nodejs/src/ingestion/pipelines/ai/process-ai-event.test.ts
index b5e885776d06..7bd33a5c6575 100644
--- a/nodejs/src/ingestion/pipelines/ai/process-ai-event.test.ts
+++ b/nodejs/src/ingestion/pipelines/ai/process-ai-event.test.ts
@@ -885,6 +885,81 @@ describe('processAiEvent()', () => {
expect(result.properties!.$ai_total_cost_usd).toBeCloseTo(0.0054, 6)
})
+ it('extracts and prices Anthropic mixed-TTL cache writes with custom rates', () => {
+ event.properties!.$ai_provider = 'anthropic'
+ event.properties!.$ai_input_token_price = 0.000003
+ event.properties!.$ai_output_token_price = 0.000015
+ event.properties!.$ai_cache_write_token_price = 0.000004
+ event.properties!.$ai_cache_write_1h_token_price = 0.000007
+ event.properties!.$ai_input_tokens = 1000
+ event.properties!.$ai_output_tokens = 100
+ event.properties!.$ai_cache_creation_input_tokens = 300
+ event.properties!.$ai_usage = {
+ usage: {
+ inputTokens: {
+ total: 1300,
+ noCache: 1000,
+ cacheRead: 0,
+ cacheWrite: 300,
+ },
+ outputTokens: { total: 100, text: 100, reasoning: 0 },
+ raw: {
+ cache_creation_input_tokens: 300,
+ cache_creation: {
+ ephemeral_5m_input_tokens: 100,
+ ephemeral_1h_input_tokens: 200,
+ },
+ },
+ },
+ providerMetadata: {
+ anthropic: {
+ usage: {
+ cache_creation_input_tokens: 300,
+ cache_creation: {
+ ephemeral_5m_input_tokens: 100,
+ ephemeral_1h_input_tokens: 200,
+ },
+ },
+ },
+ },
+ }
+
+ const result = processAiEvent(event)
+
+ expect(result.properties!.$ai_cache_creation_5m_input_tokens).toBe(100)
+ expect(result.properties!.$ai_cache_creation_1h_input_tokens).toBe(200)
+ expect(result.properties!.$ai_usage).toBeUndefined()
+ expect(result.properties!.$ai_input_cost_usd).toBeCloseTo(0.0048, 8)
+ expect(result.properties!.$ai_total_cost_usd).toBeCloseTo(0.0063, 8)
+ })
+
+ it('extracts and prices Vercel Bedrock mixed-TTL cache writes', () => {
+ event.properties!.$ai_model = 'anthropic/claude-sonnet-4'
+ event.properties!.$ai_provider = 'bedrock'
+ event.properties!.$ai_input_tokens = 1000
+ event.properties!.$ai_output_tokens = 100
+ event.properties!.$ai_cache_creation_input_tokens = 300
+ const bedrockUsage = {
+ cacheDetails: [
+ { ttl: 'T5M', inputTokens: 100 },
+ { ttl: 'T1H', inputTokens: 200 },
+ ],
+ }
+ event.properties!.$ai_usage = {
+ providerMetadata: {
+ amazonBedrock: { usage: bedrockUsage },
+ },
+ }
+
+ const result = processAiEvent(event)
+
+ expect(result.properties!.$ai_input_cost_usd).toBeCloseTo(0.004575, 8)
+ expect(result.properties!.$ai_total_cost_usd).toBeCloseTo(0.006075, 8)
+ expect(result.properties!.$ai_cache_creation_5m_input_tokens).toBe(100)
+ expect(result.properties!.$ai_cache_creation_1h_input_tokens).toBe(200)
+ expect(result.properties!.$ai_usage).toBeUndefined()
+ })
+
it('falls back to multipliers when cache prices not provided', () => {
event.properties!.$ai_provider = 'openai'
event.properties!.$ai_input_token_price = 0.001
diff --git a/posthog/taxonomy/taxonomy.py b/posthog/taxonomy/taxonomy.py
index 340e9c273b8c..b0d2ab9cdd45 100644
--- a/posthog/taxonomy/taxonomy.py
+++ b/posthog/taxonomy/taxonomy.py
@@ -2218,6 +2218,18 @@ class CoreFilterDefinition(TypedDict):
"description": "The number of tokens created in the cache for the input prompt (anthropic only).",
"examples": [23],
},
+ "$ai_cache_creation_5m_input_tokens": {
+ "label": "AI 5-minute cache creation input tokens (LLM)",
+ "description": "The number of tokens created in the 5-minute prompt cache (Anthropic only).",
+ "examples": [23],
+ "type": "Numeric",
+ },
+ "$ai_cache_creation_1h_input_tokens": {
+ "label": "AI 1-hour cache creation input tokens (LLM)",
+ "description": "The number of tokens created in the 1-hour prompt cache (Anthropic only).",
+ "examples": [23],
+ "type": "Numeric",
+ },
"$ai_cache_reporting_exclusive": {
"label": "AI cache reporting exclusive (LLM)",
"description": "Whether cache tokens are excluded from the input token count. When true, cache tokens are separate from input tokens (Anthropic-style). When false, input tokens already include cache tokens. Auto-detected from provider when not set explicitly.",
@@ -3151,6 +3163,12 @@ class CoreFilterDefinition(TypedDict):
"examples": [0.00000375],
"type": "Numeric",
},
+ "$ai_cache_write_1h_token_price": {
+ "label": "AI 1-hour cache write token price (LLM)",
+ "description": "The price per token written to the 1-hour prompt cache. Set this to override PostHog's cost calculation for 1-hour cache writes.",
+ "examples": [0.000006],
+ "type": "Numeric",
+ },
"$ai_request_price": {
"label": "AI request price (LLM)",
"description": "The flat per-request price charged by the LLM provider, independent of token usage.",