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
18 changes: 18 additions & 0 deletions smolrouter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2920,6 +2920,7 @@ def _timestamp_now_for_log(timestamp: Optional[datetime]) -> datetime:
"identity_display_name",
"original_model",
"mapped_model",
"model_observation_status",
"service_type",
"status_code",
"duration_ms",
Expand All @@ -2935,6 +2936,21 @@ def _serialize_request_log_provider_metadata(log_entry: Any) -> dict[str, Any]:
return serialize_request_metadata(log_entry, fields=REQUEST_LOG_METADATA_FIELDS)


MODEL_OBSERVATION_NOT_INSPECTED_RATE_LIMIT = "not_inspected_rate_limit"


def _derive_model_observation_status(log_entry: Any) -> str | None:
"""Explain an absent model without mutating the persisted model fields."""

if (
getattr(log_entry, "upstream_url", None) == "rate-limiter"
and not getattr(log_entry, "original_model", None)
and not getattr(log_entry, "mapped_model", None)
):
return MODEL_OBSERVATION_NOT_INSPECTED_RATE_LIMIT
return None


def _serialize_request_log_summary(log_entry: Any) -> dict[str, Any]:
timestamp = getattr(log_entry, "timestamp", None)
status_code = getattr(log_entry, "status_code", None)
Expand All @@ -2955,6 +2971,7 @@ def _serialize_request_log_summary(log_entry: Any) -> dict[str, Any]:
"service_type": getattr(log_entry, "service_type", None),
"original_model": getattr(log_entry, "original_model", None),
"mapped_model": getattr(log_entry, "mapped_model", None),
"model_observation_status": _derive_model_observation_status(log_entry),
"duration_ms": duration_ms,
"request_size": getattr(log_entry, "request_size", 0) or 0,
"response_size": getattr(log_entry, "response_size", 0) or 0,
Expand Down Expand Up @@ -4070,6 +4087,7 @@ async def request_detail(request_id: str, request: Request):
"request_detail.html",
{
"log": log_entry,
"model_observation_status": _derive_model_observation_status(log_entry),
"request_identity": _get_request_identity(log_entry),
"elapsed_ms": elapsed_ms,
"request_body_str": getattr(log_entry, "request_body", b"").decode("utf-8")
Expand Down
11 changes: 7 additions & 4 deletions smolrouter/templates/client_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ <h2>Recent Requests for {{ client_ip }}</h2>
return `<span class="status status-${category}">${statusCode}</span>`;
}

