Repository navigation
feat(gemini): support gemini-decision-1 on /v1/decisions and /v1/systemone - #51220
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Merging this PR will not alter performance1 warning
Comparing Footnotes |
Codecov Reportβ Patch coverage is
π’ Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
[Medium impact] The PR appears safe to merge, with the previous QA gap addressed and no new blocking issue found. SummaryAdds Gemini Decisions support through
Reviews (7) Β· Last reviewed commit: "fix(gemini): validate caller credentials..." Β· Reviewed by Greptile |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
β Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2d082b5. Configure here.
Handwritten 1-3 sentence description
Google launched the Gemini Decisions API today, and LiteLLM users can now call
gemini/gemini-decision-1on the proxy's /v1/decisions and /v1/systemone routes with spend tracked. Before this PR the proxy answered 400 with "Unknown Decisions provider 'gemini'"Details
TLDR
Problem this solves:
gemini/is not a decisions provider, so both routes return 400How it solves it:
GeminiDecisionsConfigposts to/v1beta/decisionswithx-goog-api-keygemini/gemini-decision-1cost map row at $0.10 per 1M input, $0 output, with Google's published rate limitsUser Flow
Before: a developer with a
gemini/gemini-decision-1deployment cannot use itUnknown Decisions provider 'gemini', same for /v1/systemoneAfter: the same request returns calibrated answers and records spend
x-litellm-response-costof about $0.0000183Pre-Submission checklist
Screenshots / Proof of Fix
Client destination overrides cannot inherit deployment keys
Greptile found that the earlier credential guard covered environment keys but could still receive a key already merged from the deployment. The router now asks the provider to validate the original caller connection before merging deployment credentials. The same validation still runs for direct SDK calls
Before at
3114ba07a7, both/v1/decisionsand/v1/systemonesent a fake deployment-key canary to a local receiver when the client suppliedapi_baseorbase_urlwithoutapi_key. After at2d082b5800, all four requests return HTTP 400 requiring an explicit key, and the receiver gets zero requests. Both routes still return HTTP 200 when the caller supplies its own key, and the receiver sees only that keyThe live reproduction uses the receiver and commands in the credential section below, with one change to its
gemini-defaultdeployment, addingapi_key: shared-google-key-canaryunderlitellm_params. Each run used two serving workers and one dedicated Redis shared by those workers. Final worker PIDs were33031, 33032. A second live run at the same2d082b5800tip used the Shared vault Gemini early-access QA credential through thegemini-realmodel configured below. Both routes returned HTTP 200 with answers and 92 input tokens each. That run again rejected all four deployment-key override requests with zero outbound calls, and both explicit-key requests succeeded. Its two worker PIDs were33821, 33822. The four new regression cases fail on the previous code and pass with the fixAt
2d082b5800, all 47 focused Gemini and call tests pass, including normal administrator-configured destinations and explicit caller keys. The 377 tests intests/unit/decisions/test_main.pypass in the freshly built environment.make checkpasses in full. Commands werePYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -p pytest_asyncio.plugin -p respx.plugin tests/unit/decisions/test_call.py tests/unit/llms/gemini/decisions/test_transformation.py -q --tb=short, the same pytest invocation fortests/unit/decisions/test_main.py, andUV_PROJECT_ENVIRONMENT=$PWD/.venv make checkRequired checks pass at
2d082b5800. Greptile reports 5/5 with no open findings, Bugbot reports no new issues, and all review threads are resolvedCI repair
The original
unit passedfailure came from the metrics-server occupied-port test expecting the child-exit message, even though startup can correctly fail through its timeout instead. Both occupied-port tests now requireMetricsServerStartupErrorwithout depending on which failure wins the race. The branch also includes the upstream Rust beta-header fix from #51272 through a merge ofmain. That upstream fix left two identical arms forCompact20260904, so the duplicate is removed to pass Rust lintThe original failure is recorded in the proxy-infra-root job. Its error was
Prometheus metrics server did not answer ... within 30s, while the test requiredexited with code ...E2E exception for the CI-only changes at
3114ba07a7. That follow-up removes a timing-dependent message assertion from two existing tests, merges the upstream branch and removes an unreachable duplicate Rust match arm with identical behavior. The production metrics-server startup code is unchanged.PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPATH=$PWD python -m pytest -p pytest_asyncio.plugin tests/unit/proxy/test_prometheus_metrics_server.py -q --tb=shortpasses all 12 tests, including live local child processes and occupied sockets.ruff check tests/unit/proxy/test_prometheus_metrics_server.pyandruff format --check tests/unit/proxy/test_prometheus_metrics_server.pyboth pass.cargo clippy --manifest-path litellm-rust/Cargo.toml -p litellm-llms-types --all-targets --locked -- -D warningspasses.cargo test --manifest-path litellm-rust/Cargo.toml -p litellm-llms-types --locked providers::anthropic::beta::testspasses all 111 tests, including the original failing config-matrix test. The provider-flow proof below covers the Gemini changesCustom Gemini destinations require their own key
At
3e10a869f5, requests that setapi_baseorbase_urlmust also supplyapi_key. This prevents the sharedGEMINI_API_KEYorGOOGLE_API_KEYfrom being sent to the requested host. The default Google destination still uses environment credentials, and an administrator-configuredGEMINI_API_BASEstill works with its environment keyThe proxy already rejects client-side connection overrides by default. This reproduction enables
allow_client_side_credentialsto exercise the affected path. It uses a fake environment-key canary and a local HTTP receiver, plus separate real Gemini calls using the Shared vault's early-access QA credentialBefore at
7be92ec774, both routes sent the environment-key canary to the custom host forapi_baseandbase_url. After at3e10a869f5, all four requests return HTTP 400 withsupply an explicit api_key, and the receiver gets zero requests. On both revisions, an explicit custom key reaches only the custom receiver, and both normal Gemini routes return HTTP 200 with answers and usageEach proxy ran with two serving workers and its own Redis. After worker PIDs were
2218, 2219. Commands used--num_workers 2 --detailed_debug --reload --use_v2_migration_resolverTo reproduce, start a receiver on port 4011 that records the
x-goog-api-keyheader and returns{"answers":{"ok":{"type":"noul","noul":0.9}}}. ExportGEMINI_API_KEY=shared-google-key-canary, setQA_GEMINI_KEYto a real Gemini key, and start Redis on port 6398. Use this proxy configRun the receiver in a separate terminal
redis-server --bind 127.0.0.1 --port 6398 --save '' --appendonly noRepeat with
base_urlreplacingapi_base. Add"api_key":"custom-gateway-key"to verify the explicit-key case returns 200 and the receiver sees that key. For real Google calls, use"model":"gemini-real"and remove the destination overrideRegression verification used
uv run --no-sync pytest tests/unit/llms/gemini/decisions/test_transformation.py tests/unit/decisions/test_call.py -q. The eight rejection cases fail on the previous implementation and pass with the guard. All 39 focused tests pass. The same tests cover default environment keys, configured destinations, custom explicit keys and bounded image downloadsThe repository gate also found two existing
Finalannotations inside the image-download loops. The local image variable now uses its inferred string type, permitted by the loop-local assignment rule. The focused image-budget tests pass with these annotationsThe full
make checkpassed at3e10a869f5. The annotation-only follow-up at13f453cd3dpasses all 39 focused tests, Ruff, the type-discipline check and targeted type checking. The live proof above ran at3e10a869f5Remote image limits and ordering
Gemini requests accept at most 16 images and 20 MiB of decoded image data. These are proxy safety limits. Downloads run one at a time within the remaining byte budget, including inline images already present. Cached images are checked against the same budget and reused. Captions stay beside their images in message inputs
The fix also repairs logging for remote images, which previously tried to parse the original URL as base64 and returned HTTP 500
Live before/after proof ran against e1de5bd and the exact latest tip 7be92ec, with two serving workers and one dedicated Redis per proxy. The focused suite passes all 29 tests. The larger combined suite has an existing test-order failure for the Gemini cost-map entry on both revisions
Both routes changed from HTTP 500 to HTTP 200 for valid remote images. Seventeen images now return HTTP 400 before any image download. Three 11 MiB images return HTTP 400 during the second download, before fetching the third. A decisions request with alternating captions and two images returns HTTP 200 with 2271 input tokens
Recorded results
[ { "phase": "final-before", "results": [ { "route": "systemone", "case": "small", "status": 500, "image_fetches": 1 }, { "route": "systemone", "case": "count", "status": 500, "image_fetches": 31 }, { "route": "systemone", "case": "large", "status": 500, "image_fetches": 9 }, { "route": "decisions", "case": "small", "status": 500, "image_fetches": 1 }, { "route": "decisions", "case": "count", "status": 500, "image_fetches": 31 }, { "route": "decisions", "case": "large", "status": 500, "image_fetches": 9 }, { "route": "decisions", "case": "paired", "status": 500, "image_fetches": 2 } ] }, { "phase": "verified", "results": [ { "route": "systemone", "case": "small", "status": 200, "image_fetches": 1 }, { "route": "systemone", "case": "count", "status": 400, "image_fetches": 0 }, { "route": "systemone", "case": "large", "status": 400, "image_fetches": 2 }, { "route": "decisions", "case": "small", "status": 200, "image_fetches": 1 }, { "route": "decisions", "case": "count", "status": 400, "image_fetches": 0 }, { "route": "decisions", "case": "large", "status": 400, "image_fetches": 2 }, { "route": "decisions", "case": "paired", "status": 200, "image_fetches": 2 } ] } ]Live reproduction commands and client
Install each checkout's dependencies and native extension, plus requests and PyYAML. Export a real
GEMINI_API_KEYand save the client below asproof.py. The client starts an image server, Redis and the proxy, sends the requests, then stops those processes. The local image server requires the test config's URL-validation overrideThe proxy command used
--num_workers 2 --detailed_debug --reload --use_v2_migration_resolver. Both logs confirmed two serving worker PIDs. Credential loading below uses the environment in place of the private QA-vault lookup used locallyTwo-worker proxies sharing one Redis on port 6397, no database, Gemini key in
GEMINI_API_KEY. Upstream API docs are at https://aistudio.google.com/docs/decisions-apiconfig.yaml and commands
redis-server --bind 127.0.0.1 --port 6397 --save '' --appendonly no python litellm/proxy/proxy_cli.py --config config.yaml --num_workers 2 --detailed_debug --reload --use_v2_migration_resolver --port 4000Requests sent to both routes
Before (13089b1)
HTTP 400litellm.BadRequestError: Unknown Decisions provider 'gemini'After (6896d80)
Response bodies below show the initial text-only proof at 6896d80, trimmed and rounded for reading. The remote-image proof above covers the latest tip 7be92ec
HTTP 200 cost=1.83e-05{"answers":{"damaged":{"type":"noul","noul":0.9895},"action":{"type":"choice","choice":"replace","probabilities":{"replace":0.9987,"keep":0.0013}},"severity":{"type":"score","score":0.4332}},"usage":{"input_tokens":183,"output_tokens":0}}HTTP 200 cost=1.83e-05{"model":"gemini/gemini-decision-1","answers":[{"type":"predicate","name":"damaged","probability":0.9895},{"type":"choice","name":"action","choice":"replace","confidence":0.9987},{"type":"score","name":"severity","score":0.4332,"confidence":0.5668}],"usage":{"input_tokens":183,"output_tokens":0,"total_tokens":183}}response_cost: 1.8299999999999998e-05. SDKsystemone,decisions,asystemoneandadecisionscalls also succeed at1.81e-05eachType
π New Feature
Caveats (if any)
Medium
Final Attestation
Note
Medium Risk
Touches Decisions auth routing, credential validation, and image fetch budgeting on request paths; incorrect behavior could leak keys or mishandle multimodal payloads.
Overview
Adds
GeminiDecisionsConfigsogemini/gemini-decision-1works on/v1/decisionsand/v1/systemone, calling Googleβs/v1beta/decisionswithx-goog-api-key, mapping noul/choice/score questions plus text/image/audio/video input, and registering pricing/metadata for the new model.Decisions routing now runs
validate_client_connectionbefore credentials merge (router and direct calls): a customapi_base/base_urlwithout an explicitapi_keyreturns 400 so shared Google or deployment keys are not sent to arbitrary hosts. For Gemini, remote images are inlined under 16 images and 20 MiB total decode budget via sequentialmax_bytesdownloads; unsupported-inline logging usesir_to_systemone_requestinstead of the upstream wire body. Also drops a duplicate RustCompact20260904beta match arm and relaxes flaky Prometheus metrics-server startup assertions.Reviewed by Cursor Bugbot for commit 2d082b5. Bugbot is set up for automated code reviews on this repo. Configure here.