Repository navigation
wrap_gemini: streamed responses are traced without their function calls or finish_reason #43
Description
Tracing Method
- SDK/Client
Language
- Python
Host
- Hosted (https://api.smith.langchain.com)
Other System Info
- langsmith: main (14bb087), 0.14.11
- google-genai 2.31.0, Python 3.12, Linux
Description
With wrap_gemini, a streamed generate_content_stream call that ends in a function call is traced as plain text: the run output has no tool_calls and no finish_reason. The same response without streaming is traced with both.
_reduce_generate_content_chunks builds the streamed output only from chunk.text (L383-L384). In google-genai, .text returns only the text parts, so function_call parts never reach the run. The non-streamed path, _process_generate_content_response, does convert them (L276).
When the model only calls a tool and sends no text, the streamed run output is {"content": ""}.
Reproduction
No network or API key needed: real GenerateContentResponse objects, shaped like the chunks Gemini streams, go through the two output processors wrap_gemini uses.
from google.genai import types
from langsmith.wrappers import _gemini
text_chunk = {"candidates": [{"content": {"role": "model", "parts": [{"text": "Checking the weather. "}]}, "index": 0}]}
call_chunk = {
"candidates": [{"content": {"role": "model", "parts": [
{"function_call": {"name": "get_weather", "args": {"city": "Paris"}}}]},
"finish_reason": "STOP", "index": 0}],
"usage_metadata": {"prompt_token_count": 5, "candidates_token_count": 7, "total_token_count": 12},
}
chunks = [types.GenerateContentResponse.model_validate(c) for c in (text_chunk, call_chunk)]
full = types.GenerateContentResponse.model_validate({
**call_chunk,
"candidates": [{**call_chunk["candidates"][0], "content": {"role": "model", "parts": [
text_chunk["candidates"][0]["content"]["parts"][0],
call_chunk["candidates"][0]["content"]["parts"][0]]}}],
})
streamed = _gemini._reduce_generate_content_chunks(chunks)
non_streamed = _gemini._process_generate_content_response(full)
print("streamed: ", {k: v for k, v in streamed.items() if k != "usage_metadata"})
print("non-streamed:", {k: v for k, v in non_streamed.items() if k != "usage_metadata"})Output:
streamed: {'content': 'Checking the weather. '}
non-streamed: {'content': 'Checking the weather. ', 'role': 'assistant', 'finish_reason': <FinishReason.STOP: 'STOP'>, 'tool_calls': [{'id': 'call_0', 'type': 'function', 'index': 0, 'function': {'name': 'get_weather', 'arguments': '{"city":"Paris"}'}}]}
The result is the same with chunks taken from a real generate_content_stream call (served by a local fake Gemini endpoint) instead of hand-built ones.
Expected behavior
The streamed run output matches the non-streamed one: role, finish_reason and tool_calls are present.
Proposed fix: in _reduce_generate_content_chunks, collect the parts of every chunk (text, function_call, thought) and the last non-empty finish_reason, then build the output with the same conversion _process_generate_content_response uses, e.g. by passing it a response whose candidate holds the merged parts.
Happy to open a PR with this and a test if that's useful.
This report was drafted with Claude Code, and the reproduction above was run on main to confirm the output.
Activity
Atharvagaur commentedon Oct 17, 2026
Reproduced on main (14bb087) with Python 3.10.11, google-genai 2.25.0 (Windows): the streamed output drops tool_calls, role and finish_reason, while the non-streamed path keeps all three.
alexcastgonz (@pcbeingused333) are you planning to open the PR for this? I already have a working fix with tests (merges parts across chunks, keeps the last non-empty finish_reason, reuses the non-streamed conversion). Happy to open it, or to hand it over if you've started.
Atharvagaur commentedon Oct 18, 2026
Following up: no reply yet, so I'll open the PR by tomorrow with the fix and tests, and link it here. alexcastgonz (@pcbeingused333) if you've already started on it, let me know before then and I'll hold off.