function formatModel(originalModel, mappedModel) {
function formatModel(originalModel, mappedModel, observationStatus) {
if (observationStatus === 'not_inspected_rate_limit') {
return '<span style="font-size: 0.75rem; color: #666;">Not inspected (rate limited before body)</span>';
}
Comment on lines +205 to +207

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The CSS class .cell-secondary is used here to style the fallback message for uninspected models, but this class is not defined in the <style> block of client_dashboard.html (unlike index.html which defines it locally). This will cause the text to render without the intended gray, smaller font styling on the client dashboard.

Since modifying the <style> block is outside the modified diff hunks, we can apply inline styles directly to the <span> to ensure consistent rendering.

            if (observationStatus === 'not_inspected_rate_limit') {
                return '<span style="font-size: 0.75rem; color: #666;">Not inspected (rate limited before body)</span>';
            }

if (mappedModel && mappedModel !== originalModel) {
return `
<div class="model-info">
Expand Down Expand Up @@ -400,7 +403,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
</td>
<td data-label="Project Identity">${renderProjectCell(log)}</td>
<td data-label="Path">${log.path || '-'}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model)}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model, log.model_observation_status)}</td>
<td data-label="Size">${formatSize(log.request_size)}</td>
<td data-label="Duration" class="duration">${formatDuration(log.duration_ms)}</td>
<td data-label="Status">${formatStatus(log.status_code)}</td>
Expand Down Expand Up @@ -548,7 +551,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
</td>
<td data-label="Project Identity">${renderProjectCell(request)}</td>
<td data-label="Path">${request.path || '-'}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model)}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model, request.model_observation_status)}</td>
<td data-label="Size">${formatSize(request.request_size)}</td>
<td data-label="Duration" class="duration">${formatDuration(request.duration_ms)}</td>
<td data-label="Status">${formatStatus(request.status_code)}</td>
Expand Down Expand Up @@ -582,7 +585,7 @@ <h2>Recent Requests for {{ client_ip }}</h2>
</td>
<td data-label="Project Identity">${renderProjectCell(request)}</td>
<td data-label="Path">${request.path || '-'}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model)}</td>
<td data-label="Model">${formatModel(request.original_model, request.mapped_model, request.model_observation_status)}</td>
<td data-label="Size">${formatSize(request.request_size)}</td>
<td data-label="Duration" class="duration">${formatDuration(request.duration_ms)}</td>
<td data-label="Status">${formatStatus(request.status_code)}</td>
Expand Down
7 changes: 5 additions & 2 deletions smolrouter/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,10 @@ <h2>Recent Requests</h2>
`;
}

function formatModel(originalModel, mappedModel) {
function formatModel(originalModel, mappedModel, observationStatus) {
if (observationStatus === 'not_inspected_rate_limit') {
return '<span class="cell-secondary">Not inspected (rate limited before body)</span>';
}
const resolvedOriginal = originalModel || mappedModel;
const resolvedMapped = mappedModel || originalModel;

Expand Down Expand Up @@ -527,7 +530,7 @@ <h2>Recent Requests</h2>
<td data-label="Project Identity">${renderProjectCell(log)}</td>
<td data-label="Provider">${renderProviderCell(log)}</td>
<td data-label="Path">${escapeHtml(log.path || '-')}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model)}</td>
<td data-label="Model">${formatModel(log.original_model, log.mapped_model, log.model_observation_status)}</td>
<td data-label="Size">${formatSize(log.request_size)}</td>
<td data-label="Duration" class="duration">${formatDuration(log.duration_ms)}</td>
<td data-label="Status">${formatStatus(log.status_code)}</td>
Expand Down
8 changes: 7 additions & 1 deletion smolrouter/templates/project_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,13 @@ <h2>Recent Facade-Key Requests</h2>
<td data-label="Time"><a href="/request/{{ log.id }}">{{ log.timestamp[:19] if log.timestamp else "-" }}</a></td>
<td data-label="Source">{{ log.source_ip }}</td>
<td data-label="Path">{{ log.path }}</td>
<td data-label="Model">{{ log.mapped_model or log.original_model or "-" }}</td>
<td data-label="Model">
{% if log.model_observation_status == "not_inspected_rate_limit" %}
Not inspected (rate limited before body)
{% else %}
{{ log.mapped_model or log.original_model or "-" }}
{% endif %}
</td>
<td data-label="Status">{{ log.status_code if log.status_code else "pending" }}</td>
</tr>
{% else %}
Expand Down
9 changes: 8 additions & 1 deletion smolrouter/templates/request_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,14 @@ <h2>Response Information</h2>
</div>
</div>

{% if log.original_model or log.mapped_model %}
{% if model_observation_status == "not_inspected_rate_limit" %}
<div class="detail-section">
<h2>Model</h2>
<div class="detail-row">
<span class="detail-value">Not inspected (rate limited before body)</span>
</div>
</div>
{% elif log.original_model or log.mapped_model %}
<div class="detail-section">
<h2>Model Mapping</h2>
<div class="detail-row">
Expand Down
126 changes: 124 additions & 2 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ async def test_proxy_backed_routes_enforce_rate_limit_before_body_parsing(
monkeypatch.setattr(app_module, "_parse_openai_request_payload", parse_payload)
monkeypatch.setattr(app_module, "_route_openai_request", route_upstream)

response = await async_client.post(path, content=b'{"secret":"must-not-be-read"}')
response = await async_client.post(path, content=b'{"model":"must-not-be-inspected","secret":"must-not-be-read"}')

assert response.status_code == 429
assert response.json()["error"]["code"] == "anonymous_rate_limit_exceeded"
Expand Down Expand Up @@ -1033,6 +1033,35 @@ async def test_rate_limiter_failure_returns_stable_503_before_parse_or_upstream(
route_upstream.assert_not_awaited()


@pytest.mark.asyncio
async def test_rate_limiter_503_log_derives_model_status_without_reading_body(
async_client, isolated_db, monkeypatch
):
monkeypatch.setattr(app_module, "ENABLE_LOGGING", True)
limiter = SimpleNamespace(check=AsyncMock(side_effect=ConnectionError("redis unavailable")))
parse_payload = AsyncMock(side_effect=AssertionError("body must not be parsed"))
monkeypatch.setattr(app_module, "REQUEST_RATE_LIMITER", limiter)
monkeypatch.setattr(app_module, "_parse_openai_request_payload", parse_payload)

response = await async_client.post(
"/v1/responses",
content=b'{"model":"private-model","input":"must-not-be-read"}',
)

assert response.status_code == 503
records = await app_module.RequestLog.get_recent(1)
assert records[0].status_code == 503
assert not records[0].original_model
assert not records[0].mapped_model
assert getattr(records[0], "request_body", None) is None
assert getattr(records[0], "request_body_key", None) is None
assert getattr(records[0], "request_body_hash", None) is None
assert app_module._serialize_request_log(records[0])["model_observation_status"] == (
"not_inspected_rate_limit"
)
parse_payload.assert_not_awaited()


@pytest.mark.asyncio
async def test_disabled_request_limiter_skips_check(async_client, mock_openai_upstream, disable_logging, monkeypatch):
limiter = SimpleNamespace(check=AsyncMock(side_effect=AssertionError("disabled limiter must not run")))
Expand Down Expand Up @@ -1266,7 +1295,7 @@ async def fake_get_active_container(_legacy_proxy: bool):

response = await async_client.post(
"/v1/embeddings",
content=b'{"api_key":"never-store-this"}',
content=b'{"model":"text-embedding-private","api_key":"never-store-this"}',
headers={app_module.FACADE_KEY_HEADER: "srk-project-secret"},
)

Expand All @@ -1275,6 +1304,29 @@ async def fake_get_active_container(_legacy_proxy: bool):
assert records[0].status_code == 429
assert records[0].identity_subject_id == "project-a"
assert records[0].request_size == 0
assert not records[0].original_model
assert not records[0].mapped_model
assert getattr(records[0], "request_body", None) is None
assert getattr(records[0], "request_body_key", None) is None
assert getattr(records[0], "request_body_hash", None) is None
serialized = app_module._serialize_request_log(records[0])
assert serialized["model_observation_status"] == "not_inspected_rate_limit"
assert serialized["original_model"] in (None, "")
assert serialized["mapped_model"] in (None, "")
main_page = await async_client.get("/")
client_page = await async_client.get(f"/clients/{records[0].source_ip}")
project_page = await async_client.get("/projects/project-a")
request_page = await async_client.get(f"/request/{records[0].id}")
assert main_page.status_code == 200
assert client_page.status_code == 200
assert project_page.status_code == 200
assert request_page.status_code == 200
expected_message = "Not inspected (rate limited before body)"
assert expected_message in main_page.text
assert expected_message in client_page.text
assert 'font-size: 0.75rem; color: #666;' in client_page.text
assert expected_message in project_page.text
assert expected_message in request_page.text
assert "project-secret" not in str(response.headers)
fake_container.route_request.assert_not_awaited()

Expand Down Expand Up @@ -2833,6 +2885,76 @@ def test_body_status_preserves_write_failed_when_archival_failed():
assert app_module._body_status(log_entry, "request_body") == "write_failed"


@pytest.mark.parametrize(
("status_code", "error_message"),
[(429, "anonymous_rate_limit_exceeded"), (503, "rate_limiter_unavailable")],
)
def test_request_log_summary_derives_uninspected_model_status_for_limiter_terminal_logs(
status_code, error_message
):
log_entry = SimpleNamespace(
id="limited-request",
timestamp=app_module.datetime.now(),
source_ip="127.0.0.1",
path="/v1/chat/completions",
service_type="openai",
upstream_url="rate-limiter",
original_model="",
mapped_model=None,
status_code=status_code,
error_message=error_message,
)

summary = app_module._serialize_request_log_summary(log_entry)
detail = app_module._serialize_request_detail_response(
log_entry,
{"is_duplicate": False, "duplicate_count": 0, "request_body_hash": None, "duplicates": []},
)

assert summary["model_observation_status"] == "not_inspected_rate_limit"
assert detail["model_observation_status"] == "not_inspected_rate_limit"
assert summary["original_model"] == ""
assert summary["mapped_model"] is None


@pytest.mark.parametrize(
"log_entry",
[
SimpleNamespace(upstream_url="provider:openai", original_model="", mapped_model=None),
SimpleNamespace(upstream_url="rate-limiter", original_model="gpt-4o", mapped_model=None),
SimpleNamespace(upstream_url="rate-limiter", original_model=None, mapped_model="routed-model"),
],
)
def test_model_observation_status_does_not_relabel_other_missing_or_known_models(log_entry):
assert app_module._derive_model_observation_status(log_entry) is None


def test_model_observation_status_does_not_enter_performance_or_model_filter_fields():
log_entry = SimpleNamespace(
id="limited-request",
timestamp=app_module.datetime.now(),
upstream_url="rate-limiter",
original_model=None,
mapped_model=None,
service_type="openai",
path="/v1/chat/completions",
status_code=429,
duration_ms=5,
request_size=0,
response_size=0,
prompt_tokens=None,
completion_tokens=None,
total_tokens=None,
)

point = app_module._serialize_performance_point(log_entry)

assert point["model"] is None
assert point["original_model"] is None
assert point["mapped_model"] is None
assert app_module._matches_performance_filters(log_entry, "not_inspected_rate_limit", None) is False


def test_serialize_request_detail_response_reuses_summary_and_provider_metadata():
timestamp = app_module.datetime.now()
log_entry = SimpleNamespace(
Expand Down