Skip to content

Bug: reasoning deltas combined with newlines in litellm #5645

Closed

Description

@lorenzbaraldi

🔴 Required Information

Describe the Bug:
In the LiteLLM adapter model, reasoning parts are combined with a newline injection (_NEW_LINE.join(text for text in reasoning_texts if text)). When using models/providers that stream reasoning in delta fragments (such as vLLM-style reasoning chunks), this mutates the original stream by inserting extra newline separators between the chunks. Consequently, the reconstructed reasoning differs from the exact output returned by the provider.

Steps to Reproduce:
Please provide a numbered list of steps to reproduce the behavior:

  1. Initialize an ADK Agent using the LiteLlm adapter.
  2. Connect to a model that generates streamed reasoning in delta fragments (eg. vllm deployed model with qwen).
  3. Execute a turn and inspect the streamed reasoning chunks and the final generated reasoning message.
  4. Observe that the reasoning string contains unexpected extra newlines that were not present in the native chunks.

Expected Behavior:
The reasoning text should be preserved exactly as streamed by concatenating fragments directly (i.e. "".join(...)), without artificially adding newlines, so that the stream represents the provider's native output precisely.

Observed Behavior:
Refer to PR #4952 implementation and the proposed fix addressing this issue.

Environment Details:

  • ADK Library Version (pip show google-adk): latest
  • Desktop OS: macOS
  • Python Version (python -V): Python 3.11+

Model Information:

  • Are you using LiteLLM: Yes
  • Which model is being used: Any model streaming reasoning with vllm (e.g. qwen3.5)

Activity

  1. added
    models[Component] This issue is related to model support
    on May 11, 2026
  2. lorenzbaraldi commented on May 11, 2026

    @lorenzbaraldi
    ContributorAuthor

    I previously opened a PR addressing this issue in google/adk-python PR #4952. Let me know if you have any feedback or suggestions.

  3. llalitkumarrr commented on May 11, 2026

    @llalitkumarrr
    Collaborator

    Hello @lorenzbaraldi ,

    Could you please share a minimal reproducible code snippet so we can investigate this issue? Additionally, let us know if you have observed this problem across other models in LiteLLM and how the results compare if you test it using the Gemini() model class. Also if you are not already using google-adk==1.33.0, we recommend upgrading and testing with this version. Sharing this information will help us analyze this issue better.

  4. added
    request clarification[Status] The maintainer need clarification or more information from the author
    on May 11, 2026
  5. lorenzbaraldi commented on May 11, 2026

    @lorenzbaraldi
    ContributorAuthor

    I’m using google-adk==1.33.0.

    I do not currently have credentials available for the native Gemini() path, so unfortunately, I cannot run a live comparison. My understanding from the code is that this issue is specific to the LiteLLM adapter path: the affected logic is _content_to_message_param() in google/adk/models/lite_llm.py, where ADK thought parts are serialized back into LiteLLM reasoning_content.

    This matters for providers such as vLLM, which can stream reasoning output as delta chunks. With vLLM reasoning enabled, streamed chat completion chunks may contain reasoning deltas in delta.reasoning or delta.reasoning_content. ADK stores those streamed reasoning deltas as separate thought parts, and later _content_to_message_param() serializes them back into LiteLLM history. If newline separators are inserted during that serialization, provider-emitted reasoning text can be changed before it is sent back on the next turn.

    Minimal local reproduction of the adapter behavior:

    import asyncio
    from google.genai import types
    from google.adk.models.lite_llm import _content_to_message_param
    
    async def main():
      p1 = types.Part.from_text(text="Hel")
      p1.thought = True
      p2 = types.Part.from_text(text="lo")
      p2.thought = True
    
      content = types.Content(role="assistant", parts=[p1, p2])
      message = await _content_to_message_param(content)
    
      print(repr(message["reasoning_content"]))
    
    asyncio.run(main())

    Before this PR, this produces:

    'Hel\nlo'
    Expected:

    'Hello'

  6. lorenzbaraldi commented on May 26, 2026

    @lorenzbaraldi
    ContributorAuthor

    Hei @llalitkumarrr I just wanted to follow up on this issue. Do you have any news or feedback?

  7. llalitkumarrr commented on Jun 11, 2026

    @llalitkumarrr
    Collaborator

    Hello @lorenzbaraldi ,

    We apologies for the delay, the PR is under review and we will let you know if any additional information is required.

  8. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

models[Component] This issue is related to model supportneeds review[Status] The PR/issue is awaiting review from the maintainer

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions