|
| 1 | +"""Tests for usage extraction in the LangGraph tracing handler. |
| 2 | +
|
| 3 | +Uses duck-typed fakes rather than real langchain_core objects because the ADK |
| 4 | +test conftest mocks langchain_core; ``_serialize_llm_result`` only relies on |
| 5 | +attribute access, so the fakes match the runtime contract. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +from types import SimpleNamespace |
| 11 | + |
| 12 | +from agentex.lib.adk._modules._langgraph_tracing import _serialize_llm_result |
| 13 | + |
| 14 | + |
| 15 | +def _make_result(msg=None, text: str = "hello", llm_output=None): |
| 16 | + generation = SimpleNamespace(text=text, message=msg) |
| 17 | + return SimpleNamespace(generations=[[generation]], llm_output=llm_output) |
| 18 | + |
| 19 | + |
| 20 | +def _make_message(content="hello", usage_metadata=None, tool_calls=None): |
| 21 | + return SimpleNamespace(content=content, usage_metadata=usage_metadata, tool_calls=tool_calls) |
| 22 | + |
| 23 | + |
| 24 | +class TestSerializeLLMResultUsage: |
| 25 | + def test_usage_from_usage_metadata(self): |
| 26 | + msg = _make_message( |
| 27 | + usage_metadata={ |
| 28 | + "input_tokens": 100, |
| 29 | + "output_tokens": 40, |
| 30 | + "total_tokens": 140, |
| 31 | + "input_token_details": {"cache_read": 25}, |
| 32 | + "output_token_details": {"reasoning": 12}, |
| 33 | + } |
| 34 | + ) |
| 35 | + output = _serialize_llm_result(_make_result(msg=msg)) |
| 36 | + |
| 37 | + assert output["content"] == "hello" |
| 38 | + assert output["usage"] == { |
| 39 | + "input_tokens": 100, |
| 40 | + "output_tokens": 40, |
| 41 | + "total_tokens": 140, |
| 42 | + "cached_input_tokens": 25, |
| 43 | + "reasoning_tokens": 12, |
| 44 | + } |
| 45 | + |
| 46 | + def test_usage_metadata_without_details(self): |
| 47 | + msg = _make_message(usage_metadata={"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}) |
| 48 | + output = _serialize_llm_result(_make_result(msg=msg)) |
| 49 | + |
| 50 | + assert output["usage"] == {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15} |
| 51 | + |
| 52 | + def test_fallback_to_llm_output_token_usage(self): |
| 53 | + msg = _make_message(usage_metadata=None) |
| 54 | + result = _make_result( |
| 55 | + msg=msg, |
| 56 | + llm_output={ |
| 57 | + "token_usage": { |
| 58 | + "prompt_tokens": 30, |
| 59 | + "completion_tokens": 20, |
| 60 | + "total_tokens": 50, |
| 61 | + # Nested detail dicts are dropped: the span blob stays flat |
| 62 | + "completion_tokens_details": {"reasoning_tokens": 4}, |
| 63 | + } |
| 64 | + }, |
| 65 | + ) |
| 66 | + output = _serialize_llm_result(result) |
| 67 | + |
| 68 | + assert output["usage"] == { |
| 69 | + "prompt_tokens": 30, |
| 70 | + "completion_tokens": 20, |
| 71 | + "total_tokens": 50, |
| 72 | + } |
| 73 | + |
| 74 | + def test_no_usage_available_omits_key(self): |
| 75 | + msg = _make_message(usage_metadata=None) |
| 76 | + output = _serialize_llm_result(_make_result(msg=msg)) |
| 77 | + |
| 78 | + assert "usage" not in output |
| 79 | + assert output["content"] == "hello" |
| 80 | + |
| 81 | + def test_content_blocks_and_tool_calls_still_serialized(self): |
| 82 | + msg = _make_message( |
| 83 | + content=[{"type": "text", "text": "block text"}], |
| 84 | + usage_metadata={"input_tokens": 1, "output_tokens": 2, "total_tokens": 3}, |
| 85 | + tool_calls=[{"name": "search", "args": {"q": "x"}}], |
| 86 | + ) |
| 87 | + output = _serialize_llm_result(_make_result(msg=msg)) |
| 88 | + |
| 89 | + assert output["content"] == "block text" |
| 90 | + assert output["tool_calls"] == [{"name": "search", "args": {"q": "x"}}] |
| 91 | + assert output["usage"]["total_tokens"] == 3 |
0 commit comments