Keeps the base's rule that a non-admin id lookup matching no spend-log row answers 403, so the detail route never consults cold storage without an owner row
A guardrail that answers one rewritten text per message it saw no longer
matches the texts the Responses handler extracted once the request carries
instructions or tool items, so the rewrite was rejected with a 500. Spread
such an answer over the structured messages' text slots and write it back
through the structured path, have Prompt Security modify return
structured_messages directly, and give the chat completions pairing the same
named rejection instead of a silent misalignment when the counts differ.
The notice now asks the sweep's own duplicateTarget whether the title
match would close the issue, so a two-word title no longer gets a close
warning the sweep would refuse to act on. The ask no longer promises
that a reply removes the label, since nothing does that automatically
A verdict is now dropped when it names a pull request, the issue itself,
or a newer issue, and the label goes on before the comment so a failed
comment leaves no marker and the rerun finishes the job. The flag logic
lives in scripts/flag-duplicate-issue.ts next to the sweep it feeds,
sharing normalizeTitle and the marker format, with bun tests that run on
pull requests touching it
A run over MAX_BASE64_LENGTH_STDOUT_LOG now stays in the log line only when it is hex or decimal with at least two distinct characters. Collapsing only mixed-case runs let every constant-byte payload through: 0x00 encodes to AAAA, 0x01 to AQEB, 0x55 to VVVV, 0xAA to qqqq, so a zero-filled upload still paid the full secret regex.
The two traceback tests that raised a 100,000-character run of one letter now raise the same text the other length-cap tests use, since a single-letter run is exactly the shape the collapse treats as a constant-byte payload
Only mixed-case runs of the base64 alphabet collapse now, so a long hex digest,
numeric id, or padding run stays in the debug line. The truncation filter also
formats the traceback at every level and collapses base64 runs in it before the
secret regex sees it, instead of only capping its length at INFO and above
The old check_duplicate_issues.yml matched on title wording, so it missed the
same bug reported in different words. Over one full week of new issues (167,
5 to 12 Sep) it flagged 2, both wrong, while hand review found 11 real
duplicates that nothing caught.
The new workflow fetches the issue through the API into a file, runs
openai/codex-action with a fixed prompt and an output schema, and lets Codex
search the tracker with gh. At a 0.95 confidence gate it would have posted 12
comments that week, 9 naming a real duplicate. It reuses the same marker
comment and potential-duplicate label as before so auto-close-duplicates.yml
keeps working unchanged, and warns about the auto-close only when the titles
actually match.
Traffic goes through LiteLLM: the key is a virtual key and the endpoint is
the proxy's /v1/responses. Comments and labels stay off until the
DUPLICATE_CHECK_ENABLED repo variable is set.
Since #37391 every log record went through the secret-redaction regex twice, once in the
filter and again in the formatter, and the formatter pass ran on the whole formatted line. At
DEBUG level a multi-megabyte request body (a multi-page PDF upload to /v1/ocr) turned each of
those lines into ten seconds of synchronous regex work on the event loop, long enough for a
Kubernetes liveness probe to restart the pod mid-request.
The filter is now the complete scrubber (message, exception text, stack info, and extras) and
stamps the record, so the formatters skip records that are already clean. The stdout
truncation filter also collapses base64 runs longer than MAX_BASE64_LENGTH_STDOUT_LOG (4096
by default) at every level before the secret regex sees them, so a debug line carrying a
request body costs milliseconds instead of seconds.
* fix(proxy): bound tool and guardrail index create_many by the spend-log statement budgets
One flush drains up to MAX_LOGS_PER_INTERVAL source transactions or logs, but a
transaction fans out to one LiteLLM_SpendLogToolIndex row per tool and a log
to one LiteLLM_SpendLogGuardrailIndex row per guardrail, so the index
create_many payload was unbounded. Both index writes now go through
spend_log_write_batches(SPEND_LOG_WRITE_BATCH_MAX_BYTES, SPEND_LOG_WRITE_BATCH_MAX_ROWS).
The tool index write moves out of the rollup batch_() so the split reduces
the query-engine payload; replayed index rows are no-ops under
skip_duplicates, and the daily rollup upserts stay in one transaction
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(proxy): pin the row budget in the index fan-out tests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Router.log_retry used to copy the failed attempt's kwargs and metadata into
metadata.previous_models. Nothing downstream read those copies, but they carried
client credentials into spend logs and grew the payload on every retry. Each
attempt now leaves a flat record (model group, deployment id, exception type and
string, attempt number), which drops RETRY_BREADCRUMB_EXCLUDED_KWARGS and the
per-retry credential masking.
num_retries_per_request was enforced from len(previous_models), which only
looked at the metadata bucket and never exceeded four records. The sync and
async client wrappers and the Rust lifecycle guard now read attempted_retries
from whichever metadata bucket the call carries.
Resolves LIT-7505
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The bridge probe asked `responses_api_bridge_check` with the summary read straight off
the Responses object, but `litellm.completion` reads it from `optional_params` via
`peek_reasoning_summary_aliases`, which the bridged request never populated. So gpt-5,
gpt-5.1 and azure/gpt-5 answered "bridging" to the probe and "not bridging" for real,
and the object still landed on Chat Completions, which only takes a string
`reasoning_effort` is now always the effort string, and `summary` rides the
`reasoning_summary` alias that main.py already reassembles into `{effort, summary}` on
the bridged path. The alias is emitted only when the probe says the model bridges, so
no chat provider ever sees it, and the probe is now asked with the exact params this
transform emits
The debug call built its message with an f-string, which
test_logging_calls_do_not_build_their_message_eagerly rejects. Pass the exception
as a %-style argument so the message is only built when the log is emitted.
The bridge probe called responses_api_bridge_check without api_base, so it
resolved the OpenAI base from globals and environment rather than from the
request, while litellm.completion runs the same check with the caller's value.
Today the two cannot disagree: this path always supplies a reasoning_effort,
which short-circuits the endpoint term in the only arm that reads it. Passing it
anyway keeps the probe a faithful mirror of the definitive check rather than one
that happens to agree.
The Responses API takes reasoning as an object, {effort, summary}. Chat
Completions takes reasoning_effort as a string enum and has no equivalent of
summary, but the completion bridge forwarded the whole object whenever summary
was set, which agentic clients set on every request.
Bedrock Converse guards its mapping with isinstance(value, str) and has no else
branch, so the object fell through, thinking was never enabled, and the caller
was billed for a non-thinking turn with nothing in the response to explain it.
The object is still forwarded for the one caller that can consume it: a model
whose cost-map mode is responses, which litellm.completion bridges back onto the
Responses API and reassembles {effort, summary} there. That decision is delegated
to responses_api_bridge_check, the same check litellm.completion runs, rather
than a second copy of the rule that could drift from it. An object carrying no
effort now yields no reasoning_effort at all.
Multi-IdP deployments can now set virtual_key_claim_field and
unregistered_jwt_client_behavior on a JWTIssuerConfig entry. Tokens from
that issuer use the issuer-specific claim path and no-match policy for the
virtual key mapping lookup; issuers that omit them keep the global values.
The auth flow now enters the mapping lookup when any issuer configures the
field, not only when the global field is set.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
/model/info fills a deployment's missing pricing in from the model cost map so the
Admin UI has a rate to display. Clients echo that whole model_info blob back on save,
and update_db_model merged it into the row, so editing an unrelated setting turned
that day's catalog price into a real per-deployment override. After that the
deployment ignored the cost map and Reload Price Data could no longer move it,
because the reload replays each deployment's stored pricing over the fresh catalog.
Drop the derived pricing from incoming model_info on the two write paths. The
drop-set is read off the same objects the read path uses, CustomPricingLiteLLMParams
plus the tiered *_above_N_tokens pattern that get_model_info passes through and no
model declares, so it cannot drift as new rates are added. output_vector_size is
exempt: it lives on the pricing model but is an embedding dimension, not a rate.
A deployment's own pricing still rides litellm_params, which is untouched, as is the
explicit-null clear, which reads the incoming model rather than the filtered dict.
The filter sits in the endpoint bodies rather than _add_model_to_db, which master-key
rotation reuses to re-serialize every stored deployment.