feat: pass through optional instruction field in the rerank API (vLLM/Qwen3-Reranker)#30757
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
4085e45 to
b096624
Compare
Greptile SummaryThis PR adds an opt-in
Confidence Score: 5/5The change is safe to merge; it adds a single optional field with a clean None-exclusion guard and no impact on existing request shapes. The core implementation is correct and well-tested. The two flagged items are both pre-existing patterns (rerank fields have never been inspected by guardrails; DashScope silently drops unsupported params consistently) rather than regressions introduced here. Neither rises to a blocking concern for this change. litellm/llms/dashscope/rerank/transformation.py —
|
| Filename | Overview |
|---|---|
| litellm/types/rerank.py | Adds instruction: Optional[str] = None to RerankRequest and OptionalRerankParams; fully backward-compatible via exclude_none. |
| litellm/rerank_api/rerank_utils.py | Threads instruction as a named param through get_optional_rerank_params and surfaces it in all_non_default_params to preserve the DeepInfra path. |
| litellm/rerank_api/main.py | Adds instruction to both rerank() and arerank() signatures and forwards it to get_optional_rerank_params. |
| litellm/llms/hosted_vllm/rerank/transformation.py | Adds "instruction" to the supported params list and forwards the field conditionally (only when non-None) into the mapped params and request body. |
| litellm/llms/base_llm/rerank/transformation.py | Adds instruction: Optional[str] = None to the abstract map_cohere_rerank_params signature, ensuring all provider overrides stay in sync. |
| litellm/llms/dashscope/rerank/transformation.py | Accepts instruction in the signature but does not forward it to the output params or add it to get_supported_cohere_rerank_params, causing silent no-ops for callers using DashScope's qwen3-rerank with an instruction. |
| litellm/llms/deepinfra/rerank/transformation.py | Accepts instruction in the signature; existing non_default_params path already forwards it correctly, so no functional change. |
| tests/test_litellm/llms/hosted_vllm/test_hosted_vllm_rerank_transformation.py | Adds 6 new unit tests covering instruction present/absent for map_cohere_rerank_params, transform_rerank_request, and get_optional_rerank_params; all are mock-only, no network calls. |
Reviews (3): Last reviewed commit: "Address review: thread `instruction` as ..." | Re-trigger Greptile
|
Thanks for the review. Addressed both points in a641009: Greptile P2 — Codecov — uncovered line in All rerank provider test suites pass locally (189 passed) and Disclosure: this change was drafted with AI assistance (Anthropic's Claude Code, Opus 4.8) and reviewed/modified by a human prior to posting. |
|
Additional validation: backported this change as a runtime patch to a live v1.89.1 proxy (
So the passthrough is confirmed working against a real Qwen3-Reranker deployment, not just unit tests. |
|
Validation after applying the patch
Outcome Instruction-aware reranking is now live in TeraContext.AI production on all application instances, routed through the LiteLLM rerank-pool (regaining cross-server load-balancing) while the per-request instruction reaches the templated reranker. Backwards-compatible: requests with no instruction are byte-identical to before. |
|
Thanks for the detailed validation work, @jhsmith409! The end-to-end proof against a real Qwen3-Reranker deployment is excellent. Triggering a fresh Greptile review on the latest commit since the current review covers an earlier SHA. |
|
Thanks for looking at this. I did not see any additional Greptile errors to address. Anything else I need to do? Jim |
|
@jhsmith409 Can you rebase with litellm_internal_staging branch? Thanks |
vLLM's /v1/rerank and /v1/score accept an optional top-level `instruction` field (folded into the model's chat_template_kwargs and consumed by the chat template — e.g. Qwen3-Reranker). LiteLLM's managed rerank route silently dropped it: RerankRequest / OptionalRerankParams had no such field, so the outgoing body was rebuilt without it. Thread an opt-in `instruction: Optional[str]` through rerank()/arerank(), get_optional_rerank_params, and the hosted_vllm transformation into the request body, only when non-None. When callers omit it, model_dump(exclude_none) drops the field and the outgoing request is byte-for-byte unchanged — fully backward-compatible. (DeepInfra already forwards `instruction` via non_default_params; this formalizes the field in the shared types.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…utils Per PR review (greptile P2 + codecov): - Make `instruction` a typed, named argument on the rerank provider interface instead of recovering it from the opaque `non_default_params` blob. Adds `instruction: Optional[str] = None` to `BaseRerankConfig.map_cohere_rerank_params` and every provider override, and forwards it explicitly from `get_optional_rerank_params`. hosted_vllm now reads the named param directly. It is still also surfaced in `non_default_params` so providers that read it there (e.g. DeepInfra) keep working now that `rerank()` consumes `instruction` as a named param rather than leaving it in **kwargs. - Add get_optional_rerank_params unit tests (present + absent) to cover the previously-uncovered threading line flagged by codecov. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
a641009 to
8f2108d
Compare
instruction field in the rerank API (vLLM/Qwen3-Reranker)instruction field in the rerank API (vLLM/Qwen3-Reranker)
|
@Sameerlite , Again, thanks for looking at this. I have rebased it to litellm_internal_staging , initially it had an issue with the title, Claude helped me figure out why and it is corrected. It's in queue for all the automated checks then looking for a reviewer when those complete. Please let me know if I'm missing anything in the process. Jim |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
|
Waiting for all the automated checks to finish, will then address them all at once. |
|
Thanks for the contribution! Triggering a fresh Greptile review after the rebase. There's also an unresolved Veria AI security thread: "Rerank guardrails miss instruction" — meaning guardrails may not apply to the new |
The rerank guardrail translation (CohereRerankHandler.process_input_messages) only scanned `query`, so the newly added `instruction` field reached the backend model unscanned. Since instruction-aware rerankers (hosted vLLM / Qwen3-Reranker) fold `instruction` into the prompt, an authenticated caller could place content there to bypass configured rerank request guardrails. Generalize the handler to scan every user-controlled text field (`query` and `instruction`) in one apply_guardrail call and write each sanitized value back by index. Query-only requests are unchanged (single-element list at index 0); non-string fields are left untouched. Adds tests covering instruction scanning, PII masking write-back, and the non-string case. Addresses the Veria AI security review on PR BerriAI#30757.
|
@Sameerlite Thanks for shepherding this through - the rebase trigger, the fresh Greptile pass, and flagging the Veria AI thread are all much appreciated. Addressed the Veria AI security finding ("Rerank guardrails miss instruction") in 8f90e9e.
Locally: the rerank guardrail + hosted_vllm rerank suites pass and ruff is clean on the changed files. The push has re-triggered the full check suite. Let me know if anything else is needed. |
…dget The lint gate (basedpyright delta-vs-base budget) flagged one new reportArgumentType: len(result.results) where results is List[RerankResponseResult] | None. Assert results is not None first to narrow the type before len()/indexing.
…dget
The basedpyright delta-vs-base gate flagged one new reportArgumentType: the
Router forwards rerank calls via an untyped `**kwargs` unpack
(`litellm.arerank(**{**data, **kwargs})`), and declaring `instruction` as a
typed named param on the public `rerank`/`arerank` entrypoints made pyright
check that key against `str | None`, adding an error at router.py with no real
safety gain. Read `instruction` from kwargs in `rerank` instead.
It remains fully typed where it matters - threaded as a typed argument through
`get_optional_rerank_params` and each provider's `map_cohere_rerank_params`
(the original Greptile P2 ask). Whole-repo reportArgumentType is back to the
base count (net 0); rerank hosted_vllm + cohere guardrail suites pass; ruff clean.
|
@Sameerlite This is ready for another look. All 75 checks are green, and both review threads are resolved:
|
* fix(ui): widen Y-axis gutter on Usage charts so large token/request labels aren't clipped
The Total Tokens Over Time and Total Requests Over Time AreaCharts on the
Usage page used Tremor's default yAxisWidth (~56 px), which is too narrow
once totals pass the hundred-million mark — leading digits of labels like
"100.00M" / "4500.00M" got clipped against the chart edge. The requests
chart was worse: it formatted with toLocaleString(), so billion-scale
request counts produced "1,000,000,000" (13 chars) and overflowed
immediately.
Fix in two places so neither alone has to carry the whole margin:
- activity_metrics.tsx: add yAxisWidth={80} to both AreaCharts, and
switch the requests chart to the shared valueFormatter so it uses the
same compact k/M/B suffixes as the tokens chart.
- value_formatters.tsx: add a >= 1e9 branch to valueFormatter /
valueFormatterSpend that emits a "B" suffix (4.50B, $4.50B), keeping
every formatted label at most 7 chars.
Co-Authored-By: Claude Opus 4 (1M context) <[email protected]>
* Update ui/litellm-dashboard/src/components/UsagePage/utils/value_formatters.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* docs(readme): add Deploy on AWS/GCP with Terraform section
Adds a quickstart for the two published Terraform modules on the public
registry (BerriAI/litellm/aws and BerriAI/litellm/google). Copy-paste
main.tf for each cloud, the one-time GCP Artifact Registry remote-repo
command, and pointers to the registry pages for the full input surface.
Sits inside the Get Started section, between the gateway/SDK table and
Run in Developer Mode -- where someone scanning the README for "how do I
deploy this" will land.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
* docs(readme): add 1-click deploy buttons for AWS + GCP
GCP gets the real 1-click: Open in Cloud Shell badge that clones the repo
and walks through `terraform apply` via the existing DeployStack
tutorial (already shipped at terraform/litellm/gcp/examples/default/
TUTORIAL.md). User just picks a project.
AWS gets a soft 1-click: a Launch in AWS CloudShell badge that opens an
in-browser, already-authenticated shell. User runs four commands
(clone + cd + cp tfvars + terraform apply) once inside. There's no
native AWS deeplink that pre-clones a repo + runs a tutorial -- CFN
"Launch Stack" + CodeBuild would be needed for that, and that's a
separate piece of work.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
* docs(readme): move AWS + GCP deploy buttons next to Render button
* docs(readme): unify deploy button sizes and badge styles
* docs(readme): bump deploy button height to 48 to match Render/Railway
* docs(readme): bump AWS/GCP badge height to compensate for SVG padding
* docs(readme): bump AWS/GCP badge height to 72
* docs(readme): bump AWS/GCP badge height to 84
* fix(readme): make deploy buttons same height (48px)
https://claude.ai/code/session_01MxQRMHSDXbqJh74rF86UBc
* docs(readme): flag GCP project ID substitution in image_registry
* docs(readme): equalize deploy button heights and fix Cloud Shell button font
GitHub rewrites an image's height attribute to "height: auto; max-height: Npx", which only caps and never stretches, so each image renders at its intrinsic height. The AWS/GCP shields badges are intrinsically 28px while the Render/Railway buttons are 40px, leaving the row uneven regardless of the height="48" we set. Replace the two shields badges with committed 40px PNGs so all four header buttons render at the same 40px.
Also swap the Cloud Shell button from open-btn.svg to open-btn.png. The SVG renders its label as live text with font-family "Roboto, Sans" and no generic fallback; since neither font exists in GitHub's render environment, the text fell back to a serif (Times New Roman). The PNG bakes in the correct typeface.
* docs(readme): collapse Railway deploy anchor to a single line
The Railway button wrapped its img across indented lines, so the anchor contained leading and trailing whitespace. GitHub underlines link content, rendering that whitespace as a small blue underline beside the button. Put the anchor on one line like the other three buttons so there is no inner whitespace to underline.
* Add Claude Fable 5 cost map entries as a data-only hotfix
Backports only the model map changes from #30064 so deployments on
released litellm versions pick up Fable 5 pricing, context window, and
the adaptive thinking flag through the hosted cost map fetch without
upgrading. Includes the supports_sampling_params flag on the 28
Fable 5 / Opus 4.7 / Opus 4.8 entries (ignored by released code, read
by the gating that ships with the next release) and the matching
one-line schema declaration so the map validation test passes.
https://claude.ai/code/session_01MZarYYT3aS7DxaNjoax6Gm
* fix: correct context window tokens for GPT-5 Pro and GPT-5.4 Mini/Nano
Three bugs in model_prices_and_context_window.json:
1. gpt-5-pro and gpt-5-pro-2025-10-06: max_input_tokens and max_tokens
were SWAPPED. GPT-5 Pro has a 400K context window (input) with 128K
max output, but the values were set as max_input=128000,
max_tokens=272000. This caused token limit errors when sending
prompts over 128K tokens to GPT-5 Pro.
2. gpt-5.4-mini and gpt-5.4-mini-2026-03-17: max_input_tokens was
272000, but GPT-5.4 Mini shares the same 1,050,000 token context
window as GPT-5.4. This was inconsistent with the azure/ variants
which already correctly had 1,050,000.
3. gpt-5.4-nano and gpt-5.4-nano-2026-03-17: same issue as Mini,
max_input_tokens was 272000 instead of 1,050,000.
Source: OpenAI model documentation and contextwindows.dev which
aggregates official context window sizes.
Fixes #30928 (partially — the issue incorrectly claims gpt-5/gpt-5-mini
should be 400K; their 272K values are correct per OpenAI docs)
* fix: also correct max_output_tokens for gpt-5-pro (272000→128000)
Per reviewer feedback, max_output_tokens was left at 272000 while
max_tokens was corrected to 128000, causing an internal inconsistency.
Both should be 128000 per OpenAI docs.
* fix(cost): price gpt-image generated output tokens as image tokens (#31147)
The OpenAI Images endpoints (/v1/images/generations, /v1/images/edits) return
usage with no output token breakdown — litellm's `ImageUsage` has no
`output_tokens_details` field — so generated-image OUTPUT tokens were priced at
the text rate (`output_cost_per_token`) instead of the image rate
(`output_cost_per_image_token`). For gpt-image-2 that is $10/1M vs $30/1M, a ~3x
undercount on the dominant cost component (image output is ~74% of spend). This
also affects azure gpt-image, which shares this calculator.
The OpenAI gpt-image cost calculator re-implemented usage handling instead of
reusing `calculate_image_response_cost_from_usage`, the shared helper that
azure_ai/gemini/vertex_ai already use. That helper classifies generated output
tokens as image tokens when the provider does not itemize output, and splits
text/image when it does.
Fix: route the ImageUsage path through `calculate_image_response_cost_from_usage`
(pre-transformed chat Usage objects are still costed directly). Adds a regression
test for the no-breakdown ImageUsage case (gpt-image-2).
* fix(bedrock): route application-inference-profile ARNs to converse (#18258) (#31098)
A bare application-inference-profile ARN passed as bedrock/arn:... fell
through to the invoke route, which cannot derive a provider from the
opaque profile id and raised 'Unknown provider=None'. The converse route
needs no provider, so detect these ARNs in get_bedrock_route and route
them to converse, matching the behavior of the already-documented
bedrock/converse/arn:... workaround.
Explicit invoke/ prefixes still win, and they remain a dead end for these
ARNs by design (no provider derivable). System-defined inference-profile
ARNs that embed a known model, and other opaque ARN types
(provisioned-model, imported-model, custom-model-deployment) that are
frequently invoke-only, are deliberately left on their current routes;
tests guard both boundaries.
* fix(moonshot): stop mutating caller messages on tool_choice='required' (#31060)
_add_tool_choice_required_message appended the "select a tool" prompt to
the caller's messages list in place, so transform_request corrupted the
caller's conversation history and appended a duplicate prompt on every
retry. Build and return a new list instead so the call stays idempotent.
Adds a regression test asserting the input messages list is unchanged
across repeated transform_request calls.
Co-authored-by: Wassbdr <[email protected]>
* fix(transcription): accept fractional usage.seconds in diarized_json responses (#30996)
gpt-4o-transcribe and compatible ASR backends return a diarized_json
response with usage={"type": "duration", "seconds": <float>}, e.g. 295.8.
TranscriptionUsageDurationObject typed seconds as int, so parsing the
response raised a pydantic ValidationError (int_from_float). That error
surfaces as an APIConnectionError which the router treats as retryable, so
it keeps re-calling the upstream (200 every time) until the upstream
rate-limits and returns 429 to the caller.
OpenAI specs this field as a float (see openai SDK UsageDuration.seconds),
so widen seconds to float. With the parse succeeding there is no exception
left to retry, which removes the loop.
Co-authored-by: Neimar Avila <[email protected]>
* fix(deepseek): drop non-function tools before chat completions call (#30910)
* fix(deepseek): drop non-function tools before chat completions call
DeepSeek's /chat/completions only accepts tools of type "function".
Requests bridged from /v1/responses can carry responses-API-native tool
types, for example a Codex CLI tool typed "namespace", which DeepSeek
rejects with "unknown variant 'namespace', expected 'function'" so the
whole request fails (issue #30722).
Filter unsupported tool types in the DeepSeek request transform so the
function tools still go through; when nothing callable remains, also drop
the now-dangling tool_choice and parallel_tool_calls
Fixes #30722
* test(deepseek): cover async tool filtering and document tool_choice assumption
Add an async_transform_request regression test so the sync and async tool
filtering paths cannot silently diverge, and document in _drop_unsupported_tools
that only non-function tools are dropped, so a function-named tool_choice always
references a surviving tool
* feat(catalog): add zai/glm-5.1, zai/glm-4.7-flash, openrouter/z-ai/glm-5.1 (#29840)
* feat(ui): surface team budget on key overview when key has no own budget (#30801)
* feat(ui): surface team budget on key overview when key has no own budget
* fix(ui): replace IIFE with derived variable and use find() for team budget display
* fix(anthropic): emit replayable streaming thinking blocks (#31022)
* feat(proxy): read cold-storage prompts back in the logs detail view (#30364)
* feat(proxy): read cold-storage prompts back in the logs detail view
When a deployment offloads prompts and responses to cold storage instead of
Postgres, the spend-log row holds only "{}" placeholders plus a
metadata.cold_storage_object_key pointer, so the UI logs detail drawer showed
nothing. The detail endpoint only read the placeholder columns and never
fetched the object back.
Resolve the payload per row based on actual content, not a config flag: if
Postgres has content, return it; otherwise read the exact stored object key and
fetch from the configured cold storage backend through ColdStorageHandler.
Reading the persisted key is a single GET. The key embeds a microsecond
timestamp that cannot be reconstructed from the millisecond-precision startTime
column, and listing the day's prefix to match on request_id would be too
expensive for this per-open path.
Also teach the detail drawer's pretty-view parser to accept a bare messages
array. The cold storage payload carries the prompt as a top-level messages list
with no proxy_server_request, so without this the output rendered while the
input stayed blank.
ColdStorageHandler gains an optional injected logger so the resolver can be unit
tested without monkeypatching. Postgres-stored prompts are unaffected: the fast
path returns the existing columns and the request-body object still renders the
same way.
* Update litellm/proxy/spend_tracking/spend_management_endpoints.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* test(proxy): cover ColdStorageHandler resolution paths and cold-storage fetch failure
Add unit tests for ColdStorageHandler (injected logger, graceful None when no
logger is configured, and resolution of a configured logger from the callback
registry) and a regression test asserting a cold storage backend exception
degrades to the Postgres values instead of surfacing a 500.
---------
Co-authored-by: Bytechoreographer <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix(mavvrik): advance metricsMarker after upload; fix scheduler startup (#31068)
* fix(mavvrik): advance metricsMarker after upload + fix scheduler startup
Two bugs fixed:
1. deliver() never called PATCH /metrics/agent/ai/{connectionId} after a
successful GCS upload, so metricsMarker stayed at 0 and every daily run
re-exported the same dates in an infinite catch-up loop.
Fix: add _update_metrics_marker(date_epoch) called at the end of deliver()
after _upload_to_gcs() succeeds. A 4xx warns but does not raise (the GCS
file is already committed). A 410 raises consistent with the rest of the
destination.
2. init_mavvrik_focus_background_job runs at proxy startup before any LLM call
has triggered lazy instantiation of MavvrikFocusLogger, so it found no
logger instance and silently skipped registering the daily export job.
Fix: if no instance is found but "mavvrik" is in litellm.callbacks, call
_init_custom_logger_compatible_class to force instantiation before
the APScheduler job is registered.
* fix(mavvrik): catch up from earliest window when metricsMarker=0
When the connector is freshly registered, metricsMarker=0 parses to None.
The catch-up block was guarded by `if last_ingested and ...` which skipped
it entirely for None, so only yesterday was exported instead of the full
_MAX_CATCHUP_DAYS window.
Fix: treat None as being _MAX_CATCHUP_DAYS behind (start from earliest_catchup).
The existing > 7 day warning only fires for non-None markers that are old.
* fix(mavvrik): use now as end_time for yesterday's export window
LiteLLM_DailyUserSpend rows for a given date get their updated_at
bumped by the spend flush job throughout the next morning. The core
database query filters on updated_at, so capping end_time at midnight
(yesterday + 1 day) missed any spend rows flushed after midnight.
Fix: pass now (cron fire time) as end_time for the daily "yesterday"
window so all fully-settled rows are captured regardless of when the
flush job ran.
Verified: claude-3-5-sonnet BilledCost went from 0.0 to ~$2.40 per
row in the exported FOCUS CSV.
* fix(mavvrik): also use now as end_time for catch-up windows
* fix(mavvrik_focus): pass required args to _init_custom_logger_compatible_class
Calling it with only logging_integration raised TypeError at proxy startup
because internal_usage_cache and llm_router have no defaults. Also fix test
name to reflect the actual status code (5xx not 4xx) used in the mock.
* ci: retrigger CI run
* feat: pass through optional `instruction` field in the rerank API (vLLM/Qwen3-Reranker) (#30757)
* Add optional `instruction` passthrough to the rerank API
vLLM's /v1/rerank and /v1/score accept an optional top-level `instruction`
field (folded into the model's chat_template_kwargs and consumed by the
chat template — e.g. Qwen3-Reranker). LiteLLM's managed rerank route silently
dropped it: RerankRequest / OptionalRerankParams had no such field, so the
outgoing body was rebuilt without it.
Thread an opt-in `instruction: Optional[str]` through rerank()/arerank(),
get_optional_rerank_params, and the hosted_vllm transformation into the
request body, only when non-None. When callers omit it, model_dump(exclude_none)
drops the field and the outgoing request is byte-for-byte unchanged — fully
backward-compatible. (DeepInfra already forwards `instruction` via
non_default_params; this formalizes the field in the shared types.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* Address review: thread `instruction` as a typed param + cover rerank_utils
Per PR review (greptile P2 + codecov):
- Make `instruction` a typed, named argument on the rerank provider interface
instead of recovering it from the opaque `non_default_params` blob. Adds
`instruction: Optional[str] = None` to `BaseRerankConfig.map_cohere_rerank_params`
and every provider override, and forwards it explicitly from
`get_optional_rerank_params`. hosted_vllm now reads the named param directly.
It is still also surfaced in `non_default_params` so providers that read it
there (e.g. DeepInfra) keep working now that `rerank()` consumes `instruction`
as a named param rather than leaving it in **kwargs.
- Add get_optional_rerank_params unit tests (present + absent) to cover the
previously-uncovered threading line flagged by codecov.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* fix: scan rerank `instruction` through request guardrails
The rerank guardrail translation (CohereRerankHandler.process_input_messages)
only scanned `query`, so the newly added `instruction` field reached the
backend model unscanned. Since instruction-aware rerankers (hosted vLLM /
Qwen3-Reranker) fold `instruction` into the prompt, an authenticated caller
could place content there to bypass configured rerank request guardrails.
Generalize the handler to scan every user-controlled text field (`query` and
`instruction`) in one apply_guardrail call and write each sanitized value back
by index. Query-only requests are unchanged (single-element list at index 0);
non-string fields are left untouched. Adds tests covering instruction
scanning, PII masking write-back, and the non-string case.
Addresses the Veria AI security review on PR #30757.
* test: narrow Optional results before len() to satisfy basedpyright budget
The lint gate (basedpyright delta-vs-base budget) flagged one new
reportArgumentType: len(result.results) where results is
List[RerankResponseResult] | None. Assert results is not None first to
narrow the type before len()/indexing.
* fix: read rerank `instruction` from kwargs to satisfy basedpyright budget
The basedpyright delta-vs-base gate flagged one new reportArgumentType: the
Router forwards rerank calls via an untyped `**kwargs` unpack
(`litellm.arerank(**{**data, **kwargs})`), and declaring `instruction` as a
typed named param on the public `rerank`/`arerank` entrypoints made pyright
check that key against `str | None`, adding an error at router.py with no real
safety gain. Read `instruction` from kwargs in `rerank` instead.
It remains fully typed where it matters - threaded as a typed argument through
`get_optional_rerank_params` and each provider's `map_cohere_rerank_params`
(the original Greptile P2 ask). Whole-repo reportArgumentType is back to the
base count (net 0); rerank hosted_vllm + cohere guardrail suites pass; ruff clean.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* fix(github_copilot): synthesize empty choices at the provider seam (#30929)
Newer Copilot Claude models (opus-4.7, opus-4.8) return responses with
choices=[], either carrying Anthropic-native content blocks or, for the
max_tokens=1 probe Claude Code sends, no content at all. github_copilot
is dispatched through the OpenAI SDK handler, which calls
convert_to_model_response_object directly and never invokes
GithubCopilotConfig.transform_response, so the empty-choices guard there
surfaced as a 500
Instead of synthesizing choices inside the shared
convert_to_model_response_object (which would silently turn empty choices
into a fabricated success for every provider), add a no-op
transform_parsed_response_dict hook on BaseConfig. GithubCopilotConfig
overrides it to synthesize choices from Anthropic-native content, reusing
its existing parsing, and the OpenAI SDK handler routes its parsed
response through the hook before generic conversion. The core utility
keeps treating empty choices as an error for all other providers
Fixes: #30927
Signed-off-by: David J. M. Karlsen <[email protected]>
* fix(router): stop fallback lookups from mutating the router fallbacks config (#30624)
* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens (#29693)
* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens
* test: scope local cost map env var with monkeypatch to avoid test pollution
* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold (#30764)
* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold
_mask_value did partial reveal by showing the first visible_prefix and last
visible_suffix characters, but for a value whose length was at or below
visible_prefix + visible_suffix (8 by default) it returned the value verbatim.
A value of exactly 8 chars fell through the length guard and computed
masked_length == 0, reconstructing the original string with no mask characters;
anything shorter hit the early return. Either way short credentials were emitted
in plaintext.
mask_dict routes real secrets through this path, so an 8-char-or-shorter redis
password, api key, or token could be written to logs and the UI unmasked. The
sibling helper mask_sensitive_keys already guards this case; _mask_value now does
the same by fully masking any value at or below the threshold.
* fix(sensitive_data_masker): add mask_short_values opt-out for truncation callers
Fully masking short values is the right default for secret masking, but
CooldownCache reuses the masker purely to truncate exception messages to the
first 50 characters, and it relies on short messages being returned readable.
Masking those blanked out short exception text and broke its tests.
Add a mask_short_values flag (default True, secure) and have CooldownCache pass
False so it keeps the truncation behavior, while every secret-masking caller
still gets short values fully masked.
* fix(mcp_debug): opt out of short-value masking to keep diagnostic token preview
MCPDebug uses the masker to preview auth tokens in debug headers and documents
that values of 10 chars or fewer are shown unchanged so token types stay
distinguishable. Pass mask_short_values=False so that diagnostic behavior is
preserved while secret maskers keep masking short values.
* fix(mcp_debug): mask short auth values in debug headers instead of echoing them
Earlier this masker opted out of short-value masking to keep a token preview, but
that echoes short authorization and token values verbatim in debug response
headers, which is the same leak this change is meant to close. Auth material
should never be emitted in full, so mask short values here too; the first/last
character preview still applies to longer tokens. Only CooldownCache keeps the
opt-out, since it truncates exception text rather than masking secrets.
* test(mcp_debug): assert masked short value preserves length
* refactor(fireworks_ai): remove deprecated audio transcriptions endpoint (#30917)
Fireworks AI deprecated audio inference on 2026-06-10
(https://docs.fireworks.ai/updates/changelog#audio-inference-and-image-generation-deprecation).
Live API testing confirms the endpoint is already non-functional: a valid
Fireworks API key receives HTTP 401 "Unauthorized" from
api.fireworks.ai/inference/v1/audio/transcriptions for every request,
regardless of payload. The audio-prod.api.fireworks.ai host referenced in
the test suite returns 401 for every path; the entire host is decommissioned.
Remove the dead FireworksAIAudioTranscriptionConfig class and every
reference to it across the codebase:
- Delete litellm/llms/fireworks_ai/audio_transcription/ directory (17-line
config class that inherited from OpenAIWhisperAudioTranscriptionConfig)
- Remove the Fireworks branch from
ProviderConfigManager.get_provider_audio_transcription_config() in
litellm/utils.py; update the stale comment in
get_optional_params_transcription that referenced fireworks ai
- Remove the FireworksAIAudioTranscriptionConfig entries from
LLM_CONFIG_NAMES and _LLM_CONFIGS_IMPORT_MAP in
litellm/_lazy_imports_registry.py
- Remove the TYPE_CHECKING re-export in litellm/__init__.py
- Remove the transcription branch in the fireworks_ai case of
get_supported_openai_params() in
litellm/litellm_core_utils/get_supported_openai_params.py
- Remove the whisper-v3 and whisper-v3-turbo entries from
model_prices_and_context_window.json and
litellm/model_prices_and_context_window_backup.json (both had
mode: audio_transcription and zero-cost pricing)
- Remove the TestFireworksAIAudioTranscription test class and its
imports from tests/llm_translation/test_fireworks_ai_translation.py
No other provider is affected. The openai_compatible_providers list,
FireworksAIMixin, and the OpenAI Whisper transcription handler all stay
because they are shared with other Fireworks endpoints and other
providers. The provider_endpoints_support.json registry already had
audio_transcriptions set to false for fireworks_ai.
* feat: add darkbloom provider (#30876)
* feat: add darkbloom provider
* fix: document darkbloom provider endpoints
* fix: address darkbloom review feedback
* fix: update darkbloom tool metadata
* fix: fail fast for non-Postgres database URLs (#30883)
* fix(proxy): fail fast on non-PostgreSQL DATABASE_URL instead of hanging on startup
LiteLLM's Prisma datasource is pinned to provider = 'postgresql', so a sqlite:// or mysql:// DATABASE_URL can never connect.
Today that surfaces as an opaque startup stall where the port never binds, and a separate 'DB not connected' 500 on /key/generate when no DATABASE_URL is set at all leaves operators guessing what to configure.
Validate the DATABASE_URL / DIRECT_URL scheme in run_server before any Prisma call and exit with an actionable message naming the unsupported scheme.
Also reword CommonProxyErrors.db_not_connected_error to tell the operator to set DATABASE_URL to a postgresql:// connection string.
Add regression tests covering postgres acceptance and sqlite/mysql/mssql rejection.
* fix: resolve CI failures and proxy DB URL typing issue
* fix(proxy): fail fast on non-PostgreSQL DATABASE_URLs with clear startup errors instead of hanging
* Validate DIRECT_URL alongside DATABASE_URL startup guards
* fix(bedrock): surface modeled HTTP status for mid-stream error events so 5xx is retryable (#24608) (#30946)
* fix(bedrock): surface modeled HTTP status for mid-stream error events (#24608)
* test(bedrock): mid-stream server errors trigger streaming fallback (#24608)
* style(bedrock): black-format stream-error helper (#24608)
* fix(mcp): re-land native tool preservation with typed annotations (#30645)
* fix(mcp): preserve native tools in semantic filter hook with typed annotations
* fix(mcp): tighten _is_mcp_tool Chat Completions shape check
* fix(sambanova): return embeddings supported params instead of dropping them (#30937)
* fix(router): send fallback metadata when streaming (#30914)
When a streaming request triggers a fallback, there was previously no way to
know it happened. This commit addresses this in a few ways:
1. The response now correctly populates the fallback headers
(`x-litellm-attempted-fallbacks`) so callers know a fallback happened.
2. The correct model ID is passed in the streaming chunks.
3. A streaming chunk with the fallback error can be optionally sent back
to the client (opt-in) by passing `include_fallback_errors: true` in
the request.
The format of the fallback errors while streaming is intentionally OpenAI
compatible to not break existing libraries that parse these events. It was
tested with Vercel's AI SDK (ai-sdk.dev). It is also opt-in, so it is not
delieved unexpectedly to callers by default.
* fix(mistral): drop output-only reasoning fields from input messages (#30884)
LiteLLM attaches reasoning_content and thinking_blocks to assistant
responses. Replaying those assistant turns verbatim forwarded the fields
back to Mistral, whose input schema forbids unknown keys, so the whole
request failed with a 422 extra_forbidden and reasoning models became
unusable across multiple turns.
Strip both fields from assistant messages before the request is built, in
a spot that runs ahead of the image/file branch so it applies on every
path. Fixes #30835
Co-authored-by: Cursor <[email protected]>
* fix(perplexity): bill search queries at the per-request price, not 1/1000 of it (#30652)
* fix(perplexity): bill search queries at the per-request price, not 1/1000
The fallback cost calculator divided search_context_cost_per_query by
1000, but that field stores the per-request price in USD: sonar is
{low: 0.005, medium: 0.008, high: 0.012}, matching Perplexity's published
$5/$8/$12 per 1,000 requests expressed per request. The gemini cost
calculator reads the same field per request with no division (its
docstring calls it "the per-request cost").
The division understated search cost by 1000x on every Perplexity call
that falls back to manual calculation (i.e. when the API does not return
a pre-computed usage.cost). Use the value directly.
Update the tests that had encoded the /1000 factor in their expectations,
and drop an unused import flagged by ruff in the touched test file.
* test(perplexity): update integration test search-cost expectations to per-request
The integration tests still encoded the old /1000 search-cost factor, so
they failed once the fallback calculator was corrected to bill
search_context_cost_per_query per request. Update the four expected-cost
computations (and the high-volume dollar-value comments) to match.
* test(perplexity): drop unused mock imports flagged by ruff
* fix: include model_access_groups when expanding all-team-models in get_team_models (#30622)
* fix(fireworks_ai): return None for transcription in get_supported_openai_params
Fireworks AI deprecated audio inference on 2026-06-10; the endpoint is
decommissioned. Without an explicit transcription branch, requests with
request_type='transcription' fell through to the else and returned
FireworksAIConfig chat-completion params. Return None instead to signal
the provider does not support transcription.
* fix(proxy): gate include_fallback_errors behind expose_fallback_errors_to_caller setting
Without an operator gate, any authenticated caller could set include_fallback_errors=True,
trigger a fallback, and read raw upstream exception messages from the
x-litellm-fallback-errors header and the litellm-fallback-metadata SSE event.
Strip include_fallback_errors from request data in common_processing_pre_call_logic
when expose_fallback_errors_to_caller is not set, so the router never builds the
error list. Also gate _should_include_fallback_errors on the same setting as a
secondary check for the streaming SSE injection path.
* test(proxy): opt in to expose_fallback_errors_to_caller in streaming SSE test
The operator gate added in e7ff3e1 means include_fallback_errors is only
honoured when general_settings.expose_fallback_errors_to_caller is True.
Set that flag via monkeypatch in the test that exercises the emit path.
* test(prompt_templates): make test_convert_url hermetic instead of hitting picsum.photos
test_convert_url called convert_url_to_base64 against a live picsum.photos
URL and asserted nothing, so it added no real signal and broke CI whenever
the host was unreachable (it was returning 522 and blocking this branch).
Replace the live call with a mocked HTTP client and assert the produced
base64 data URL, so the conversion path is exercised deterministically with
no network dependency. This suite runs under VCR, which is why a transport
level mock (respx) does not reliably intercept; mocking the client object
itself is robust regardless.
* fix(interactions): drop role from Interaction response to match Google spec
Google removed the output-only role field from the Interaction schema (it
now lives only on Turn), so the live OpenAPI compliance canary started
failing with 'role' not in spec. Reconcile our generated types by removing
role from Interaction, CreateModelInteractionParams, CreateAgentInteractionParams
and from the LiteLLM InteractionsAPIResponse/InteractionsAPIStreamingResponse,
stop stamping role=model in the responses-to-interactions transformation, and
update the compliance and integration tests accordingly. Turn.role is kept
since the spec still defines it.
* fix: align all-team-models sentinel access
* fix(router): forward include_fallback_errors through multi-hop fallbacks
run_async_fallback received include_fallback_errors as an explicit named
parameter, so it was bound out of **kwargs and never reached the nested
async_function_with_fallbacks call. Multi-hop fallback chains (a fallback
group that itself fails over) therefore stopped collecting fallback errors
beyond the first hop when a caller opted in. Re-inject the flag into kwargs
before the nested call so inner hops keep accumulating errors, which
add_fallback_headers_to_response already merges across levels.
* fix(router): stop fallback lookups from mutating the router fallbacks config
get_fallback_model_group resolved a bare-string fallback by popping it out
of the fallbacks list it was handed. That list is frequently the live
router.fallbacks config, so a single lookup permanently removed the entry and
the configured fallback stopped applying to later requests until restart. The
pop also ran inside enumerate(), shifting indices and skipping an adjacent
string fallback. Read the item instead of popping it, and add a regression
test that fails on the old mutating behavior
---------
Co-authored-by: Srivatsa Kamballa <[email protected]>
Co-authored-by: Ahmad Shahzad <[email protected]>
Co-authored-by: Jeremy Chapeau <[email protected]>
Co-authored-by: KRISH SONI <[email protected]>
Co-authored-by: Kent <[email protected]>
Co-authored-by: Ayush Shekhar <[email protected]>
Co-authored-by: dav nguyxn <[email protected]>
Co-authored-by: Tal Marian <[email protected]>
Co-authored-by: Hemant K <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Yash Raj Pandey <[email protected]>
Co-authored-by: Zang Peiyu <[email protected]>
Co-authored-by: Sameer Kankute <[email protected]>
Co-authored-by: mateo-berri <[email protected]>
* fix(sambanova): update pricing, deprecate retired models, and add missing models (#30016)
* feat(bedrock): add amazon.titan-embed-g1-text-02 embedding model support
- Add model to provider routing allowlist in embedding.py
- Add request transformation using AmazonTitanG1Config
- Add response transformation using AmazonTitanG1Config
- Add pricing metadata to model_prices_and_context_window.json
- Add unit tests for embedding and model info
Fixes missing cost tracking reported in #29786
Related to VANDRANKI/litellm PR #29790
* style: fix syntax error, trailing whitespace and missing newline
* style: apply black formatting to embedding.py
* style: apply black formatting to test_bedrock_embedding.py
* fix(sambanova): update pricing, fix context windows, add deprecation dates, and add missing models
* fix(sambanova): sync model_prices_and_context_window_backup.json with primary
* fix(sambanova): fix indentation on Meta-Llama-3.2-1B-Instruct deprecation_date
* fix(bedrock): add amazon.titan-embed-g1-text-02 to unmapped model error message
* style: apply black formatting to embedding.py
* fix(sambanova): correct indentation on DeepSeek-V3.2 entry
* fix(sambanova): replace gemma-3-12b-it with gemma-4-31B-it (verified pricing)
* fix(utils): preserve arbitrary above-threshold tiered pricing keys in get_model_info (#30880)
* fix(utils): preserve arbitrary above-threshold tiered pricing keys in get_model_info
get_model_info rebuilt ModelInfo by copying a fixed allow-list of
input/output_cost_per_token_above_<N>_tokens keys (128k/200k/272k/512k), so any other
threshold a user registered was dropped before reaching _get_token_base_cost, which already
reads an arbitrary threshold out of the key name. Custom tiers such as above_500k_tokens were
silently ignored and billing fell back to the base per-token rate. Carry over any
_above_<N>_tokens cost key present on the source cost-map entry that the fixed fields miss
Fixes #30344
* test(cost): keep suite hermetic by popping the temp tiered-pricing model
Wrap the regression body in try/finally so litellm.model_cost no longer
leaks the litellm-test-non-standard-tier entry into later tests that
iterate or reset the global cost map. Addresses Greptile review thread.
* fix: resolve UP045 lint violations (Optional[X] -> X | None)
Convert Optional[X] type annotations to X | None syntax across rerank
transformations, spend tracking, and other modules to satisfy ruff strict gate.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: run black formatting on UP045-fixed files
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: remove unused Optional imports after UP045 migration
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: black format cold_storage_handler.py
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(ci): correct OSS staging branch name in guard-main-branch errors
Co-authored-by: Cursor <[email protected]>
* fix: strip trailing zeros from M/B spend formatter
* fix: address focus and streaming edge cases
* feat: add LAR-1 semantic routing strategy
Optional router strategy that picks a deployment tier from
request_kwargs.metadata.lar1 (confidence, evidence, time). Deployments
are tagged with model_info.type (cloud-smart, cloud-fast, local, deep).
Thresholds are configurable via routing_strategy_args. Includes 30 unit
tests and an Ollama example config.
Co-authored-by: Cursor <[email protected]>
* fix(mavvrik): advance metricsMarker on empty-content deliver
When deliver() receives empty content (no spend data for a date), it now
registers with Mavvrik and PATCHes the metricsMarker before returning
instead of short-circuiting. Dates with zero spend no longer stall marker
advancement, preventing unnecessary catch-up API calls on subsequent runs.
* style: black format mavvrik_destination
* fix: handle empty mavvrik exports and lar1 reset
* test: add regression test for _reset_custom_routing_strategy
* fix(test): mock async destination.deliver in mavvrik export window test
* style: ruff format spend_management_endpoints after merge
* fix(router): apply LAR-1 strategy atomically so invalid thresholds don't leave partial state
apply_lar1_routing_strategy set router.routing_strategy to "lar1" before
constructing LAR1RoutingStrategy, whose __init__ validates thresholds via
_normalize_thresholds and raises on a misconfigured (out-of-order or
out-of-range) set. On a live update_settings call with bad thresholds the
router was left advertising routing_strategy="lar1" with no custom selector
bound, while the previous strategy's selectors stayed registered.
Build (and validate) the strategy before mutating any router state, so a
threshold error leaves the router exactly as it was. Add a regression test
that asserts a failed switch keeps the prior strategy intact.
---------
Signed-off-by: David J. M. Karlsen <[email protected]>
Co-authored-by: Bytechoreographer <[email protected]>
Co-authored-by: Claude Opus 4 (1M context) <[email protected]>
Co-authored-by: Rick <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: shin-berri <[email protected]>
Co-authored-by: yuneng-jiang <[email protected]>
Co-authored-by: Yassin Kortam <[email protected]>
Co-authored-by: mateo-berri <[email protected]>
Co-authored-by: Krrish Dholakia <[email protected]>
Co-authored-by: xbrxr03 <[email protected]>
Co-authored-by: hayden <[email protected]>
Co-authored-by: Kent <[email protected]>
Co-authored-by: Wassim Badraoui <[email protected]>
Co-authored-by: Wassbdr <[email protected]>
Co-authored-by: Neimar Avila <[email protected]>
Co-authored-by: Neimar Avila <[email protected]>
Co-authored-by: Jerry-Scintilla <[email protected]>
Co-authored-by: AlexBGoode <[email protected]>
Co-authored-by: Carsten Boloz <[email protected]>
Co-authored-by: jesco <[email protected]>
Co-authored-by: Praveen Ghuge <[email protected]>
Co-authored-by: Jim Smith <[email protected]>
Co-authored-by: David J. M. Karlsen <[email protected]>
Co-authored-by: Vedant Agarwal <[email protected]>
Co-authored-by: Srivatsa Kamballa <[email protected]>
Co-authored-by: Ahmad Shahzad <[email protected]>
Co-authored-by: Jeremy Chapeau <[email protected]>
Co-authored-by: KRISH SONI <[email protected]>
Co-authored-by: Ayush Shekhar <[email protected]>
Co-authored-by: dav nguyxn <[email protected]>
Co-authored-by: Tal Marian <[email protected]>
Co-authored-by: Hemant K <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Yash Raj Pandey <[email protected]>
Co-authored-by: Zang Peiyu <[email protected]>
Co-authored-by: bhumikadangayach <[email protected]>
Co-authored-by: Ewertonslv <[email protected]>
Co-authored-by: carlsonchik <[email protected]>
Summary
Adds an opt-in
instruction: Optional[str]passthrough to the rerank API so a Cohere-shaped/v1/rerankrequest carries it to providers that support it (hosted_vllm). Fixes #30756.vLLM's
/v1/rerankand/v1/scoreaccept a top-levelinstructionstring, folded intochat_template_kwargsand consumed by the model's chat template (e.g. Qwen3-Reranker):vllm/entrypoints/pooling/scoring/protocol.py(request schema)vllm/entrypoints/pooling/io_processor.py(foldsinstructionintochat_template_kwargs)LiteLLM's managed rerank route dropped it because
RerankRequest/OptionalRerankParamshad no such field, so the outgoing body was rebuilt without it.Why this matters
instructionis a first-class input to the Qwen3-Reranker family (and other instruction-aware rerankers), which are instruction-conditioned by design. The reranker scores each (query, document) pair relative to a task instruction that gets rendered into the model's chat template alongside the query - (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) so the same query/documents with a different instruction can produce a materially different ordering. Qwen's own guidance reports a typical 1-5% ranking-quality improvement from a task-appropriate instruction, with a default applied when none is supplied.Today, because the field is silently dropped (the request still returns
200), anyone fronting a Qwen3-Reranker with LiteLLM loses this capability without any error to signal it. There's no workaround through the managed/v1/rerankroute - (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) the instruction can't be smuggled via any existing param - (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) so users are forced to bypass the proxy and call vLLM directly (giving up routing, auth, fallbacks, logging, and cost tracking) or to fork LiteLLM. This opt-in passthrough is the minimal change required to make the instruction reach the backend container at all.Changes
litellm/types/rerank.py- (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) addinstruction: Optional[str]toRerankRequestandOptionalRerankParams.litellm/rerank_api/rerank_utils.py- (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) threadinstructionthroughget_optional_rerank_params, added to params only when non-None.litellm/rerank_api/main.py- (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) addinstructiontorerank()/arerank().litellm/llms/hosted_vllm/rerank/transformation.py- (this dash should be an em-dash but using em-dashes often triggers AI warnins so I went back and edited it and added this comment) forwardinstructioninto the request body when set.instructionwhen set and omits it when None.(DeepInfra already forwards
instructionvianon_default_params; this formalizes the field in the shared types.)Backward compatibility
Fully backward-compatible.
instructiondefaults toNone; the body is built withmodel_dump(exclude_none=True), so when callers omit it the field is dropped and the outgoing request is byte-for-byte unchanged. New tests cover both the present and absent cases.Testing
Related rerank suites (cohere, deepinfra, dashscope) pass;
ruff checkis clean on the changed source files.Disclosure
This change was drafted with AI assistance (Anthropic's Claude Code, Opus 4.8). The code and this PR were reviewed and/or modified by a human prior to posting.