Skip to content

fix(models): resolve nested provider behind litellm_proxy prefix - #6578

Open
vietnamesekid wants to merge 1 commit into
google:mainfrom
vietnamesekid:fix/litellm-proxy-nested-provider
Open

fix(models): resolve nested provider behind litellm_proxy prefix#6578
vietnamesekid wants to merge 1 commit into
google:mainfrom
vietnamesekid:fix/litellm-proxy-nested-provider

Conversation

@vietnamesekid

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

Problem:

_get_provider_from_model splits the model string on the first / and treats
that segment as the provider. That assumption breaks for nested LiteLLM Proxy
identifiers, because litellm_proxy names the transport, not the model family:

_get_provider_from_model("litellm_proxy/azure/my-deployment")  # -> "litellm_proxy"

litellm_proxy is not in _FILE_ID_REQUIRED_PROVIDERS ({"openai", "azure"}),
so the Azure/OpenAI file-upload path in _get_content never runs. The PDF goes
out as an inline file_data block instead of an uploaded file_id. When the
proxy translates that for the Azure Responses API, Azure rejects the content
item before inference:

BadRequestError: AzureException BadRequestError
Missing required parameter: 'input[0].content[3]'

While tracing this I found the same first-segment assumption in four more
helpers, so the blast radius is wider than the PDF case in the issue. Every
provider-specific behavior silently degrades once a model is reached through
the proxy:

Helper litellm_proxy/... input Before Expected
_get_provider_from_model litellm_proxy/azure/gpt-4 litellm_proxy azure
_is_anthropic_model litellm_proxy/anthropic/claude-4 False True
_is_litellm_vertex_model litellm_proxy/vertex_ai/gemini-2.5-flash False True
_is_litellm_gemini_model litellm_proxy/vertex_ai/gemini-2.5-flash False True
_extract_gemini_model_from_litellm litellm_proxy/vertex_ai/gemini-2.5-pro vertex_ai/gemini-2.5-pro gemini-2.5-pro

Practical effect beyond the PDF bug: proxied Anthropic models lose thinking
block formatting, and proxied Gemini models are not recognized as Vertex or
Gemini routes.

Worth noting that litellm.get_llm_provider() also returns litellm_proxy
for these strings. That is correct for LiteLLM, which only needs to know where
to send the request. ADK uses the value for something different, namely how to
shape the payload, and that has to follow the provider that actually serves
the model. So the fix belongs here rather than upstream.

Solution:

Add _strip_proxy_prefix() and call it before provider and model family
detection. A proxied model is then shaped exactly like its direct equivalent.

Three things I deliberately kept intact:

  1. Model strings without the prefix take the same code path as before. The
    helper returns the input unchanged when there is nothing to strip.
  2. A bare litellm_proxy/<deployment> has no nested provider, so the
    remainder falls through to the existing model name heuristics.
    litellm_proxy/azure-gpt-4 resolves to azure; an opaque
    litellm_proxy/my-deployment resolves to "".
  3. The prefix match is case insensitive, so LiteLLM_Proxy/azure/gpt-4 works.

One behavior change worth calling out for review: an opaque
litellm_proxy/my-deployment now returns "" rather than "litellm_proxy".
I grepped for consumers of that literal and there are none. "" is already
the established "provider not determinable" value in this function, so
unknown deployments now take the generic path, which is the honest answer when
the backing provider cannot be known from the string alone.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Extended the existing test_get_provider_from_model table with the nested
proxy forms, the case insensitive variant, and both bare deployment cases.

Added test_model_family_detection_through_litellm_proxy, which pins all four
model family helpers across proxied and direct strings so the two stay in
lockstep.

Added test_get_content_pdf_proxied_azure_uses_file_id as the regression test
for the reported symptom. It drives _get_content from the model string and
asserts the upload actually happens with custom_llm_provider="azure", which
is the part that was silently skipped.

$ pytest tests/unittests/models/test_litellm.py -q
376 passed in 2.77s

$ pytest tests/unittests/models/ -q
957 passed, 34 warnings in 12.12s

I checked that the new tests actually fail without the fix rather than passing
by construction. Reverting just the _strip_proxy_prefix call in
_get_provider_from_model and rerunning:

8 failed, 19 passed, 349 deselected
FAILED test_get_provider_from_model[litellm_proxy/azure/my-deployment-azure]
FAILED test_get_provider_from_model[litellm_proxy/openai/gpt-4o-openai]
FAILED test_get_provider_from_model[litellm_proxy/anthropic/claude-3-anthropic]
FAILED test_get_provider_from_model[litellm_proxy/vertex_ai/gemini-pro-vertex_ai]
FAILED test_get_provider_from_model[LiteLLM_Proxy/azure/gpt-4-azure]
FAILED test_get_provider_from_model[litellm_proxy/azure-gpt-4-azure]
FAILED test_get_provider_from_model[litellm_proxy/my-deployment-]
FAILED test_get_content_pdf_proxied_azure_uses_file_id

