Skip to content

wrap_gemini: streamed responses are traced without their function calls or finish_reason #43

Open

Description

Tracing Method

  • SDK/Client

Language

  • Python

Host

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

  1. Atharvagaur commented on Oct 17, 2026

    @Atharvagaur

    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.

  2. Atharvagaur commented on Oct 18, 2026

    @Atharvagaur

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions