A fake-streamed provider hands the adapter one chunk carrying both the
delta payload and the finish_reason, which is exactly what the combined
chunk splitter exists for, but its content check never listed the refusal.
The translation short-circuits on finish_reason, so that refusal text was
dropped and the client got `stop_reason: refusal` over an empty content
array, the symptom this PR set out to fix.
Both refusal accumulators also drop their `mutable-ok` lists for a plain
string attribute
The first-delta guard read `delta.refusal` directly, while the translation
three lines later goes through `openai_chat_refusal_text`, which also reads
the `provider_specific_fields` LiteLLM parks unrecognized fields in. A
provider that sends the refusal that way had its only refusal delta skipped
as blank, so the client got `stop_reason: refusal` over an empty content
array, which is the symptom this PR set out to fix
The /v1/messages adapter lowers a tier the entry does not accept, so dropping max from the astra
rows moves that path from Foundry's 400 to a request at xhigh. Nothing pinned that, and the guard
test's docstring named gpt-6-astra as the only gpt-5 name with an azure_ai row, which 11 rows
contradict.
The new Lyria passthrough branch runs before the image-generation branch
and keys on the same `predictions[0].bytesBase64Encoded` shape imagen
returns, so only the cost-map lookup separates them. Cover an imagen
predict response end to end so a future change that drops that lookup
fails here instead of misbilling images as audio.
Google prices Lyria per generated clip, so every Vertex Lyria entry in the
price map now carries a single output_cost_per_image and both the speech
and the passthrough cost paths read that one field. The old
output_cost_per_second and audio_seconds_per_prediction pair assumed a
30 second clip, which does not match the 32.768 second WAV Vertex returns,
and no other model in the map priced audio that way
Drops max_audio_length_hours and max_audio_per_prompt from the price map,
its schema, the generator, and ModelInfo, since nothing reads them, and
drops the audio_mime_type hidden param for the same reason: the response
already carries the resolved content type on its own header
Folds the per-model bundled catalog lookups into one cached parse of the
local cost map, validated with a TypeAdapter over a ReadOnly TypedDict
gpt-6-astra is the only gpt-5-family name with an azure_ai row. Prefixing the rest
cost them every effort flag, since get_llm_provider sends an azure_ai name down the
azure provider when a global AZURE_AI_API_BASE points at an openai.azure.com host and
azure/<model> is not a key either, which turned temperature, top_p and logprobs on
azure_ai/gpt-5.1-chat-latest from accepted into an UnsupportedParamsError.
Both bridges opened an empty text block on a refused streaming turn and
closed it without a single delta, so a client replaying that assistant
turn got HTTP 400 "text content blocks must be non-empty" from Anthropic.
The safeguard-refusal fallback that motivated withholding the text only
runs on the awaited non-streaming response, so nothing needed it withheld
Move the refusal readers into the shared messages/utils helpers so the
adapters stop reaching into each other's private statics, which is also
what put reportPrivateUsage over its budget
The AzureAIStudioConfig.map_openai_params override now carries dict[str, object]
annotations instead of bare dict, and the docstrings added to the new tests go away
since the test names already say what they cover. No behavior change
Foundry deployments of gpt-6-astra reached through azure_ai used the bare OpenAI card
for the reasoning_effort none gates, so temperature and top_p were refused while the
azure_ai card says none is supported. AzureAIStudioConfig now dispatches gpt-5 series
params through AzureAIGPT5Config, which looks capabilities up under the azure_ai/
prefix the way the azure route does
Also carries the search_context_cost_per_query block azure/gpt-6-astra has, adds a
flex service tier cost test that fails at the merge base, and keeps the wildcard test
from stripping azure_ai/gpt-6-astra out of the provider set
The chat and Responses bridges serialize tool_use blocks with model_dump(), so every
bridged /v1/messages response carried LiteLLM's internal provider_specific_fields key
(null, or a Gemini thought signature). Clients replay the block verbatim, and the next
turn that lands on a native Anthropic deployment (auto-router tier change, model swap)
is rejected with "tool_use.provider_specific_fields: Extra inputs are not permitted"
Strip the key from replayed content blocks at the single native Anthropic dispatch so
already-poisoned transcripts self-heal on every native provider, and stop emitting the
null on new responses. The bridges keep reading the signature for the Gemini round trip
Closes#19739
Format the model-picked id with %r so control characters in it cannot
break the log line. The regression test for the unlisted id keeps to
generic scoping wording
Emulated file_search now warns when the model returns a vector_store_id that is
not one of the request's stores, naming the dropped id and the stores that were
searched instead. H16 asserts the warning is emitted exactly once.
The emulated file_search handler searched whatever vector_store_id the
model returned, so a model steered to an id outside the request's
file_search tool reached a store the per-key vector store permission
check never saw. An id outside the request's stores now falls back to
those stores; an id that is one of them still narrows the search to it.
Health checks probed every OCR deployment with a PDF, which Cohere Parse
rejects, so /health, background health checks, and the UI Test Connection
button marked Cohere Parse deployments unhealthy. BaseOCRConfig gains a
get_health_check_document hook (PDF by default) that CohereParseConfig
overrides with a 1x1 PNG data URI. cohere also gains ocr in the provider
endpoint matrix
* fix(responses): decode JSON-string tool schemas before sending to the provider
A caller that hands a tool schema over already JSON-encoded reached the
Responses API with a string `parameters`, and the provider rejected the
request with a 400 naming the routed model instead of the offending tool.
Decode it at the one place every Responses request converges, and refuse
anything that is neither an object nor a string encoding one.
Collapses the duplicated input/tool sanitization block shared by the
request and compact-request builders into a single owner, so the decode
cannot be wired into one path and not the other.
* test(responses): pin null tool schemas as accepted, and type the parametrized cases
The Responses API serves `parameters: null` and an omitted schema alike, so
neither may raise. Pin both against a future tightening, annotate the
parametrized inputs, and trim the docstrings back to what the code does not
already say.
Bedrock passthrough Converse routes flattened every non-empty string under
toolConfig.tools into the guardrail INPUT texts, so tool names, tool
descriptions and JSON-schema strings (object, property names, titles, type
names, enum values) each arrived as a separate guardrail item. A request whose
only prompt was one benign user message could be blocked outright because a
denied term appeared in an app-authored tool definition.
Tool definitions are now excluded from the extracted texts, matching every
other guardrail translation handler, which carries tool definitions in the
structured tools input rather than in texts. Caller content stays scanned:
message text, toolUse.input, toolResult content and json, and
additionalModelRequestFields are unchanged.
Resolves LIT-5797
* fix(anthropic): never carry cache_control on translated thinking blocks
The /v1/messages adapter built every thinking and redacted_thinking block with
cache_control=content.get("cache_control", {}), so a block the client never
marked still came out carrying an empty cache_control. anthropic_messages_pt
replays thinking blocks verbatim and first, so that value landed at content[0]
of the outbound assistant message and Anthropic rejected the request with
messages.N.content.0.thinking.cache_control: Extra inputs are not permitted.
Anthropic's schema has no cache_control on either block type, so there is
nothing to gate or translate here, only to stop copying. Every sibling block
type already routes through _add_cache_control_if_applicable; these two were
the only ones setting the key unconditionally.
This is reachable from any caller that round-trips Anthropic messages through
the OpenAI shape, which is why shadow eval saw it on a majority of sampled
Claude Code turns while the same traffic served natively was fine.
* test(anthropic): assert the outbound wire body for redacted thinking blocks
Async searches reach the sync client through executor threads, so the LRU cache
is shared state. A key could be evicted between the lookup and the reordering
that followed it, and the reordering then raised KeyError and became a 500.
Reproduced at 15 failures per run with 16 threads over 34 keys and a 1ns switch
interval; the regression test is that workload.
litellm only retries 408, 409, 429 and 5xx, so classifying a dropped connection
as a 400 turned one replica set failover into a permanently failed search. It is
a 503 now, with the message still naming the misconfigurations that also close a
connection.
The client cache skipped insertion once it held 32 entries, so any store added
after that rebuilt its client on every search, paying an SRV lookup, a TLS
handshake and topology discovery each time. It evicts the least recently used
entry instead, which only drops the cache's own reference.
Also trims the explanatory comments to the one-line form the repo asks for.
The base vector store interface grew an embedding_executor argument, and
litellm.vector_stores.search now always passes one. MongoDB still carried its
own embedding_fn/aembedding_fn constructor seam, so every search through the
public entry point failed with an unexpected keyword argument.
Drop the local seam in favour of the shared executor: one path instead of two,
and the unit tests now drive the same seam production uses.
tlsCAFile and tlsCertificateKeyFile are how a self-managed deployment presents a
private CA, so they are the options on-prem operators actually set. pymongo opens
those files itself during TLS setup and lets OSError out, which is neither a
PyMongoError nor a ValueError, so it missed every branch of the translator and
litellm.exception_type turned it into a 500 with a traceback in the body. A
mistyped path, or one that exists on the host but not inside the container, is a
routine mistake and has to read as a 400 naming the file.
Matched on the exception carrying a filename so a socket-level OSError still falls
through to the branches that handle it. Verified against a self-managed mongod with
a missing CA file, a CA path that is a directory, and a missing client certificate.
mongod serves $vectorSearch identically whether mongot runs under Atlas or beside
a self-managed deployment, so the provider already worked against on-prem. The
guidance did not: a refused connection told the operator to check their project's
IP access list and whether the cluster was paused, neither of which exists outside
Atlas, and the index errors claimed an "Atlas Vector Search index" they do not have.
Every message now names a remedy for both, keeping the Atlas-specific hint labelled
as such.
Also diagnoses unescaped credentials, which self-managed deployments hit more often
because the password is usually generated. pymongo reports those three different
ways and none of them mentions the password: '@', ':' and '%' raise an RFC 3986
complaint, '/' is read as the database separator and surfaces as Bad database name,
and an unescaped ':' looks like a bad port and comes back as a plain ValueError.
All three now point at the credentials. The ValueError branch's comment claimed it
fired on an unescaped '/', which pymongo actually reports as InvalidURI; corrected
to the port parse it really catches.
Verified against a self-managed mongod 8.0 with mongot, reached over plain
mongodb:// with no SRV and no TLS: 13 cases with live OpenAI embeddings, and 4
credential cases against an auth-enabled instance whose password holds % @ / and :.
list_search_indexes returns the same queryable and status fields there as on Atlas,
so the index-readiness check needed no change.