Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion app/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,16 @@ async def _commit_row(*, retry: bool) -> None:
# be the resolved primary, or a cascaded fallback if the primary 404'd.
if isinstance(response, dict):
actual_resolved = response.get("model") or resolved_model
except HTTPException:
except HTTPException as exc:
# Defensive only: record the outcome so this arm can't leave the
# handler-local status_code at its initial 200, which would make the
# finally write a success-shaped row (cost ~0, no error_type) for a
# failed request. Unreachable today: the adapter blanket-translates
# every exception into UpstreamProviderError (packages/litellm_adapter/
# client.py, acompletion), so nothing in this try raises HTTPException.
# Kept because the sibling arms already record their status and this one
# would silently mis-log the first time that stops holding.
status_code = exc.status_code
raise
except UpstreamProviderError as exc:
status_code = exc.http_status
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/test_chat_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,42 @@ async def test_chat_completion_validation_error_for_empty_messages(chat_client):
assert r.json()["error"]["type"] == "validation_error"


async def test_chat_completion_blocking_httpexception_logs_real_status(chat_client):
"""The blocking path's `except HTTPException` arm records the raised status
instead of leaving the handler-local 200, so the finally cannot write a
success-shaped row for a failed request.

The exception is injected directly: the adapter translates every upstream
failure into UpstreamProviderError, so this shape is not reachable from real
traffic today. The test pins the handler's arm logic, not a live bug.
"""
from fastapi import HTTPException

client, fake = chat_client
fake.acompletion = AsyncMock(
side_effect=HTTPException(status_code=429, detail="local key rate limited")
)

r = await client.post(
"/v1/chat/completions",
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "hi"}],
},
)
assert r.status_code == 429

from sqlalchemy import select

from packages.db import session as session_mod
from packages.db.models.request_log import RequestLog

async with session_mod._session_factory() as s:
log = (await s.execute(select(RequestLog))).scalars().one()
assert log.status_code == 429
assert log.cost_microcents == 0


async def test_chat_completion_logs_active_strategy(chat_client):
"""RequestLog.routing_strategy reflects the configured strategy, not a
hardcoded value, and the same strategy is echoed in the response header."""
Expand Down
Loading