Skip to content

Commit 07cf32b

Browse files
authored
fix(llmo): tool calls are broken for most providers (#299)
* fix(llmo): set the $ai_tools properly for all providers * fix(llmo): remove privacy mode from $ai_tools * chore(llmo): bump version * chore(llmo): run formatter * fix(llmo): properly set tool calls in $ai_output_choices * chore(llmo): bump version * chore(llmo): run formatter * fix(llmo): fix types error * feat(llmo): change $ai_output_choices to have an array of content * chore(llmo): run formatter * feat(llmo): create text type object * chore(llmo): update CHANGELOG.md
1 parent 0076b66 commit 07cf32b

File tree

10 files changed

+1095
-237
lines changed

10 files changed

+1095
-237
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 6.3.4 - 2025-08-04
2+
3+
- fix: Set `$ai_tools` for all providers and `$ai_output_choices` for all non-streaming provider flows properly
4+
15
# 6.3.3 - 2025-08-01
26

37
- fix: `get_feature_flag_result` now correctly returns FeatureFlagResult when payload is empty string instead of None

posthog/ai/langchain/callbacks.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -556,12 +556,9 @@ def _capture_generation(
556556
"$ai_latency": run.latency,
557557
"$ai_base_url": run.base_url,
558558
}
559+
559560
if run.tools:
560-
event_properties["$ai_tools"] = with_privacy_mode(
561-
self._ph_client,
562-
self._privacy_mode,
563-
run.tools,
564-
)
561+
event_properties["$ai_tools"] = run.tools
565562

566563
if isinstance(output, BaseException):
567564
event_properties["$ai_http_status"] = _get_http_status(output)
@@ -587,7 +584,8 @@ def _capture_generation(
587584
]
588585
else:
589586
completions = [
590-
_extract_raw_esponse(generation) for generation in generation_result
587+
_extract_raw_response(generation)
588+
for generation in generation_result
591589
]
592590
event_properties["$ai_output_choices"] = with_privacy_mode(
593591
self._ph_client, self._privacy_mode, completions
@@ -618,7 +616,7 @@ def _log_debug_event(
618616
)
619617

620618

621-
def _extract_raw_esponse(last_response):
619+
def _extract_raw_response(last_response):
622620
"""Extract the response from the last response of the LLM call."""
623621
# We return the text of the response if not empty
624622
if last_response.text is not None and last_response.text.strip() != "":

posthog/ai/openai/openai.py

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from posthog.ai.utils import (
1313
call_llm_and_track_usage,
14+
extract_available_tool_calls,
1415
get_model_params,
1516
with_privacy_mode,
1617
)
@@ -167,6 +168,7 @@ def generator():
167168
usage_stats,
168169
latency,
169170
output,
171+
extract_available_tool_calls("openai", kwargs),
170172
)
171173

172174
return generator()
@@ -182,7 +184,7 @@ def _capture_streaming_event(
182184
usage_stats: Dict[str, int],
183185
latency: float,
184186
output: Any,
185-
tool_calls: Optional[List[Dict[str, Any]]] = None,
187+
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
186188
):
187189
if posthog_trace_id is None:
188190
posthog_trace_id = str(uuid.uuid4())
@@ -212,12 +214,8 @@ def _capture_streaming_event(
212214
**(posthog_properties or {}),
213215
}
214216

215-
if tool_calls:
216-
event_properties["$ai_tools"] = with_privacy_mode(
217-
self._client._ph_client,
218-
posthog_privacy_mode,
219-
tool_calls,
220-
)
217+
if available_tool_calls:
218+
event_properties["$ai_tools"] = available_tool_calls
221219

222220
if posthog_distinct_id is None:
223221
event_properties["$process_person_profile"] = False
@@ -341,7 +339,6 @@ def _create_streaming(
341339
start_time = time.time()
342340
usage_stats: Dict[str, int] = {}
343341
accumulated_content = []
344-
accumulated_tools = {}
345342
if "stream_options" not in kwargs:
346343
kwargs["stream_options"] = {}
347344
kwargs["stream_options"]["include_usage"] = True
@@ -350,7 +347,6 @@ def _create_streaming(
350347
def generator():
351348
nonlocal usage_stats
352349
nonlocal accumulated_content # noqa: F824
353-
nonlocal accumulated_tools # noqa: F824
354350

355351
try:
356352
for chunk in response:
@@ -389,31 +385,12 @@ def generator():
389385
if content:
390386
accumulated_content.append(content)
391387

392-
# Process tool calls
393-
tool_calls = getattr(chunk.choices[0].delta, "tool_calls", None)
394-
if tool_calls:
395-
for tool_call in tool_calls:
396-
index = tool_call.index
397-
if index not in accumulated_tools:
398-
accumulated_tools[index] = tool_call
399-
else:
400-
# Append arguments for existing tool calls
401-
if hasattr(tool_call, "function") and hasattr(
402-
tool_call.function, "arguments"
403-
):
404-
accumulated_tools[
405-
index
406-
].function.arguments += (
407-
tool_call.function.arguments
408-
)
409-
410388
yield chunk
411389

412390
finally:
413391
end_time = time.time()
414392
latency = end_time - start_time
415393
output = "".join(accumulated_content)
416-
tools = list(accumulated_tools.values()) if accumulated_tools else None
417394
self._capture_streaming_event(
418395
posthog_distinct_id,
419396
posthog_trace_id,
@@ -424,7 +401,7 @@ def generator():
424401
usage_stats,
425402
latency,
426403
output,
427-
tools,
404+
extract_available_tool_calls("openai", kwargs),
428405
)
429406

430407
return generator()
@@ -440,7 +417,7 @@ def _capture_streaming_event(
440417
usage_stats: Dict[str, int],
441418
latency: float,
442419
output: Any,
443-
tool_calls: Optional[List[Dict[str, Any]]] = None,
420+
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
444421
):
445422
if posthog_trace_id is None:
446423
posthog_trace_id = str(uuid.uuid4())
@@ -470,12 +447,8 @@ def _capture_streaming_event(
470447
**(posthog_properties or {}),
471448
}
472449

473-
if tool_calls:
474-
event_properties["$ai_tools"] = with_privacy_mode(
475-
self._client._ph_client,
476-
posthog_privacy_mode,
477-
tool_calls,
478-
)
450+
if available_tool_calls:
451+
event_properties["$ai_tools"] = available_tool_calls
479452

480453
if posthog_distinct_id is None:
481454
event_properties["$process_person_profile"] = False

posthog/ai/openai/openai_async.py

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from posthog import setup
1313
from posthog.ai.utils import (
1414
call_llm_and_track_usage_async,
15+
extract_available_tool_calls,
1516
get_model_params,
1617
with_privacy_mode,
1718
)
@@ -168,6 +169,7 @@ async def async_generator():
168169
usage_stats,
169170
latency,
170171
output,
172+
extract_available_tool_calls("openai", kwargs),
171173
)
172174

173175
return async_generator()
@@ -183,7 +185,7 @@ async def _capture_streaming_event(
183185
usage_stats: Dict[str, int],
184186
latency: float,
185187
output: Any,
186-
tool_calls: Optional[List[Dict[str, Any]]] = None,
188+
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
187189
):
188190
if posthog_trace_id is None:
189191
posthog_trace_id = str(uuid.uuid4())
@@ -213,12 +215,8 @@ async def _capture_streaming_event(
213215
**(posthog_properties or {}),
214216
}
215217

216-
if tool_calls:
217-
event_properties["$ai_tools"] = with_privacy_mode(
218-
self._client._ph_client,
219-
posthog_privacy_mode,
220-
tool_calls,
221-
)
218+
if available_tool_calls:
219+
event_properties["$ai_tools"] = available_tool_calls
222220

223221
if posthog_distinct_id is None:
224222
event_properties["$process_person_profile"] = False
@@ -344,7 +342,6 @@ async def _create_streaming(
344342
start_time = time.time()
345343
usage_stats: Dict[str, int] = {}
346344
accumulated_content = []
347-
accumulated_tools = {}
348345

349346
if "stream_options" not in kwargs:
350347
kwargs["stream_options"] = {}
@@ -354,7 +351,6 @@ async def _create_streaming(
354351
async def async_generator():
355352
nonlocal usage_stats
356353
nonlocal accumulated_content # noqa: F824
357-
nonlocal accumulated_tools # noqa: F824
358354

359355
try:
360356
async for chunk in response:
@@ -393,31 +389,12 @@ async def async_generator():
393389
if content:
394390
accumulated_content.append(content)
395391

396-
# Process tool calls
397-
tool_calls = getattr(chunk.choices[0].delta, "tool_calls", None)
398-
if tool_calls:
399-
for tool_call in tool_calls:
400-
index = tool_call.index
401-
if index not in accumulated_tools:
402-
accumulated_tools[index] = tool_call
403-
else:
404-
# Append arguments for existing tool calls
405-
if hasattr(tool_call, "function") and hasattr(
406-
tool_call.function, "arguments"
407-
):
408-
accumulated_tools[
409-
index
410-
].function.arguments += (
411-
tool_call.function.arguments
412-
)
413-
414392
yield chunk
415393

416394
finally:
417395
end_time = time.time()
418396
latency = end_time - start_time
419397
output = "".join(accumulated_content)
420-
tools = list(accumulated_tools.values()) if accumulated_tools else None
421398
await self._capture_streaming_event(
422399
posthog_distinct_id,
423400
posthog_trace_id,
@@ -428,7 +405,7 @@ async def async_generator():
428405
usage_stats,
429406
latency,
430407
output,
431-
tools,
408+
extract_available_tool_calls("openai", kwargs),
432409
)
433410

434411
return async_generator()
@@ -444,7 +421,7 @@ async def _capture_streaming_event(
444421
usage_stats: Dict[str, int],
445422
latency: float,
446423
output: Any,
447-
tool_calls: Optional[List[Dict[str, Any]]] = None,
424+
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
448425
):
449426
if posthog_trace_id is None:
450427
posthog_trace_id = str(uuid.uuid4())
@@ -474,12 +451,8 @@ async def _capture_streaming_event(
474451
**(posthog_properties or {}),
475452
}
476453

477-
if tool_calls:
478-
event_properties["$ai_tools"] = with_privacy_mode(
479-
self._client._ph_client,
480-
posthog_privacy_mode,
481-
tool_calls,
482-
)
454+
if available_tool_calls:
455+
event_properties["$ai_tools"] = available_tool_calls
483456

484457
if posthog_distinct_id is None:
485458
event_properties["$process_person_profile"] = False

0 commit comments

Comments
 (0)