Formatted with pyink 25.12.0 and isort 8.0.1, matching the pinned versions
in .pre-commit-config.yaml.

Manual End-to-End (E2E) Tests:

Verified the conversion decision without network access, mocking the upload so
the emitted content block is visible. This is the assertion that matters,
since it is the payload Azure rejects:

import asyncio
from unittest import mock
from google.adk.models import lite_llm
from google.adk.models.lite_llm import (
    _content_to_message_param, _get_provider_from_model, _ensure_litellm_imported)
from google.genai import types

_ensure_litellm_imported()

pdf = types.Content(role="user", parts=[
    types.Part(text="summarize"),
    types.Part(inline_data=types.Blob(mime_type="application/pdf", data=b"%PDF-1.4 fake")),
])

async def show(model):
    provider = _get_provider_from_model(model)
    with mock.patch.object(lite_llm.litellm, "acreate_file",
                           new=mock.AsyncMock(return_value=mock.Mock(id="file-abc"))):
        msg = await _content_to_message_param(pdf, provider=provider, model=model)
    print(f"{model:36} provider={provider!r:12} -> {msg['content'][1]}")

for m in ["azure/gpt-4", "litellm_proxy/azure/my-deployment", "litellm_proxy/my-deployment"]:
    asyncio.run(show(m))

Before:

azure/gpt-4                          provider='azure'      -> {'type': 'file', 'file': {'file_id': 'file-abc', 'format': 'application/pdf'}}
litellm_proxy/azure/my-deployment    provider='litellm_proxy' -> {'type': 'file', 'file': {'file_data': 'data:application/pdf;base64,JVBERi0xLjQgZmFrZQ=='}}
litellm_proxy/my-deployment          provider='litellm_proxy' -> {'type': 'file', 'file': {'file_data': 'data:application/pdf;base64,JVBERi0xLjQgZmFrZQ=='}}

After:

azure/gpt-4                          provider='azure'      -> {'type': 'file', 'file': {'file_id': 'file-abc', 'format': 'application/pdf'}}
litellm_proxy/azure/my-deployment    provider='azure'      -> {'type': 'file', 'file': {'file_id': 'file-abc', 'format': 'application/pdf'}}
litellm_proxy/my-deployment          provider=''           -> {'type': 'file', 'file': {'file_data': 'data:application/pdf;base64,JVBERi0xLjQgZmFrZQ=='}}

The proxied Azure model now produces a payload byte for byte identical to the
direct Azure model. The opaque deployment still uses file_data, which is the
correct fallback when the backing provider is unknowable from the string.

I did not run this against a live Azure backed proxy, since I do not have a
deployment to test with. The reporter in #6538 is set up for that and could
confirm on a real endpoint.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Scope note: I limited this to reading the nested provider out of the model
string. Routing, credentials, and how LiteLLM itself resolves the proxy are
untouched.

If maintainers would rather keep the blast radius to the reported PDF bug, the
change to _get_provider_from_model alone fixes #6538 and the four model
family helpers can be split into a follow up. I kept them together because
they share one root cause, and fixing only the provider lookup leaves the same
bug reachable through the Anthropic and Gemini paths.

@google-cla

google-cla Bot commented Aug 4, 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.

@adk-bot adk-bot added the models [Component] This issue is related to model support label Aug 4, 2026
`litellm_proxy` selects the transport, not the model family, but
`_get_provider_from_model` took the first path segment and classified
`litellm_proxy/azure/<deployment>` as the `litellm_proxy` provider.

Since that value is not in `_FILE_ID_REQUIRED_PROVIDERS`, the Azure/OpenAI
file-upload path was skipped and PDFs were emitted as a bare `file_data`
block. Azure rejects the resulting `input_file` item with
`Missing required parameter: 'input[N].content[M]'` before inference.

The same first-segment assumption also broke the model-family predicates,
so proxied Anthropic models lost thinking-block formatting and proxied
Gemini models were not recognized as Vertex/Gemini routes.

Strip the routing prefix before provider and model-family detection, so a
proxied model is shaped exactly like its direct equivalent. A bare
`litellm_proxy/<deployment>` has no nested provider and still falls back to
the model-name heuristics. Non-proxied model strings are unaffected.

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

Labels

models [Component] This issue is related to model support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] LiteLlm sends invalid PDF content for Azure-backed models through litellm_proxy

3 participants