Skip to content
Closed
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
19 changes: 19 additions & 0 deletions products/ai_observability/frontend/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1442,13 +1442,22 @@ describe.each(IMPLS)('AI observability utils [$name]', ({ normalizeMessage, norm
'custom {type:"function", tool_name, content}',
{ type: 'function', tool_name: 'lookup', content: 'opaque' },
],
[
'legacy Gemini function_response (posthog-python 7.30.1-7.35.x)',
{ type: 'function_response', function_response: { name: 'get_weather', response: { temp: '18C' } } },
],
])('returns true for: %s', (_, item) => {
expect(isToolResult(item)).toBe(true)
})

it.each<[name: string, item: unknown]>([
['plain text part', { type: 'text', text: 'hi' }],
['Anthropic tool_use (a tool CALL, not a result)', { type: 'tool_use', id: 't1', name: 'x', input: {} }],
[
'legacy Gemini function_call (a tool CALL, not a result)',
{ type: 'function_call', function_call: { name: 'get_weather', args: {} } },
],
['function_response without the nested payload', { type: 'function_response' }],
[
'OpenAI tool CALL with nested function object',
{ type: 'function', function: { name: 'get_weather', arguments: '{}' } },
Expand Down Expand Up @@ -2582,6 +2591,16 @@ describe.each(IMPLS)('AI observability utils [$name]', ({ normalizeMessage, norm
{ type: 'tool-result', toolCallId: 'a', toolName: 'search_docs', result: 'ok' },
true,
],
[
'accepts legacy Gemini `function_call`',
{ type: 'function_call', function_call: { name: 'get_weather', args: { city: 'SF' } } },
true,
],
[
'accepts legacy Gemini `function_response`',
{ type: 'function_response', function_response: { name: 'get_weather', response: { temp: '18C' } } },
true,
],
['rejects text content items', { type: 'text', text: 'hi' }, false],
['rejects image items', { type: 'image_url', image_url: { url: 'x' } }, false],
['rejects file items', { type: 'file', file: { filename: 'f', file_data: 'd' } }, false],
Expand Down
22 changes: 20 additions & 2 deletions products/ai_observability/frontend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,28 @@ function isCustomFunctionToolResult(item: unknown): boolean {
return isObject(item) && item.type === 'function' && isString(item.tool_name) && !isObject(item.function)
}

// Gemini function calls and their responses, in the raw form the google-genai
// SDK emits. posthog-python 7.30.1 through 7.35.x copied that form into
// `$ai_input` without converting it (PostHog/posthog-python#725), so traces from
// that window still contain it.
// `function_call` needs its nested object checked. The OpenAI Responses API uses
// the same `type` with a top-level `name` and `call_id`, and
// `isOpenAIResponsesFunctionCall` already owns that case.
function isLegacyGeminiFunctionResponse(item: unknown): boolean {
return isObject(item) && item.type === 'function_response' && isObject(item.function_response)
}

function isLegacyGeminiFunctionCall(item: unknown): boolean {
return isObject(item) && item.type === 'function_call' && isObject(item.function_call)
}

export function isToolResult(item: unknown): boolean {
return (
isAnthropicToolResultMessage(item) ||
isVercelSDKToolResultMessage(item) ||
isOpenAIResponsesFunctionCallOutput(item) ||
isCustomFunctionToolResult(item)
isCustomFunctionToolResult(item) ||
isLegacyGeminiFunctionResponse(item)
)
}

Expand All @@ -535,7 +551,9 @@ export function isToolStepItem(item: unknown): boolean {
isVercelSDKToolCallMessage(item) ||
isVercelSDKToolResultMessage(item) ||
isOpenAIResponsesFunctionCall(item) ||
isOpenAIResponsesBuiltinToolCall(item)
isOpenAIResponsesBuiltinToolCall(item) ||
isLegacyGeminiFunctionCall(item) ||
isLegacyGeminiFunctionResponse(item)
) {
return true
}
Expand Down
Loading