Skip to content

Commit b98aa23

Browse files
authored
feat(ai): add $ai_tokens_source property to detect token value overrides (#3147)
Adds "sdk" or "passthrough" to $ai_generation events to flag when token properties are externally overridden via posthogProperties. Mirrors the same change in posthog-python (PostHog/posthog-python#444).
1 parent 85030ed commit b98aa23

4 files changed

Lines changed: 59 additions & 1 deletion

File tree

.changeset/ai-tokens-source.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@posthog/ai': patch
3+
---
4+
5+
Add `$ai_tokens_source` property ("sdk" or "passthrough") to all `$ai_generation` events to detect when token values are externally overridden via `posthogProperties`

packages/ai/src/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ type EmbeddingCreateParams = OpenAIOrignal.EmbeddingCreateParams
1818
type TranscriptionCreateParams = OpenAIOrignal.Audio.Transcriptions.TranscriptionCreateParams
1919
type AnthropicTool = AnthropicOriginal.Tool
2020

21+
const TOKEN_PROPERTY_KEYS = new Set([
22+
'$ai_input_tokens',
23+
'$ai_output_tokens',
24+
'$ai_cache_read_input_tokens',
25+
'$ai_cache_creation_input_tokens',
26+
'$ai_total_tokens',
27+
'$ai_reasoning_tokens',
28+
])
29+
30+
export function getTokensSource(posthogProperties?: Record<string, unknown>): string {
31+
if (posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key))) {
32+
return 'passthrough'
33+
}
34+
return 'sdk'
35+
}
36+
2137
// limit large outputs by truncating to 200kb (approx 200k bytes)
2238
export const MAX_OUTPUT_SIZE = 200000
2339
const STRING_FORMAT = 'utf8'
@@ -727,6 +743,7 @@ export const sendEventToPosthog = async ({
727743
$ai_trace_id: traceId,
728744
$ai_base_url: baseURL,
729745
...params.posthogProperties,
746+
$ai_tokens_source: getTokensSource(params.posthogProperties),
730747
...(distinctId ? {} : { $process_person_profile: false }),
731748
...(tools ? { $ai_tools: tools } : {}),
732749
...errorData,

packages/ai/tests/anthropic.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,25 @@ describe('PostHogAnthropic', () => {
389389
const [captureArgs] = captureMock.mock.calls
390390
const { properties } = captureArgs[0]
391391
expect(properties['$ai_usage']).toBeDefined()
392+
expect(properties['$ai_tokens_source']).toBe('sdk')
393+
})
394+
395+
conditionalTest('should set tokens_source to passthrough when token properties are overridden', async () => {
396+
const response = await client.messages.create({
397+
model: 'claude-3-opus-20240229',
398+
messages: [{ role: 'user', content: 'Hello Claude' }],
399+
max_tokens: 100,
400+
posthogDistinctId: 'test-user-123',
401+
posthogProperties: { $ai_input_tokens: 99999 },
402+
})
403+
404+
expect(response).toEqual(mockResponse)
405+
406+
const captureMock = mockPostHogClient.capture as jest.Mock
407+
const [captureArgs] = captureMock.mock.calls
408+
const { properties } = captureArgs[0]
409+
expect(properties['$ai_tokens_source']).toBe('passthrough')
410+
expect(properties['$ai_input_tokens']).toBe(99999)
392411
})
393412

394413
conditionalTest('should handle system prompts correctly', async () => {

packages/ai/tests/utils.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
import { toContentString } from '../src/utils'
1+
import { toContentString, getTokensSource } from '../src/utils'
2+
3+
describe('getTokensSource', () => {
4+
it.each([
5+
['undefined properties', undefined, 'sdk'],
6+
['empty properties', {}, 'sdk'],
7+
['unrelated properties', { foo: 'bar' }, 'sdk'],
8+
['$ai_input_tokens override', { $ai_input_tokens: 999 }, 'passthrough'],
9+
['$ai_output_tokens override', { $ai_output_tokens: 999 }, 'passthrough'],
10+
['$ai_total_tokens override', { $ai_total_tokens: 999 }, 'passthrough'],
11+
['$ai_cache_read_input_tokens override', { $ai_cache_read_input_tokens: 500 }, 'passthrough'],
12+
['$ai_cache_creation_input_tokens override', { $ai_cache_creation_input_tokens: 200 }, 'passthrough'],
13+
['$ai_reasoning_tokens override', { $ai_reasoning_tokens: 300 }, 'passthrough'],
14+
['mixed override and custom', { $ai_input_tokens: 999, custom_key: 'value' }, 'passthrough'],
15+
])('%s → %s', (_name, props, expected) => {
16+
expect(getTokensSource(props)).toBe(expected)
17+
})
18+
})
219

320
describe('toContentString', () => {
421
describe('string inputs', () => {

0 commit comments

Comments
 (0)