Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1b7cbe0
test(ci): add used_client_oauth_token to the GCS pub/sub spend-log go…
yuneng-berri Oct 1, 2026
b87dd5a
test(ui): give the auto-router threshold save wait room for the avail…
yuneng-berri Oct 1, 2026
8516a1b
test(e2e): expect no pricing tier on bills for streamed calls OpenAI …
yuneng-berri Oct 1, 2026
0a0ca34
test(e2e-ui): wait for the call-id search before hovering the logs row
yuneng-berri Oct 1, 2026
60106d2
test(e2e): run the Together structured-output case on the hybrid Qwen…
yuneng-berri Oct 1, 2026
3de5cda
Merge remote-tracking branch 'origin/main' into litellm_ci_stale_test…
yuneng-berri Oct 1, 2026
67ec099
test(integration): read the agent 365 guardrail status by its own nam…
yuneng-berri Oct 1, 2026
a1b141d
test(unit): ignore asyncio's leaked-task records in the budget limite…
yuneng-berri Oct 1, 2026
2684cd6
test(e2e-ui): fill the create-tag fields inside the dialog
yuneng-berri Oct 1, 2026
a9b283a
test(integration): run integration proxies with the CI license
yuneng-berri Oct 1, 2026
59d8e01
ci: save GitHub Actions caches only from main and bump codecov-action…
yuneng-berri Oct 1, 2026
0186784
test(unit): join the session-minting thread before collecting the han…
yuneng-berri Oct 1, 2026
6fe41e9
test(integration): relaunch owned proxies that lose their port, expir…
yuneng-berri Oct 1, 2026
46b162e
ci(circleci): give the base SDK wheel build the same 30 minute no-out…
yuneng-berri Oct 1, 2026
25a7f0d
test(integration): model the budget-reset database outage as 10 secon…
yuneng-berri Oct 1, 2026
a5dfeba
ci: move the unit-test uv cache split into a composite action
yuneng-berri Oct 1, 2026
cad59af
Merge remote-tracking branch 'origin/main' into litellm_ci_stale_test…
yuneng-berri Oct 1, 2026
92e1ab1
test(unit): point tiktoken at the bundled cache for every unit test
yuneng-berri Oct 1, 2026
8325201
test(integration): answer model discovery probes in the hosted_vllm w…
yuneng-berri Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions tests/e2e/llm_translation/test_together_ai_e2e.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Live e2e: Together AI through the gateway on /chat/completions and /v1/messages.

The reasoning and tool-calling backend is the cheapest live ``together_ai/`` chat row
in the proxy's own cost map that carries both capability flags; the structured-output
and cache-pricing backends are likewise the cheapest rows carrying
``supports_response_schema`` and a ``cache_read_input_token_cost``. Two backends are
pinned because the registry has no flag for what they prove: ``enable_thinking`` and
the ``{"reasoning": {"enabled": false}}`` toggle that ``reasoning_effort="none"`` maps
to are Qwen hybrid-model contracts, and MiniMax-M3 is the serverless model whose
in the proxy's own cost map that carries both capability flags; the cache-pricing
backend is likewise the cheapest row carrying a ``cache_read_input_token_cost``. Two
backends are pinned because the registry has no flag for what they prove: ``enable_thinking``
and the ``{"reasoning": {"enabled": false}}`` toggle that ``reasoning_effort="none"`` maps
to are Qwen hybrid-model contracts, and the structured-output case runs on that hybrid
model with reasoning off, since a reasoning-only model can spend the whole token budget
thinking and return no content. MiniMax-M3 is the serverless model whose
template renders a replayed ``reasoning_content`` back into the prompt (Qwen and
DeepSeek silently drop it). MiniMax-M3 honors that replayed field on nearly every call, not
every call (one miss in dozens of otherwise identical calls), so the replay case asks
Expand Down Expand Up @@ -109,7 +110,6 @@
class _Needs:
function_calling: bool = False
reasoning: bool = False
response_schema: bool = False
cache_read_pricing: bool = False


Expand Down Expand Up @@ -172,7 +172,6 @@ def qualifies(name: str, entry: CostMapEntry) -> bool:
and (entry.output_cost_per_token or 0.0) > 0
and (not needs.function_calling or bool(entry.supports_function_calling))
and (not needs.reasoning or bool(entry.supports_reasoning))
and (not needs.response_schema or bool(entry.supports_response_schema))
and (not needs.cache_read_pricing or (entry.cache_read_input_token_cost or 0.0) > 0)
)

Expand Down Expand Up @@ -586,10 +585,9 @@ def ask(reasoning_effort: str | None) -> OutMessage:

@pytest.mark.covers("llm.chat_completions.together_ai.structured_output.nonstream.works")
def test_response_format_json_schema_shapes_the_reply(
self, client: PassthroughClient, resources: ResourceManager, registry: dict[str, CostMapEntry]
self, client: PassthroughClient, resources: ResourceManager
) -> None:
backend = _cheapest_together_chat_model(registry, _Needs(response_schema=True))
model, key = _register(client, resources, backend)
model, key = _register(client, resources, HYBRID_REASONING_BACKEND)

message = _message(
unwrap(
Expand All @@ -599,12 +597,13 @@ def test_response_format_json_schema_shapes_the_reply(
model=model,
messages=[ChatMessage(role="user", content=PERSON_PROMPT)],
max_tokens=1024,
reasoning_effort="none",
response_format=PERSON_RESPONSE_FORMAT,
),
)
)
)
assert message.content, f"{backend} returned no content: {message}"
assert message.content, f"{HYBRID_REASONING_BACKEND} returned no content: {message}"
person = _Person.model_validate_json(message.content)
assert person.name, f"schema-shaped reply carries an empty name: {message.content!r}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
The streaming cases pin the served-tier contract: OpenAI stamps the tier it actually
used on every stream chunk, and that echo is what the caller sees and what the bill
must be computed on. The request sets no service_tier, so the only place the tier
can come from is the provider's response. The spend row must record the served tier
and price input at that tier's rate, and every chunk the proxy relays must carry the
same service_tier the provider sent.
can come from is the provider's response. The spend row must record the tier the bill
was priced on and price input at that tier's rate, and every chunk the proxy relays must
carry the same service_tier the provider sent. A served `default` tier is base pricing,
which the bill records as no tier
"""

import json
Expand Down Expand Up @@ -61,7 +62,8 @@

REASONING_EFFORT = "high"

TIER_INPUT_RATES = {"default": INPUT_RATE, "priority": PRIORITY_INPUT_RATE}
PRICING_BASIS_FOR_SERVED_TIER: dict[str, str | None] = {"default": None, "priority": "priority"}
INPUT_RATE_FOR_PRICING_BASIS: dict[str | None, float] = {None: INPUT_RATE, "priority": PRIORITY_INPUT_RATE}


class _StreamChunk(BaseModel):
Expand Down Expand Up @@ -213,17 +215,20 @@ def test_streamed_call_records_and_bills_the_served_tier(
)
chunks = _stream_chunks(result.stream_events)
served_tier = _served_tier(chunks)
assert served_tier in TIER_INPUT_RATES, f"no custom rate registered for served tier {served_tier!r}"
assert served_tier in PRICING_BASIS_FOR_SERVED_TIER, (
f"no custom rate registered for served tier {served_tier!r}"
)
pricing_basis = PRICING_BASIS_FOR_SERVED_TIER[served_tier]
stream_id = chunks[0].id
assert stream_id, f"first stream chunk carried no id: {result.stream_events[0][:200]}"

row = poll_cost_row(client.proxy, stream_id)
assert row is not None, f"no spend row with a cost breakdown landed for {stream_id}"
assert row.breakdown.service_tier == served_tier, (
f"the provider served tier {served_tier!r} on every chunk but the bill records "
f"pricing basis {row.breakdown.service_tier!r}"
assert row.breakdown.service_tier == pricing_basis, (
f"the provider served tier {served_tier!r} on every chunk, so the bill should record pricing "
f"basis {pricing_basis!r}, but it records {row.breakdown.service_tier!r}"
)
assert_fresh_tokens_billed_at(row, TIER_INPUT_RATES[served_tier])
assert_fresh_tokens_billed_at(row, INPUT_RATE_FOR_PRICING_BASIS[pricing_basis])
assert_total_is_sum_of_components(row)

@pytest.mark.covers("llm.chat_completions.openai.service_tier.stream.echoes_served_tier")
Expand Down Expand Up @@ -264,7 +269,14 @@ def test_responses_stream_records_the_served_tier(
client.proxy,
resources,
"tier-responses-stream",
LiteLLMParamsBody(model=STREAM_BACKEND, api_key=OPENAI_API_KEY),
LiteLLMParamsBody(
model=STREAM_BACKEND,
api_key=OPENAI_API_KEY,
input_cost_per_token=INPUT_RATE,
output_cost_per_token=OUTPUT_RATE,
input_cost_per_token_priority=PRIORITY_INPUT_RATE,
output_cost_per_token_priority=PRIORITY_OUTPUT_RATE,
),
)

result = client.proxy.responses_stream(
Expand All @@ -282,14 +294,18 @@ def test_responses_stream_records_the_served_tier(
)
served_tier = completed.response.service_tier
assert served_tier, f"response.completed carried no service_tier: {completed.response}"
assert served_tier in TIER_INPUT_RATES, f"no custom rate registered for served tier {served_tier!r}"
assert served_tier in PRICING_BASIS_FOR_SERVED_TIER, (
f"no custom rate registered for served tier {served_tier!r}"
)
pricing_basis = PRICING_BASIS_FOR_SERVED_TIER[served_tier]

row = poll_cost_row_where(client.proxy, scoped_key, lambda r: r.spend is not None and r.spend > 0)
assert row is not None, f"no spend row with a cost breakdown landed for the streamed responses call on {model}"
assert row.breakdown.service_tier == served_tier, (
f"response.completed served tier {served_tier!r} but the bill records "
f"pricing basis {row.breakdown.service_tier!r}"
assert row.breakdown.service_tier == pricing_basis, (
f"response.completed served tier {served_tier!r}, so the bill should record pricing basis "
f"{pricing_basis!r}, but it records {row.breakdown.service_tier!r}"
)
assert_fresh_tokens_billed_at(row, INPUT_RATE_FOR_PRICING_BASIS[pricing_basis])

@pytest.mark.covers("quota_management.spend_tracking.service_tier_stream.messages_records_served_tier")
def test_messages_stream_records_the_served_tier(
Expand All @@ -299,7 +315,14 @@ def test_messages_stream_records_the_served_tier(
client.proxy,
resources,
"tier-messages-stream",
LiteLLMParamsBody(model=STREAM_BACKEND, api_key=OPENAI_API_KEY),
LiteLLMParamsBody(
model=STREAM_BACKEND,
api_key=OPENAI_API_KEY,
input_cost_per_token=INPUT_RATE,
output_cost_per_token=OUTPUT_RATE,
input_cost_per_token_priority=PRIORITY_INPUT_RATE,
output_cost_per_token_priority=PRIORITY_OUTPUT_RATE,
),
)

result = client.proxy.messages_stream(
Expand All @@ -322,8 +345,9 @@ def test_messages_stream_records_the_served_tier(

row = poll_cost_row_where(client.proxy, scoped_key, lambda r: r.spend is not None and r.spend > 0)
assert row is not None, f"no spend row with a cost breakdown landed for the streamed messages call on {model}"
served_tier = row.breakdown.service_tier
assert served_tier in TIER_INPUT_RATES and served_tier is not None, (
pricing_basis = row.breakdown.service_tier
assert pricing_basis in INPUT_RATE_FOR_PRICING_BASIS, (
"the anthropic wire format carries no service_tier, so the bill is the only record of "
f"the tier OpenAI served; the row recorded pricing basis {served_tier!r}"
f"the tier OpenAI served; the row recorded pricing basis {pricing_basis!r}"
)
assert_fresh_tokens_billed_at(row, INPUT_RATE_FOR_PRICING_BASIS[pricing_basis])
8 changes: 8 additions & 0 deletions tests/e2e/ui/tests/logs/logs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ test.describe("Logs page", () => {
await dismissFeedbackPopup(page);
const search = visibleTestId(page, "datatable-search");
await expect(search).toBeVisible({ timeout: 20_000 });
const searched = page.waitForResponse(
(response) =>
response.url().includes("/spend/logs/ui") &&
new URL(response.url()).searchParams.get("search") === callId &&
response.status() === 200,
{ timeout: 20_000 },
);
await search.fill(callId);
await searched;

const row = requestLogsRows(page).filter({ hasText: requestId });
await expect(row, `no logs row for call id ${callId}`).toHaveCount(1, { timeout: 30_000 });
Expand Down
9 changes: 4 additions & 5 deletions tests/e2e/ui/tests/tagManagement/tagManagement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ test.describe("Tag management", () => {
async () => {
await navigateToPage(page, DashboardPage.TagManagement);
await page.getByRole("button", { name: "+ Create New Tag" }).click();
await expect(
page.getByRole("dialog", { name: "Create New Tag" }),
).toBeVisible();
await page.getByLabel("Tag Name").fill(tagName);
await page.getByLabel("Description").fill(description);
const createDialog = page.getByRole("dialog", { name: "Create New Tag" });
await expect(createDialog).toBeVisible();
await createDialog.getByLabel("Tag Name").fill(tagName);
await createDialog.getByLabel("Description").fill(description);
await page.getByRole("button", { name: "Create Tag" }).click();

await expect
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/mcp/test_mcp_agent_365_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
TENANT: Final = "00000000-0000-4000-8000-0000000a3650"
REJECTED: Final = "Agent 365 guardrail rejected the tool call"
GUARDRAIL_ROWS: Final = (
"SELECT metadata->'guardrail_information' AS gi FROM \"LiteLLM_SpendLogs\" "
'WHERE api_key = %s AND call_type = %s ORDER BY "startTime"'
"SELECT COALESCE(jsonb_path_query_first(metadata, "
"'$.guardrail_information[*] ? (@.guardrail_name == $name).guardrail_status', "
"jsonb_build_object('name', %s::text)) #>> '{}', 'none') AS status "
'FROM "LiteLLM_SpendLogs" WHERE api_key = %s AND call_type = %s ORDER BY "startTime"'
)
FALLBACKS: Final = (None, "fail_open", "fail_closed")

Expand Down Expand Up @@ -95,11 +97,11 @@ def upstream_tool_names(self) -> tuple[str, ...]:

def guardrail_statuses(self, call_type: str, at_least: int) -> list[str]:
rows: Final = eventually(
lambda: read_rows(GUARDRAIL_ROWS, (sha256(self.key.encode()).hexdigest(), call_type)),
lambda: read_rows(GUARDRAIL_ROWS, (self.alias, sha256(self.key.encode()).hexdigest(), call_type)),
lambda seen: len(seen) >= at_least,
seconds=70,
)
return [row["gi"][0]["guardrail_status"] if row["gi"] else "none" for row in rows]
return [str(row["status"]) for row in rows]


@contextmanager
Expand Down
Loading
Loading