Skip to content

fix(gemini): prevent duplicate model path generation when api_base includes full route#24786

Open
OnHighEngineer wants to merge 1 commit into
BerriAI:mainfrom
OnHighEngineer:fix-gemini-api-base-duplicate
Open

fix(gemini): prevent duplicate model path generation when api_base includes full route#24786
OnHighEngineer wants to merge 1 commit into
BerriAI:mainfrom
OnHighEngineer:fix-gemini-api-base-duplicate

Conversation

@OnHighEngineer
Copy link
Copy Markdown

Fixes OpenHands/OpenHands#13645 by checking if api_base already contains the full proxy route to avoid appending it twice in _check_custom_proxy.

Copilot AI review requested due to automatic review settings March 30, 2026 12:33
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 30, 2026 0:35am

Request Review

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Mar 30, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing OnHighEngineer:fix-gemini-api-base-duplicate (650b782) with main (5812053)

Open in CodSpeed

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts Gemini custom api_base handling in the Vertex/Gemini URL builder to avoid generating duplicate /models/{model}:{endpoint} segments when a proxy api_base already includes part (or all) of the route (per issue #13645).

Changes:

  • Updates _check_custom_proxy to detect when api_base already ends with /models/{model}:{endpoint} and use it as-is.
  • Adds handling for api_base that ends with /models/{model} by appending :{endpoint}.
  • Ensures the fallback path construction strips trailing / before appending /models/....

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +396 to +401
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
elif api_base.endswith(f"/models/{model}"):
url = f"{api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endswith(...) checks don’t account for trailing slashes. If api_base is provided as a full route with a trailing / (e.g. .../models/{model}:{endpoint}/ or .../models/{model}/), none of the first two branches match and the else branch will append /models/... again, reintroducing the duplicate-path bug. Consider normalizing once (e.g., api_base_normalized = api_base.rstrip('/')) and using that for both suffix checks and URL construction (and optionally returning the normalized value in the first branch for consistency).

Suggested change
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
elif api_base.endswith(f"/models/{model}"):
url = f"{api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)
api_base_normalized = api_base.rstrip("/")
if api_base_normalized.endswith(f"/models/{model}:{endpoint}"):
url = api_base_normalized
elif api_base_normalized.endswith(f"/models/{model}"):
url = f"{api_base_normalized}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base_normalized, model, endpoint)

Copilot uses AI. Check for mistakes.
Comment on lines +396 to +401
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
elif api_base.endswith(f"/models/{model}"):
url = f"{api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces new behavior (accepting api_base that already includes /models/{model} or /models/{model}:{endpoint}), but the existing unit tests only cover the case where api_base stops at the version prefix. Add tests that pass api_base with the full route (and with/without trailing /) to ensure URL construction doesn’t duplicate paths and remains stable.

Suggested change
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
elif api_base.endswith(f"/models/{model}"):
url = f"{api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)
# Normalize api_base to safely handle with/without trailing "/"
normalized_api_base = api_base.rstrip("/")
if normalized_api_base.endswith(f"/models/{model}:{endpoint}"):
url = normalized_api_base
elif normalized_api_base.endswith(f"/models/{model}"):
url = f"{normalized_api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(normalized_api_base, model, endpoint)

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Greptile Summary

This PR adds a duplicate-route guard to _check_custom_proxy in VertexBase. When api_base already contains the full /models/{model}:{endpoint} path (as can happen with certain Cloudflare AI Gateway / OpenHands proxy configurations), the old code appended the path a second time, producing an invalid URL. The fix introduces two endswith checks before the unconditional append and also strips a trailing slash in the fallback branch.

Key observations:

  • The logic change is correct for the reported scenario and is a minimal, targeted fix.
  • Neither of the two new if/elif branches is exercised by any existing or newly-added test. All test cases pass an api_base that ends at the base path (e.g. .../v1beta), so every call still hits the else branch — the actual regression being fixed is never tested.
  • The endswith comparison operates on the raw api_base string. If api_base contains a query string, the match will silently fail and the duplicate-path bug can still manifest.
  • The rstrip(\"/\") addition in the else branch is a correct incidental improvement that prevents a double-slash when api_base has a trailing slash.

Confidence Score: 4/5

Safe to merge for the common case, but the two new code branches have no test coverage, leaving the claimed fix unverified.

The logic is sound and the change is minimal/localized. The else branch (existing behaviour) is well-tested and unchanged in semantics. The main concern is that neither new condition (endswith full path, endswith model-only path) has a corresponding test, meaning the specific bug being fixed is not proven by the test suite. Additionally, api_base values with query strings can bypass the new guards.

litellm/llms/vertex_ai/vertex_llm_base.py — the two new endswith branches need test coverage in tests/test_litellm/llms/vertex_ai/test_vertex_llm_base.py.

Important Files Changed

Filename Overview
litellm/llms/vertex_ai/vertex_llm_base.py Added two endswith guards in _check_custom_proxy to skip re-appending /models/{model}:{endpoint} when api_base already contains that route; also strips a trailing slash in the else branch. The logic is correct for the common case but the two new branches lack any test coverage, and an api_base containing query parameters can still bypass the guards.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_check_custom_proxy called\nwith api_base set"] --> B{custom_llm_provider\n== 'gemini'?}
    B -- No --> C{use_psc_endpoint_format?}
    C -- Yes --> D["Build PSC URL:\napi_base/v1/projects/.../endpoints/model:endpoint"]
    C -- No --> E["url = api_base:endpoint"]
    B -- Yes --> F{model is None?}
    F -- Yes --> G["raise ValueError"]
    F -- No --> H{"api_base.endswith('/models/model:endpoint')?"}
    H -- Yes --> I["url = api_base (no-op)"]
    H -- No --> J{"api_base.endswith('/models/model')?"}
    J -- Yes --> K["url = api_base + ':' + endpoint"]
    J -- No --> L["url = api_base.rstrip('/') + '/models/model:endpoint'"]
    I --> M{gemini_api_key is None?}
    K --> M
    L --> M
    M -- Yes --> N["raise ValueError"]
    M -- No --> O["auth_header = x-goog-api-key header"]
    O --> P{stream is True?}
    D --> P
    E --> P
    P -- Yes --> Q["url += '?alt=sse'"]
    P -- No --> R["return auth_header, url"]
    Q --> R
Loading

Reviews (1): Last reviewed commit: "fix(gemini): prevent duplicate model pat..." | Re-trigger Greptile

Comment on lines +396 to +401
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
elif api_base.endswith(f"/models/{model}"):
url = f"{api_base}:{endpoint}"
else:
url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No tests for the two new branch conditions

The PR introduces two new if/elif branches (lines 396-399) to detect when api_base already contains the full model route, but neither branch is exercised by any test. All existing and newly-added test cases pass an api_base such as "https://proxy.example.com/generativelanguage.googleapis.com/v1beta" (which does not end in /models/{model}:{endpoint} or /models/{model}), so every call still falls through to the else clause.

The scenario the PR is meant to fix — where api_base is already a fully-qualified URL like "https://proxy.example.com/v1beta/models/gemini-pro:generateContent" — has no regression test. Without a test for this path:

  • Confirmation that the fix actually prevents the duplicate suffix is only manual.
  • A future refactor could silently re-introduce the bug.

Per the project's requirement, a PR claiming to resolve an issue should include a passing test that demonstrates the resolution. Please add at least one test case covering each new branch, e.g.:

# api_base already contains full model:endpoint path – should be returned as-is
(
    "https://proxy.example.com/v1beta/models/gemini-pro:generateContent",
    "gemini",
    "test-api-key",
    "generateContent",
    False,
    None,
    "...",  # url arg (unused in this path)
    "gemini-pro",
    {"x-goog-api-key": "test-api-key"},
    "https://proxy.example.com/v1beta/models/gemini-pro:generateContent",
),
# api_base contains model but no endpoint – endpoint should be appended once
(
    "https://proxy.example.com/v1beta/models/gemini-pro",
    "gemini",
    "test-api-key",
    "generateContent",
    False,
    None,
    "...",
    "gemini-pro",
    {"x-goog-api-key": "test-api-key"},
    "https://proxy.example.com/v1beta/models/gemini-pro:generateContent",
),

Rule Used: What: Ensure that any PR claiming to fix an issue ... (source)

Comment on lines +396 to +397
if api_base.endswith(f"/models/{model}:{endpoint}"):
url = api_base
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 endswith check silently misses api_base values with query parameters

If api_base contains any query string (e.g. a trailing ?alt=sse from a previous streaming build), the endswith(f"/models/{model}:{endpoint}") check returns False. The code then falls through to the else branch and appends /models/{model}:{endpoint} a second time, reproducing the duplicate-path bug this PR is meant to fix.

A more robust approach is to compare only the URL path component:

from urllib.parse import urlparse

parsed = urlparse(api_base)
path = parsed.path.rstrip("/")
if path.endswith(f"/models/{model}:{endpoint}"):
    url = api_base
elif path.endswith(f"/models/{model}"):
    url = f"{api_base.rstrip('/')}:{endpoint}"
else:
    url = "{}/models/{}:{}".format(api_base.rstrip("/"), model, endpoint)

This is a low-probability edge case for most callers, but worth guarding against given how varied proxy configurations can be.

@StatPan
Copy link
Copy Markdown

StatPan commented May 1, 2026

Hi — this exact bug is still affecting downstream integrations as of 2026-05-01 (repro from OpenHands: OpenHands/OpenHands#14028, OpenHands/OpenHands#14029).

Since this PR has been inactive since Mar 30, I opened a tracking issue to re-surface the bug and connect the downstream impact: #26979.

I also prepared a fresh branch on my fork with the same fix direction plus regression tests covering the full-route / model-prefix api_base cases that were previously untested.

If this PR is no longer being advanced, I can open a replacement PR and carry it forward so this does not stay stuck upstream. Happy to do that if preferred.

Tracking issue: #26979

@StatPan
Copy link
Copy Markdown

StatPan commented May 1, 2026

@OnHighEngineer pinging you directly as well — I wanted to check in before taking this over.

There has not been follow-up on this PR since Mar 30, and the bug is still actively affecting downstream OpenHands integrations. I need to get this fixed upstream quickly, so unless you are already planning to continue this PR, I'm planning to open a follow-up/replacement PR and carry it through.

If you'd prefer to keep driving this one yourself, happy to defer — otherwise I'll proceed so the issue does not stay unresolved.

Context: #26979, OpenHands/OpenHands#14028, OpenHands/OpenHands#14029

@StatPan
Copy link
Copy Markdown

StatPan commented May 1, 2026

Quick follow-up: I've proceeded with a replacement PR so this can move forward without waiting further.

Replacement PR: #26980
Tracking issue: #26979

@OnHighEngineer thanks for the original fix direction here — I carried the same upstream bugfix forward and added regression coverage for the previously untested full-route / model-prefix api_base cases.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: litellm.NotFoundError: GeminiException

4 participants