Commit graph

12001 commits

Author SHA1 Message Date
Yucheng Zhu
b728bba7e1 refactor(proxy): satisfy the type-discipline ceiling on the disclosure surface
One never-rebound name on the org info path carries Final, matching the ratchet the rest of the stack now sits under.
2026-08-12 22:52:58 -07:00
Yucheng Zhu
3a12f794b3 feat(proxy): disclose resolved destinations on team and organization info
Adds resolved_logging_exporters to /team/info and /organization/info, naming every destination whose access scope grants that identity and which actually builds.

Squashed onto the rebased export branch; the previous history conflicted with staging's Final-annotation pass.
2026-08-12 22:52:58 -07:00
Yucheng Zhu
29806dee16 refactor(otel/v2): satisfy the type-discipline and strict-lint ceilings
Some checks failed
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Never-rebound names carry Final, deliberate rebinds carry rebind-ok with the reason, and the endpoint suffix check collapses into one endswith call. Two hook parameters and the constructor kwargs move from Any to object, which is both stricter and what the bodies actually rely on.

The remaining constructions keep a mutable-ok naming what needs a concrete dict: pydantic model_copy payloads, the OTel SDK Resource call, and the LRU caches that are mutable by design.
2026-08-12 10:59:02 -07:00
Yucheng Zhu
93061061fa feat(otel): export the trace to the resolved destinations
Fans each request's trace out to the destinations its identity resolved, applies per-request credentials only to the exporter their own backend contributed, and caches tracer providers per credential, endpoint and transport.

Squashed onto the current staging tip; the previous history conflicted with staging's Final-annotation pass over the same OTEL v2 modules.
2026-08-12 10:45:55 -07:00
Yucheng Zhu
4588c3b3f0 refactor(otel/v2): satisfy the type-discipline and strict-lint ceilings
Some checks failed
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Same ratchet as the credential-model branch: never-rebound names carry Final, the request-state mirror logs at debug instead of swallowing silently, and the empty header and resource fallbacks share one immutable mapping.

Two constructions keep a mutable-ok with a reason: the dedup accumulator consumed on the next line, and the TypedDict rows the resolver returns.
2026-08-12 10:43:25 -07:00
Yucheng Zhu
fc1ef61826 fix(otel/v2): keep the request path off opentelemetry when the feature is not in use
Publishing the destination ContextVar imports the OTel context module, and opentelemetry ships only in the proxy-runtime extra. That call sat outside the resolver's try, so a litellm[proxy] install raised ModuleNotFoundError on every request over a feature it never enabled; the flag being off did not help, since the gate lives inside the resolver and an empty result still fell through.

The helper now returns before publishing when the flag is off, and a missing package with the flag on warns instead of raising, matching what instrument_fastapi_app already did.
2026-08-12 10:37:09 -07:00
Yucheng Zhu
d31b4a3fde perf(otel/v2): resolve the caller's org only when a destination is org-scoped
The org fallback loads the team, which on a cache miss is a Prisma read on the authentication path. It cannot change which destinations are selected unless some access names an org, so it now runs only then; the logging credentials are also parsed once instead of twice.
2026-08-12 10:37:09 -07:00
Yucheng Zhu
b5a51e72cd feat(otel): resolve a request's trace destinations from its identity 2026-08-12 10:37:09 -07:00
Yucheng Zhu
168a20b065 fix: bind GET /credentials to its handler, not the verdict helper
Some checks failed
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
The type-discipline refactor inserted _destination_verdict between the
@router.get decorator and get_credentials, so FastAPI registered the helper
as the listing endpoint. It declares a CredentialItem parameter, which FastAPI
reads as a required request body, and every GET /credentials returned 422.

