-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
test(e2e): pin regional cost-map uplift and rate-limit attribution #34652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mubashir1osmani
wants to merge
1
commit into
BerriAI:litellm_internal_staging
Choose a base branch
from
mubashir1osmani:litellm_e2e_shipped_regression_net
base: litellm_internal_staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| """Live e2e: regional model variants resolve their own uplifted cost-map pricing. | ||
|
|
||
| Bedrock publishes a regional variant of each Claude model (`us.`, `eu.`) priced | ||
| above the `global.` variant, and the cost map carries all three. The mappings | ||
| regressed so regional deployments resolved the global rate (LIT-3912), which | ||
| silently under-bills every regional call and makes a customer's cost reporting | ||
| disagree with their AWS invoice. | ||
|
|
||
| `/model/info` is where the resolved rate is observable without spending money on | ||
| a call, so this registers the three variants of one model and pins each rate. It | ||
| asserts the exact expected uplift rather than merely `regional > global`: a | ||
| mapping that resolved regional to some other model's rate would still satisfy an | ||
| inequality while being just as wrong. | ||
|
|
||
| No provider call is made, so this needs no AWS credentials. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from e2e_config import unique_marker | ||
| from endpoints_client import EndpointsClient | ||
| from lifecycle import ResourceManager | ||
| from models import LiteLLMParamsBody, ModelInfoEntry | ||
|
|
||
| pytestmark = pytest.mark.e2e | ||
|
|
||
| BASE_MODEL = "anthropic.claude-opus-4-7" | ||
| GLOBAL_INPUT_COST = 5e-06 | ||
| GLOBAL_OUTPUT_COST = 2.5e-05 | ||
| REGIONAL_UPLIFT = 1.1 | ||
| RELATIVE_TOLERANCE = 1e-9 | ||
|
|
||
|
|
||
| def _matches(actual: float, expected: float) -> bool: | ||
| """Float-safe equality for rates; the uplift arithmetic is not exact in binary.""" | ||
| return abs(actual - expected) <= abs(expected) * RELATIVE_TOLERANCE | ||
|
|
||
|
|
||
| def _register( | ||
| client: EndpointsClient, resources: ResourceManager, prefix: str | ||
| ) -> str: | ||
| model = f"e2e-regional-{prefix}-{unique_marker()}" | ||
| model_id = client.create_model( | ||
| model, | ||
| LiteLLMParamsBody( | ||
| model=f"bedrock/{prefix}.{BASE_MODEL}", aws_region_name="us-east-1" | ||
| ), | ||
| ) | ||
| resources.defer(lambda: client.delete_model(model_id)) | ||
| return model | ||
|
|
||
|
|
||
| def _entry(entries: list[ModelInfoEntry], model_name: str) -> ModelInfoEntry: | ||
| for entry in entries: | ||
| if entry.model_name == model_name: | ||
| return entry | ||
| pytest.fail(f"{model_name} absent from /model/info; the deployment did not register") | ||
|
|
||
|
|
||
| def _resolved_costs(client: EndpointsClient, model: str) -> tuple[float, float]: | ||
| """The rate the proxy resolved from the cost map, with no override configured.""" | ||
| resolved = _entry(client.proxy.model_info(), model).model_info | ||
| assert resolved.input_cost_per_token is not None, ( | ||
| f"{model}: /model/info resolved no input_cost_per_token, so the deployment " | ||
| f"matched no cost-map entry at all" | ||
| ) | ||
| assert resolved.output_cost_per_token is not None, ( | ||
| f"{model}: /model/info resolved no output_cost_per_token" | ||
| ) | ||
| return resolved.input_cost_per_token, resolved.output_cost_per_token | ||
|
|
||
|
|
||
| class TestRegionalUpliftPricing: | ||
| @pytest.mark.covers("mgmt.model.info.reports_regional_uplift") | ||
| def test_regional_variants_price_above_global( | ||
| self, endpoints_client: EndpointsClient, resources: ResourceManager | ||
| ) -> None: | ||
| global_model = _register(endpoints_client, resources, "global") | ||
| global_input, global_output = _resolved_costs(endpoints_client, global_model) | ||
|
|
||
| assert _matches(global_input, GLOBAL_INPUT_COST), ( | ||
| f"global variant input rate drifted from the cost map: " | ||
| f"expected {GLOBAL_INPUT_COST}, got {global_input}" | ||
| ) | ||
| assert _matches(global_output, GLOBAL_OUTPUT_COST), ( | ||
| f"global variant output rate drifted from the cost map: " | ||
| f"expected {GLOBAL_OUTPUT_COST}, got {global_output}" | ||
| ) | ||
|
|
||
| for prefix in ("us", "eu"): | ||
| model = _register(endpoints_client, resources, prefix) | ||
| input_cost, output_cost = _resolved_costs(endpoints_client, model) | ||
|
|
||
| assert _matches(input_cost, global_input * REGIONAL_UPLIFT), ( | ||
| f"{prefix}. variant input rate {input_cost} is not the expected " | ||
| f"{REGIONAL_UPLIFT}x uplift over the global rate {global_input}; a " | ||
| f"regional deployment billed at the global rate under-charges every call" | ||
| ) | ||
| assert _matches(output_cost, global_output * REGIONAL_UPLIFT), ( | ||
| f"{prefix}. variant output rate {output_cost} is not the expected " | ||
| f"{REGIONAL_UPLIFT}x uplift over the global rate {global_output}" | ||
| ) |
102 changes: 102 additions & 0 deletions
102
tests/e2e/logging/test_prometheus_ratelimit_failure_e2e.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """Live e2e: a rate-limit rejection is attributed to the gateway, not the vendor. | ||
|
|
||
| Operators page on 429s, and the response to one depends entirely on who produced | ||
| it: a vendor 429 means back off or fail over, while a gateway 429 means the | ||
| customer's own key limit is set too low. `litellm_proxy_failed_requests_metric` | ||
| has to make that difference readable, and it has to name the provider, otherwise | ||
| a multi-provider deployment cannot tell which upstream is saturating (PR #27687). | ||
|
|
||
| The two are distinguished by `exception_class`: the gateway's own limiter raises | ||
| `HTTPException`, while a vendor rejection surfaces its provider error class (for | ||
| example `Openai.RateLimitError`). This drives a key over its own RPM limit, which | ||
| is unambiguously a gateway-side rejection, and requires the resulting series to | ||
| be labelled that way and to carry a provider. | ||
|
|
||
| The metric is written on the failure-logging callback, so the scrape polls to a | ||
| deadline rather than sleeping once. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import time | ||
|
|
||
| import pytest | ||
| from prometheus_client.parser import text_string_to_metric_families | ||
|
|
||
| from e2e_config import unique_marker | ||
| from lifecycle import ResourceManager | ||
| from logging_client import LoggingClient | ||
| from models import KeyGenerateBody | ||
|
|
||
| pytestmark = pytest.mark.e2e | ||
|
|
||
| DRIVER_MODEL = "gemini-2.5-flash" | ||
| FAILURE_METRIC = "litellm_proxy_failed_requests_metric_total" | ||
| GATEWAY_EXCEPTION_CLASS = "HTTPException" | ||
| RPM_LIMIT = 1 | ||
| CALLS = 3 | ||
|
|
||
|
|
||
| def _rate_limited_series(exposition: str, alias: str) -> tuple[dict[str, str], ...]: | ||
| """Every 429 sample on the failure metric belonging to our key.""" | ||
| return tuple( | ||
| sample.labels | ||
| for family in text_string_to_metric_families(exposition) | ||
| for sample in family.samples | ||
| if sample.name == FAILURE_METRIC | ||
| and sample.labels.get("api_key_alias") == alias | ||
| and sample.labels.get("exception_status") == "429" | ||
| ) | ||
|
|
||
|
|
||
| class TestPrometheusRateLimitAttribution: | ||
| @pytest.mark.covers("logging.prometheus.failure.attributes_rate_limit_source") | ||
| def test_gateway_rate_limit_is_labelled_with_source_and_provider( | ||
| self, client: LoggingClient, resources: ResourceManager | ||
| ) -> None: | ||
| alias = f"e2e-rl-attr-{unique_marker()}" | ||
| key = client.proxy.generate_key( | ||
| KeyGenerateBody( | ||
| key_alias=alias, | ||
| models=[DRIVER_MODEL], | ||
| user_id=f"e2e-{alias}", | ||
| rpm_limit=RPM_LIMIT, | ||
| ) | ||
| ) | ||
| resources.defer(lambda: client.delete_key(key)) | ||
|
|
||
| statuses = tuple( | ||
| client.chat_raw(key, DRIVER_MODEL, f"say ok {unique_marker()}").status_code | ||
| for _ in range(CALLS) | ||
| ) | ||
| assert 429 in statuses, ( | ||
| f"driving {CALLS} calls against an rpm_limit of {RPM_LIMIT} produced no 429; " | ||
| f"statuses were {statuses}, so the limiter never rejected and there is " | ||
| f"nothing for the metric to attribute" | ||
| ) | ||
|
|
||
| deadline = time.monotonic() + client.proxy.poll_timeout | ||
| series: tuple[dict[str, str], ...] = () | ||
| while time.monotonic() < deadline: | ||
| series = _rate_limited_series(client.scrape_metrics(), alias) | ||
| if series: | ||
| break | ||
| time.sleep(client.proxy.poll_interval) | ||
|
|
||
| assert series, ( | ||
| f"{FAILURE_METRIC} has no 429 series for api_key_alias={alias}; a rate-limit " | ||
| f"rejection that is never counted cannot be alerted on" | ||
| ) | ||
|
|
||
| classes = {labels.get("exception_class") for labels in series} | ||
| assert classes == {GATEWAY_EXCEPTION_CLASS}, ( | ||
| f"a gateway rpm rejection must be attributed to the gateway as " | ||
| f"{GATEWAY_EXCEPTION_CLASS}, got {sorted(c or '' for c in classes)}; " | ||
| f"a vendor error class here would send operators chasing the upstream" | ||
| ) | ||
|
|
||
| providers = {labels.get("api_provider") for labels in series} | ||
| assert providers and all(providers), ( | ||
| f"{FAILURE_METRIC} 429 series carries an empty api_provider ({providers}); " | ||
| f"a multi-provider deployment cannot tell which upstream the rejection was for" | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The status assertion only requires one 429, so the test can validate the gateway labels even when the initial provider request fails. Assert that the first response succeeds before checking that later requests exceed the key's RPM limit; otherwise the advertised end-to-end provider scenario remains unverified.