Skip to content

feat(ai): record the Anthropic web search count in the LangChain callback handler - #1097

Open
pcbeingused333 wants to merge 5 commits into
PostHog:mainfrom
pcbeingused333:fix/langchain-web-search-count
Open

pcbeingused333 wants to merge 5 commits into
PostHog:mainfrom
pcbeingused333:fix/langchain-web-search-count

Conversation

@pcbeingused333

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

The posthog-js LangChain handler records $ai_web_search_count, and so do the Python OpenAI, Anthropic and Gemini wrappers, but the Python LangChain callback handler never did. When Anthropic reports usage.server_tool_use.web_search_requests (as a dict or an object), _parse_usage_model now reads it into a new web_search_count field of ModelUsage, and the handler adds $ai_web_search_count to the event, including a count of 0. Nothing is added when the provider reports no count. langchain-anthropic passes the raw Anthropic usage (with server_tool_use) in llm_output for non-streamed calls, which is where the handler reads it; streamed calls only carry the standard usage_metadata, so no count is recorded for them. Only this exact count is handled; posthog-js also infers a single search from citations or grounding metadata for other providers, which is not ported here.

💚 How did you test it?

  • TestWebSearchCount in posthog/test/ai/langchain/test_callbacks.py: a response with two web search requests, one with zero, and one without server_tool_use. The first two fail on master and pass with the change; the third guards the omitted case.
  • posthog/test/ai/langchain/test_callbacks.py: 83 passed, 8 skipped. ruff format --check passes on the touched directories. The rest of the suite and a live LangChain call with the Anthropic web search tool were not run.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed. (n/a)
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file (added .sampo/changesets/langchain-web-search-count.md)

🤖 Agent context

Autonomy: Fully autonomous

  • The gap was found and the change, tests and description were written by Claude Code (Claude Sonnet 5.5) by comparing the Python and JS LangChain handlers. Agent-authored, needs human review.

🤖 Generated with Claude Code

🤖 Generated with Claude Code

@pcbeingused333
pcbeingused333 requested a review from a team as a code owner October 21, 2026 17:10

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated advisory code review.

Comment thread references/public_api_snapshot.txt
@marandaneto
marandaneto requested review from a team October 22, 2026 10:19
@marandaneto

Copy link
Copy Markdown
Member

@PostHog/team-ai-observability do we need any backend/frontend change for this?

pcbeingused333 and others added 4 commits October 23, 2026 11:32
…back handler

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	posthog/test/ai/langchain/test_callbacks.py
@marandaneto
marandaneto force-pushed the fix/langchain-web-search-count branch from df78690 to dd3f216 Compare October 23, 2026 09:32
Comment thread posthog/ai/langchain/callbacks.py Outdated
)
if usage.reasoning_tokens is not None:
event_properties["$ai_reasoning_tokens"] = usage.reasoning_tokens
if usage.web_search_count is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emits $ai_web_search_count even when it's 0, but the other Python wrappers only emit it when it's > 0 (posthog/ai/utils.py), so a LangChain Anthropic event would differ from a direct Anthropic wrapper event for the same call. Can we gate on > 0 here too?

Comment thread posthog/ai/langchain/callbacks.py Outdated
parsed_usage["reasoning"] = usage["output_token_details"].get("reasoning")

# Anthropic reports the exact number of web searches of a response
server_tool_use = usage.get("server_tool_use")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This never fires on real ChatAnthropic output. langchain-anthropic always sets AIMessage.usage_metadata, so the generations loop in _parse_usage runs after the llm_output["usage"] parse and overwrites it with usage that has no server_tool_use, resetting web_search_count to None. Same llm_output gives 2 without usage_metadata on the message and None with it. Could _parse_usage read server_tool_use from llm_output["usage"] separately and merge it into whichever ModelUsage wins for the tokens?


class TestWebSearchCount:
@staticmethod
def _captured_properties(llm_output):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this follow the rest of the file? The mock_client fixture and the module-level imports already cover CallbackHandler, uuid, AIMessage and MagicMock, and the three tests could be one @pytest.mark.parametrize test like test_stop_reason_resolution just above.

metadata={"ls_provider": "anthropic", "ls_model_name": "claude-sonnet"},
)
response = LLMResult(
generations=[[ChatGeneration(message=AIMessage(content="r"))]],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatAnthropic always sets usage_metadata on the message, and this fixture leaves it out, which is why the tests pass while the feature does nothing on real output. With usage_metadata={"input_tokens": 10, "output_tokens": 5, "total_tokens": 15} they cover the real path, and should fail until the override above is fixed.

@carlos-marchal-ph

Copy link
Copy Markdown
Contributor

@PostHog/team-ai-observability do we need any backend/frontend change for this?

No, but there were some issues with the implementation that I surfaced on my review

…d omit zero

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@pcbeingused333

Copy link
Copy Markdown
Contributor Author

Thanks, all four points addressed in the latest commit:

  • $ai_web_search_count is now emitted only when the count is greater than 0, like posthog/ai/utils.py.
  • server_tool_use is read from llm_output["usage"] separately in _parse_usage and merged into whichever ModelUsage wins for tokens, so it is no longer lost when usage_metadata is set on the message.
  • The three tests are now one parametrized test_web_search_count using the module-level imports and the mock_client fixture.
  • The fixture sets usage_metadata={"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}; with the callbacks.py change reverted the two-searches case fails, and with it the test passes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants