Skip to content

fix(lite_llm): honor capability for schema and tools - #6955

Closed
JimmyWang0417 wants to merge 1 commit into
google:mainfrom
JimmyWang0417:contrib/issue-6954-lite-llm-capability
Closed

JimmyWang0417 wants to merge 1 commit into
google:mainfrom
JimmyWang0417:contrib/issue-6954-lite-llm-capability

Conversation

@JimmyWang0417

@JimmyWang0417 JimmyWang0417 commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

What's going on

LiteLlm.capabilities reported output_schema_and_tools=True for every
provider and model it wrapped. For providers that cannot combine an output
schema with tools, ADK therefore skipped the SetModelResponseTool fallback
and could repeatedly call tools instead of returning structured output.

The fix

  • Report the capability conservatively (False) by default.
  • Add a keyword-only output_schema_and_tools constructor argument so callers
    can opt in for a known-compatible provider/model combination.
  • Keep the capability-only argument out of LiteLLM completion kwargs.
  • Cover the default and opt-in behavior for LiteLlm and Gemma3Ollama.

Closes #6954

Testing Plan

Unit Tests

  • Added and updated unit tests for the capability default, opt-in, and
    completion-argument filtering.
  • pytest -q tests/unittests/models/test_capabilities.py tests/unittests/models/test_litellm.py tests/unittests/models/test_gemma_llm.py tests/unittests/utils/test_output_schema_utils.py tests/unittests/flows/llm_flows/test_basic_processor.py tests/unittests/flows/llm_flows/test_output_schema_processor.py (514 passed)
  • ruff check and pyink --check on all changed Python files.
  • python -m compileall and git diff --check.

Manual End-to-End (E2E) Tests

  • Re-ran the issue reproducer against the current PR head and its direct
    base using the same two-turn script, one deterministic tool, one Pydantic
    output schema, and one session.

Environment and method:

  • PR base: 86b2e5baaa487558b10fc9f2b48fe0d4ceefb755
  • PR head: ced2d36f601224ef867daf0da4505caf082b3f3d
  • Python 3.13.9; source checkout reports google-adk 2.8.0
  • litellm 1.98.0, google-genai 2.22.0, pydantic 2.13.5
  • Credentialed OpenRouter-compatible gateway, using LiteLLM's
    openrouter/<model-id> route for the Gemini models; credentials and endpoint
    are intentionally omitted.
  • Prompt and tool/schema were kept identical between base and head. Each turn
    was bounded with RunConfig(max_llm_calls=8) to prevent an unbounded baseline
    loop from consuming credits.

Primary reproduction (gemini-3.1-flash-lite, three independent runs, two
turns per run):

Revision Capability Function calls per turn Structured turns Terminated turns Median wall time*
Base 86b2e5b True 1, 8, 8, 8, 8, 8 1/6 1/6 9.741 s
PR head ced2d36 False 2, 2, 2, 2, 2, 2 6/6 6/6 2.216 s

On the PR head every turn followed get_current_time -> set_model_response, returned a valid CityTime object, and terminated. On the
base, five of six turns reached the eight-call safety limit while repeatedly
calling get_current_time; the remaining turn returned structured output
without the fallback tool. This is the same failure mode described in #6954.

*Wall time is the observed wall time of this bounded gateway run, not a
benchmark. Base runs that hit the call limit are not successful completions, so
the primary evidence is the call sequence, termination, and structured-output
validity. The independent live OpenRouter validation in the issue reporter's
PR comment
also measured 4/12/18/19 baseline calls versus exactly 2 calls per turn on the
fix, and is limited to openrouter/google/gemini-3.1-flash-lite.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have added tests that prove my fix is effective.
  • New and existing focused unit tests pass locally.
  • I have manually tested end-to-end with a live provider (see the E2E
    environment and results above).

@google-cla

google-cla Bot commented Aug 30, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@yrangana

yrangana commented Aug 30, 2026 •

Copy link
Copy Markdown

Thanks for picking this up. Confirming it fixes the issue: tested against a live provider, since that was the gap noted in the PR description. Reporter of #6954.

Setup: branch contrib/issue-6954-lite-llm-capability at 4333de9, installed from source into a clean venv; litellm 1.98.0, Python 3.13.14; model openrouter/google/gemini-3.1-flash-lite; an agent with one tool and a BaseModel output_schema; two turns in one session.

Baseline on released google-adk 2.8.0 (capability hardcoded True): 4, 12, 18 and 19 tool calls across four runs of identical code, plus one run that did not terminate within several minutes.

With this branch, default output_schema_and_tools=False:

Sydney  calls=2  10:30 AM  OK
Tokyo   calls=2  09:30 PM  OK