Add the GET routing regression test alongside the existing PATCH one; the
other tests call get_credentials directly and stay green while the route is
dead.
2026-08-12 10:33:53 -07:00
Yucheng Zhu
04457114cc fix(otel/v2): pin the generic passthrough to HTTP and report whether a destination builds
An `arize` credential carrying only `otel_endpoint` is built by the generic passthrough but
resolved under the `arize` name, so the router applied Arize's intrinsic gRPC transport to
the plain HTTP URL the admin typed. The destination delivered nothing while `/team/info`
kept advertising it as active, which is the invariant `resolved_logging_exporter_names`
exists to hold. The passthrough now pins `otlp_http`, which is what it has always meant;
adapter-built Arize destinations keep their gRPC default, covered by its own test.

`GET /credentials` now reports `resolves_to_destination` for logging credentials, computed
by `destination_for_credential` so it cannot drift from the resolver or the disclosure. The
dashboard's Scope column needs this verdict; recomputing the adapter rules there would drift
from them, and that drift is what let a dead destination read as live.

Both regression tests were mutation-checked by reverting their own fix in isolation.
2026-08-12 10:33:53 -07:00
Yucheng Zhu
6897201b4f feat(credentials): admin-owned logging credential, access shape, and destination mapping 2026-08-12 10:33:53 -07:00
Yassin Kortam
b0626cad8c
perf(proxy): stagger scheduled background jobs across jobs and pods (#36589)
APScheduler anchors an interval job at now + interval, so every scheduled
background job registered in one proxy startup shares a single firing instant
for the life of the process, and every replica a rollout brought up together
shares that instant too. Each tick the spend flushes, budget reset sweep,
config-in-DB reload, credential reload and cost pollers all hit Postgres at the
same moment, on every pod, competing with request-path auth and budget queries.

Shift each eligible job by a deterministic offset derived from
sha256(job_id, identity), where identity covers the pod and the worker process.
The offset lives in the trigger rather than in a one-off next_run_time, because
a cron trigger recomputes each fire from the wall clock and would otherwise snap
straight back onto the shared instant. An interval job is never offset by more
than one of its own periods.

Only schedules LiteLLM chose are shifted: interval jobs always, cron jobs only
when the id is one of the product's own defaults, so an operator-supplied
crontab keeps the instant it asks for. general_settings.scheduled_job_stagger
turns it off, widens the window, replaces the identity, or pins a job. The
applied offsets are logged once at startup and each fire logs its scheduled
instant against its actual start.

Resolves LIT-5433
2026-08-12 09:17:31 -07:00
Mateo Wang
8c2edbfc66
Merge pull request #36590 from BerriAI/litellm_lit012_readonly_typeddict
feat(lint): gate writable TypedDict fields with LIT012
2026-08-12 08:39:34 -07:00
Praveen11558
0ca0fa22b8
fix: refactor HTTP handler initialization with client support (#30952)
* bug: Refactor HTTP handler initialization with client support

* Update transformation.py

* bug: fixing the passing of clientID for the psc calls

* Update llm_http_handler.py

* Update llm_http_handler.py

* Update transformation.py

* Remove duplicate 'plugins' field definition

Removed duplicate definition of 'plugins' field.

* Update proxy_server.py

* Update transformation.py

* Update transformation.py

* Update test_vertex_gemma_transformation.py

* Refactor HTTP client handling for Vertex Gemma

* Refactor tests to use mock_get_client for HTTP calls

* Update transformation.py

* Update transformation.py

* Refactor patches for async HTTP client in tests

* fix: refactor HTTP handler initialization with client support

---------

Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
2026-08-12 15:17:51 +02:00
Mateo Wang
f64479e74d
Merge pull request #34177 from atomic/fix/nvidia-nim-ranking-image-passages-top-n
fix(nvidia_nim): preserve image passages and stop sending top_k to /v1/ranking
2026-08-12 01:42:00 -07:00
Mateo Wang
f8caaf4d2d
Merge pull request #32536 from dcadenas/litellm_fix_codex_responses_namespace_tools
fix(responses): preserve Codex namespace tool calls
2026-08-12 01:10:30 -07:00
Mateo Wang
cfcd0cda8a fix(responses): leave namespace unset on non-namespace tool calls 2026-08-12 00:28:15 -07:00
Mateo Wang
9cc5a818c3
Merge pull request #36154 from BerriAI/devin_ai_sse_keepalive_openai_routes
feat(proxy): global SSE keepalive ping interval for OpenAI-shaped streaming routes
2026-08-12 00:19:13 -07:00
Mateo Wang
a64a83bf36 fix(responses): keep custom_tool_call echoes on their advertised short name 2026-08-12 00:19:08 -07:00
Mateo Wang
9bfe593241
Merge pull request #35880 from BerriAI/devin_ai_fix_cost_estimate_onprem_provider_35210
fix(proxy): forward resolved provider and deployment pricing in /cost/estimate
2026-08-12 00:08:40 -07:00
Mateo Wang
23b805d5a4
Merge pull request #36447 from BerriAI/litellm_anthropic_fast_mode_speed_usage
fix(anthropic): preserve speed=fast in usage for /v1/messages and pass-through
2026-08-12 00:05:59 -07:00
Mateo Wang
ca14e52b08 fix(responses): requalify echoed namespace tool calls with their flattened name 2026-08-12 00:01:34 -07:00
yuneng-jiang
5e620af405
Merge pull request #36600 from BerriAI/litellm_/bedrock-retired-sonnet-test-model
test(bedrock): repoint live Claude tests off the retired Claude 3 Sonnet
2026-08-11 23:42:03 -07:00
mateo-berri
1d7c23a424 Merge branch 'litellm_internal_staging' into fix/nvidia-nim-ranking-image-passages-top-n 2026-08-11 23:41:54 -07:00
yuneng-jiang
e5e6728c8e
Merge pull request #36597 from BerriAI/litellm_/litellm-test-failures-debug-b4200d
fix(model_prices): advertise native structured output on every Bedrock DeepSeek V3.2 and GLM 5 id
2026-08-11 23:41:46 -07:00
Mateo Wang
397fcd0e6b fix(responses): serialize flattened namespace tools and keep tool results adjacent to tool_calls 2026-08-11 23:41:44 -07:00
mateo-berri
e53f044d20 fix(proxy): resolve the global SSE keepalive interval through the per-deployment engine
The outer wrap_sse_stream_with_keepalive_pings layer duplicated the
keepalive engine that PR #34423 already runs inside async_data_generator
for chat completions and responses streams, and it kept pinging
deployments whose operator set keepalive_seconds: 0 as a hard disable.
sse_keepalive_ping_interval_seconds is now the global fallback inside
_resolve_keepalive_seconds, so deployment and request values keep
precedence, an explicit 0 still disables, the [1, 300]s clamp applies,
and router-less proxies arm the wrap when the global default is set.
2026-08-11 23:37:43 -07:00
mateo-berri
d5a1896cf4 test: drop rerank package marker colliding with voyage test package 2026-08-11 23:37:27 -07:00
mateo-berri
0fdbe03c50 fix(proxy): honor model_info custom pricing in /cost/estimate 2026-08-11 23:25:01 -07:00
mateo-berri
d9ad21699c fix(anthropic): preserve fast-mode speed on parsed messages responses
The Rust messages bridge logs a parsed Anthropic response without an
httpx_response, so the fallback transform dropped the request speed and
billed fast-mode calls at the standard rate. Thread optional_params
speed into transform_parsed_response and add a regression test for the
parsed-response branch.
2026-08-11 23:21:23 -07:00
Mateo Wang
ae2a1f4aba Merge branch 'litellm_internal_staging' into litellm_fix_codex_responses_namespace_tools 2026-08-11 23:16:34 -07:00
mateo-berri
22088138ca test(nvidia_nim): move ranking transform regressions to the covered unit tree 2026-08-11 23:08:46 -07:00
mateo-berri
464a4cf207 Merge remote-tracking branch 'origin/litellm_internal_staging' into pr35880_local 2026-08-11 23:01:05 -07:00
Mateo Wang
7e80e094c4
Merge pull request #36529 from william-xue/fix-responses-passthrough-stream-cost
fix(proxy): track streamed passthrough Responses cost
2026-08-11 21:58:42 -07:00
mateo-berri
08a73740ec fix(passthrough): keep prompt/completion token split for streamed OpenAI rows 2026-08-11 21:28:55 -07:00
mateo-berri
5e14649c54 fix(passthrough): bill streamed Responses calls that end failed
A stream can terminate with a response.failed event that still reports
consumed tokens; those were rebuilt as None and logged at zero spend.
Parse response.failed alongside completed and incomplete, matching the
buffered path, which prices any terminal response that reports usage.
2026-08-11 21:01:18 -07:00
yucheng-berri
8bfb7772e4
fix(batches): attribute Anthropic passthrough batch cost to the creating key, team and tags (#36468)
The Anthropic batch create never persisted the creating key's hashed token or its
request tags on the managed object, so when CheckBatchCost billed the batch hours
later there was nothing to attribute it to. Key spend, key budgets and tag spend
never moved for batch usage.

Persist both from the create, the way the Vertex passthrough already does, and
register the batch only from the collection route. An id-scoped route cannot
rebuild the unified object id, because it embeds the model and the model comes
from the create's request body, so it could only claim a row it did not create or
fail the model_object_id unique constraint.

The shared metadata helpers, the route predicate and the registration-result
logging now live in batch_attribution instead of being copied per provider. The
Anthropic write previously logged success unconditionally, before the
fire-and-forget task had run.

Resolves LIT-5288
2026-08-11 20:52:43 -07:00
devin-ai-integration[bot]
7a55ca811b
fix(responses): init completed_response on bridge streaming iterator (#35413)
LiteLLMCompletionStreamingIterator overrides __init__ without calling
super().__init__(), so completed_response was only set once the stream
reached RESPONSE_COMPLETED. On a mid-stream provider error the router's
_extract_partial_responses_usage read source_iterator.completed_response
during fallback recovery and raised AttributeError, masking the real
provider error (e.g. Anthropic 529) and bypassing configured retries and
fallbacks. Initialize the attribute to None so recovery degrades to no
partial usage instead of crashing.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 03:45:34 +00:00
mateo-berri
2df121c821 fix(passthrough): bill streamed Responses calls that end incomplete
Streams that terminate with response.incomplete (e.g. max_output_tokens
reached) carry real usage in the terminal event but were rebuilt as None
and logged at zero spend, letting callers bypass budget enforcement.
Parse response.incomplete alongside response.completed when
reconstructing the streamed response.
2026-08-11 20:21:17 -07:00
mateo-berri
dc30e1816d refactor(passthrough): move Responses stream terminal-event parsing into OpenAI provider config
Addresses review feedback: the ResponseCompletedEvent SSE parsing now lives
in OpenAIResponsesAPIConfig next to the other Responses stream event handling,
and the proxy logging handler calls it. Adds coverage for streams that end
without a response.completed event.
2026-08-11 20:11:53 -07:00
Mateo Wang
b4f5e46a44
Merge pull request #30817 from geraint0923/litellm_fix_xai_web_search_cost_billing
Some checks are pending
Unit Tests: LLM Provider Transformations / All Other Providers (push) Waiting to run
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Proxy Legacy Tests / auth-and-jwt (push) Waiting to run
Unit Tests: Proxy Legacy Tests / key-generation (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-config (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-response-and-misc (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server-extras (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-token-counter (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-user-auth-and-spend (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-utils (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
fix(xai): bill web_search from server_side_tool_usage_details
2026-08-11 19:40:27 -07:00
mateo-berri
d96f76ca66 fix(cost-tracking): bill web searches reported only in server_side_tool_usage_details 2026-08-11 19:24:53 -07:00
Yuneng Jiang
5e094cd266
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/bedrock-retired-sonnet-test-model 2026-08-11 18:52:32 -07:00
Yuneng Jiang
d49114b101
test(bedrock): repoint live Claude tests off the retired Claude 3 Sonnet
AWS no longer serves `anthropic.claude-3-sonnet-20240229-v1:0`. The streaming
path returns a plain 404, "Model with the provided id
anthropic.claude-3-sonnet-20240229-v1:0 is not found", and the non-streaming
path answers 500 for the same reason. Our own cost map has carried a
2026-07-30 deprecation date for it since #36538

That accounts for 20 failures across local_testing_part1, local_testing_part2
and llm_translation_testing. litellm maps both statuses correctly, so the
tests are what went stale, not the client

Replacement is `us.anthropic.claude-sonnet-4-5-20250929-v1:0`: a like-for-like
Sonnet, and the newest Bedrock Sonnet this repo exercises against the real API
in tests/e2e. Newer ids exist in the cost map, but nothing in the repo calls
them live, so picking one would be an unverified guess about model access on
the CI account

Scope is limited to the tests that actually issue a request. The occurrences
that assert on the model string itself, or that feed mocked transformations,
keep the old id so their assertions stay meaningful
2026-08-11 18:49:47 -07:00
tin-berri
06943b6468
feat(router): make routing groups callable as virtual models and list them in /v1/models (#36519)
* feat(router): make routing groups callable as virtual models and list them in /v1/models

* fix(router): traffic-scoped cooldown exemption, live model_names on delete, group-info cache invalidation

* fix(router): share one recognized-model predicate across proxy gates, resolve aliases in group cooldown, read metadata via the dual-bucket owner

* fix(router): close the gate and cache families for callable groups, strip member access_groups from group rows, prove cooldown wiring end to end

* refactor(router): cache materialized group rows under the model-group cache owner and drop the redundant wiring test

* fix(router): warn-and-shadow on group name collisions, name-level test coverage for group helpers, faithful router doubles in a2a and cursor tests

* test(router): pin group cooldown metadata across the retry path
2026-08-11 18:41:19 -07:00
mateo-berri
6bce073520 fix(responses): keep chat-shaped usage extras from colliding in the bridge
Gemini image usage carries prompt_tokens and friends as extra fields on
ResponseAPIUsage, which collided with the bridge's explicit kwargs and
raised TypeError. Exclude keys the bridge already sets explicitly.
2026-08-11 18:34:27 -07:00
Yuneng Jiang
d1670fd52e
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/litellm-test-failures-debug-b4200d 2026-08-11 18:34:05 -07:00
Yuneng Jiang
5669742ea6
fix(model_prices): advertise native structured output on every Bedrock DeepSeek V3.2 and GLM 5 id
`supports_native_structured_output` was set only on the bare `deepseek.v3.2`
and `zai.glm-5` entries, so the cross-region inference profiles and the
region-pinned ids resolved to None. The flag gates the native
`outputConfig.textFormat` branch in BedrockConverseConfig, so callers
addressing the same model as `us.deepseek.v3.2` or
`bedrock/us-west-2/deepseek.v3.2` silently fell back to synthetic tool
injection. `us.` is the form Bedrock steers callers toward, so the most
common way to reach these models was the one missing the capability.

Adds the flag to the 12 affected ids and keeps the packaged backup in sync.

test_get_model_info_bedrock_models already caught the region-pinned ids, but
it filters on `litellm_provider == "bedrock"` and the cross-region profiles
carry `bedrock_converse`, so reverting just `us.deepseek.v3.2` and
`eu.deepseek.v3.2` left it green. The new parity test covers the prefixed
profiles and fails on exactly that mutation.
2026-08-11 18:30:18 -07:00
mubashir1osmani
4725cb4661
test(e2e): cover google-native generateContent framing and prometheus queue time (#34650)
* test(e2e): cover google-native generateContent framing and prometheus queue time

Adds live coverage for three shipped regressions that had none, all reached
through surfaces a customer drives from Google SDKs and operator dashboards.

The managed google-native route (`/v1beta/models/{model}:generateContent`) had
no harness support at all, so EndpointsClient gains generate_content and
stream_generate_content plus the request body models, and a new suite asserts
the two contracts that broke there: the response carries
x-litellm-response-cost so SDK traffic reconciles against spend (LIT-4076), and
the stream relays single-prefixed SSE frames with no OpenAI [DONE] terminator.
A doubled `data:` prefix, a leaked bytes literal, or the [DONE] sentinel each
fail the stream test; [DONE] absence is only asserted once real content has
arrived, because a first-chunk upstream error legitimately falls back to the
OpenAI error shape and does emit it.

The prometheus test pins litellm_request_queue_time_seconds to an actual
observation on our own key's series rather than to the family merely existing,
which is the distinction the original regression turned on: the histogram stayed
registered while nothing was ever written to it (LIT-2034).

Each assertion was mutation-checked against the live proxy; inverting the
[DONE] expectation, the cost-header expectation, or the metric name fails the
corresponding test.

* refactor(e2e): simplify google native coverage
2026-08-12 01:28:26 +00:00
mubashir1osmani
67643606ab
test(e2e): add reproducers for passthrough and model budget gaps (#34657)
* test(e2e): add failing reproducers for two open gateway bugs

Both tests assert the behavior a customer expects and both are red today. They
are reproducers, not regressions: the product is wrong, not the tests.

Native passthrough returns almost none of the operational headers the managed
route does. A /gemini/ generateContent call comes back with three x-litellm-*
headers and no x-ratelimit-* at all, against sixteen and four on
/v1beta/models/{m}:generateContent for the same prompt, and critically it omits
x-litellm-response-cost. Customers front provider-native traffic through this
route and read those headers to reconcile spend and pace themselves, so native
traffic is currently invisible to the tooling that covers every other route.

/budget/update rejects any model_max_budget with a 500. The reported symptom was
model ids containing dots, and that reproduces (prisma raises "Unexpected
`-5.2[FloatValue]` Expected `:`" because the key is interpolated into a GraphQL
query unquoted, so glm-5.2 lexes as an identifier followed by a float), but the
plain name gpt4o fails too, on a separate "model_max_budget should be of any of
the following types: Json" type mismatch at budget_management_endpoints.py:173.
Omitting the field returns 200. The test drives both names so the failure says
whether per-model budgets are broken outright or only for punctuated ids; today
it stops on the plain name, which is the wider bug.

* test(e2e): add reproducer for unenforced end-user per-model rate limits

model_max_budget accepts an rpm_limit alongside the spend cap, and /budget/new
stores it: the create response echoes {"gemini-2.5-flash": {"rpm_limit": 1,
"max_budget": 100.0, "budget_duration": "1d"}}. Attach that budget to an end
user, drive three calls as that user, and all three return 200. The limit is
accepted, persisted, and then ignored.

The same shape already works when the budget hangs off a key, which is what
makes this quietly dangerous: the API gives every indication the cap is in
force. A customer using it to hold one end user to a slow rate on a shared key
gets no throttling at all.

Harness additions this needs: ModelBudgetEntry carries the rpm_limit/tpm_limit
the route already accepts, BudgetNewBody and create_budget carry
model_max_budget, and create_customer can attach an existing budget_id rather
than only an inline max_budget.

Red today, for the reason in the assertion message.

* test(e2e): tighten model_max_budget reproducers and drop in-loop closure

Trim the reproducer docstrings to the contract they assert, keeping the
failure messages that document each red-by-design bug. Replace the nested
per-model closure in the /budget/update test with a module-level predicate
and a per-model helper so nothing closes over a loop variable, and fix the
import order the merge left unsorted.

* test(e2e): skip the three reproducers while their gateway bugs stay open

The passthrough header contract, /budget/update model_max_budget, and
end-user per-model rpm enforcement reproducers all still fail against
staging by design. Skip each with the product gap named so the combined
suite can gate merges on green while the collector keeps reporting the
cells as uncovered.

* test(e2e): validate model budget response contracts

* refactor(e2e): unify model budget schema

* refactor(e2e): reuse shared model budget type
2026-08-11 18:15:34 -07:00