Two function calls per turn, which is the minimum: get_current_time plus set_model_response. Stable across turns, and the second turn still calls the tool rather than answering from history. Capability plumbing also checked: default reports False, and LiteLlm(model, output_schema_and_tools=True) reports True.

The conservative default looks right. LiteLLM cannot answer this question even in principle, since it reports the two capabilities independently and has no API for the combination:

litellm.supports_response_schema(model="openrouter/google/gemini-3.1-flash-lite")   # True
litellm.supports_function_calling(model="openrouter/google/gemini-3.1-flash-lite")  # True

Both True while the combination still loops, so opt-in is the only correct direction.

LGTM from my side.

@yrangana

Copy link
Copy Markdown

Heads up that cla/google is failing on this PR, which blocks review. It is a one-time signature at https://cla.developers.google.com/ and the check re-runs automatically afterwards.

Worth doing soon: #5057 fixed the equivalent problem on the native Gemini path and went stale partly for this reason.

@JimmyWang0417

JimmyWang0417 commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Thank you for taking the time to test this with a live provider and for the careful review. Your detailed comparison of the tool-call counts was very helpful and made the regression much clearer.

You were also right to point out that the Google CLA had not been signed. I overlooked it before opening the PR, and I’m sorry that this delayed the review. The CLA is now signed and the check is passing.

Your test confirms the fix for openrouter/google/gemini-3.1-flash-lite, but it does not establish that the code is error-free for every LiteLLM provider/model combination. The unit tests cover the capability handling, but there may still be cases we have not exercised. I’ll avoid claiming broader coverage than we have and will address any further issues found in review or testing. The default remains conservative (output_schema_and_tools=False), with opt-in for combinations known to support both constraints.

Thanks again for catching the issue and for the thorough, responsible review.

LiteLlm cannot infer whether every provider supports combining an output schema with tools, so report the capability conservatively by default and allow explicit opt-in for known-compatible models. Keep the capability-only option out of LiteLLM completion arguments.

Fixes google#6954
@JimmyWang0417

Copy link
Copy Markdown
Contributor Author

Following up on my previous reply, I reran the same two-turn reproducer on the current PR head and its base using OpenRouter.

Environment: Python 3.13.9, google-adk 2.8.0, LiteLLM 1.98.0, OpenRouter, and gemini-3.1-flash-lite. The reproducer uses one real get_current_time tool, one Pydantic CityTime output schema, and one session. Each version was run three times, for six turns total, with the same prompts, tool, schema, and RunConfig(max_llm_calls=8).

Metric Base (before) PR head (after)
Turns completed normally 1/6 6/6
Turns reaching the 8-call safety limit 5/6 0/6
Valid structured outputs 1/6 6/6
Calls per turn 1, 8, 8, 8, 8, 8 2, 2, 2, 2, 2, 2
Median wall time per turn ~9.741 s ~2.216 s

A turn reaching the 8-call safety limit is counted as not completing normally; this reproduces the reported repeated-tool-call issue. The PR head completed all six turns with the expected call sequence:

get_current_time -> set_model_response

I also checked gemini-2.5-flash-lite and gemini-2.5-flash-nothinking with the same reproducer. Both completed 2/2 turns on the PR head with the same two-call sequence, while the base completed only 1/2 turns for each model.

The focused unit-test suite also passes: 514 passed.

The wall-time values are included only as observations from this capped reproducer, not as a general performance benchmark. These results validate the reported OpenRouter/model path and are not intended to generalize to every LiteLLM provider or model.

copybara-service Bot pushed a commit that referenced this pull request Sep 21, 2026
Resolve LiteLlm capabilities per model via litellm.supports_response_schema with
provider-level fallbacks and explicit Gemini and Claude model family gating.
Providers with native schema support (e.g. OpenAI, Azure OpenAI) continue to pair
schemas directly with tools, even when using custom deployment names missing from
litellm's static model map. Models that cannot combine structured outputs with
function calling (such as Claude models across direct Anthropic or third-party
aggregator routes and Gemini models accessed via LiteLLM/OpenRouter) and any
unmapped providers not affirmed by litellm's static map conservatively default to
output_schema_and_tools=False, falling back to SetModelResponseTool instead of
looping. Users with custom routing requirements can subclass LiteLlm and override
capabilities.

Merge #6955

Closes #6954

PiperOrigin-RevId: 985404522
@adk-bot

adk-bot commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Thank you @JimmyWang0417 for your contribution! 🎉

Your changes have been successfully imported and merged via Copybara in commit e37af14.

Closing this PR as the changes are now in the main branch.

@adk-bot adk-bot added the merged [Status] This PR is merged label Sep 21, 2026
@adk-bot adk-bot closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged [Status] This PR is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: LiteLlm.capabilities hardcodes output_schema_and_tools=True, bypassing the SetModelResponseTool workaround

4 participants