Skip to content

Commit b469096

Browse files
feat: expose safe router diagnostics
1 parent f3a8ab5 commit b469096

6 files changed

Lines changed: 772 additions & 20 deletions

File tree

docs/log_driven_quality_loop.md

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Log-Driven Quality Loop
22

3-
IntentMux improves routing quality from production metadata, optional local
4-
prompt review logs, external AI-assisted review, and redacted review samples.
5-
Route audit logs identify routing drift, low-confidence decisions, failures,
6-
and latency regressions; prompt review logs provide local-only semantic evidence
7-
when explicitly enabled. Only reviewed, redacted samples are promoted into eval
8-
cases or route banks.
3+
IntentMux uses production metadata to explain routing drift, low-confidence
4+
decisions, failures, and latency regressions. Prompt review logs are optional
5+
local-only evidence when explicitly enabled. Production logs, prompt review, and
6+
AI-assisted review are operational triage inputs, not routing-quality ground
7+
truth, and are not a standing pipeline for expanding eval cases or route banks.
98

109
`examples/eval_bank.sample.yaml` is the tracked public example for regression
1110
and baseline comparison. Generated `data/semantic_sets/eval_bank.yaml` is a
@@ -69,13 +68,9 @@ audit logs
6968
-> daily health / route summary / route-error budget
7069
-> review candidate selection
7170
-> optional local prompt review lookup by request_id
72-
-> AI review packet for an external reviewer
73-
-> human audit for escalations, uncertainty, and policy changes
74-
-> redacted production_review JSONL
75-
-> eval bank import
76-
-> route bank / threshold / margin change
77-
-> route quality report
78-
-> production rollout gate
71+
-> optional local AI review packet for operational triage
72+
-> public dataset regression report for any routing-policy change
73+
-> production rollout gate for bug fixes or explicitly justified changes
7974
-> observe new logs
8075
```
8176

@@ -91,8 +86,23 @@ uv run python scripts/router_log_summary.py /data/logs/routes/*.jsonl \
9186
--json
9287
```
9388

94-
Use `scripts/select_review_candidates.py` to select metadata-only records that
95-
deserve AI review and possible human audit:
89+
Directory inputs are auto-discovered for common runtime layouts such as
90+
`logs/routes/*.jsonl` and dated `cloud-route-audits/*/*.jsonl`; discovery is
91+
bounded with `--max-files` so cloud snapshots do not accidentally expand into
92+
unbounded full-history scans. JSON output includes low-risk `candidate_clusters`
93+
derived from route metadata only.
94+
95+
For a live process without log shipping, `/v1/intentmux/status` exposes safe
96+
runtime config shape and `/v1/intentmux/counters` exposes low-cardinality
97+
in-process counters. These endpoints are diagnostic surfaces only; they do not
98+
replace external monitoring, persistent route audit logs, or daily quality
99+
reports. In cloud mode they require IntentMux inbound auth and omit local paths,
100+
raw target model names, raw hard-rule keywords, prompts, responses, and keys.
101+
Outside cloud mode these diagnostic endpoints also require inbound auth whenever
102+
`ROUTER_INBOUND_API_KEY` or rotation keys are configured.
103+
104+
Use `scripts/select_review_candidates.py` to select metadata-only records for
105+
bounded operational triage:
96106

97107
```bash
98108
uv run python scripts/select_review_candidates.py /data/logs/routes/*.jsonl \
@@ -185,6 +195,10 @@ changes, but do not promote request structure alone into a `deep` route.
185195

186196
## AI Review Packet
187197

198+
AI review packets are local-only operational triage artifacts. They can help an
199+
operator summarize repeated failure clusters, but they are not labels and do
200+
not by themselves justify route-bank, threshold, margin, or hard-rule changes.
201+
188202
Generate a local-only packet for an external AI reviewer:
189203

190204
```bash
@@ -285,7 +299,8 @@ Any route bank, threshold, margin, or hard-rule change should include:
285299
in production or `examples/eval_bank.sample.yaml` in a clean clone;
286300
- route log summary from current-day or post-migration production traffic;
287301
- `scripts/route_quality_report.py` JSON/Markdown output;
288-
- candidate review evidence when the change is production-log driven;
302+
- public/reproducible dataset evidence for the behavior being changed;
303+
- candidate review evidence only as operational context, not ground truth;
289304
- rollback plan limited to IntentMux config, assets, or image.
290305

291306
Do not change LiteLLM config unless the failure is proven to be in the LiteLLM
@@ -298,8 +313,9 @@ IntentMux is ready to call itself log-driven when:
298313
- daily health and strict E2E run reliably against production;
299314
- review candidates are generated from mounted audit logs;
300315
- AI review packets and summaries are generated from mounted audit logs;
301-
- at least one accepted, redacted production review batch has entered eval;
302-
- route bank changes require a quality report;
316+
- private production review is clearly marked operational-only;
317+
- route bank changes require public/reproducible eval evidence and a quality
318+
report;
303319
- production rollout uses the documented gate and observes fresh logs after
304320
deployment.
305321

router/app.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import hashlib
34
import json
45
import logging
56
import secrets
@@ -14,6 +15,7 @@
1415
from router.observability import (
1516
AuditLogger,
1617
PromptReviewLogger,
18+
RouteCounters,
1719
configure_logging,
1820
log_route_complete,
1921
log_route_error,
@@ -81,6 +83,7 @@ def create_app(
8183
max_chars=settings.prompt_log_max_chars,
8284
)
8385
audit_metadata = route_audit_metadata(settings)
86+
route_counters = RouteCounters()
8487

8588
app = FastAPI(title="IntentMux")
8689

@@ -117,6 +120,11 @@ def require_inbound_auth(request: Request) -> JSONResponse | None:
117120
headers={"www-authenticate": "Bearer"},
118121
)
119122

123+
def require_diagnostic_auth(request: Request) -> JSONResponse | None:
124+
if settings.cloud_mode or settings.inbound_api_keys:
125+
return require_inbound_auth(request)
126+
return None
127+
120128
@app.get("/v1/models")
121129
async def models(request: Request) -> JSONResponse:
122130
auth_error = require_inbound_auth(request)
@@ -145,6 +153,20 @@ async def ready(request: Request) -> JSONResponse:
145153
status_code=200 if report.ready else 503,
146154
)
147155

156+
@app.get("/v1/intentmux/status")
157+
async def runtime_status(request: Request) -> JSONResponse:
158+
auth_error = require_diagnostic_auth(request)
159+
if auth_error is not None:
160+
return auth_error
161+
return JSONResponse(runtime_status_payload(settings))
162+
163+
@app.get("/v1/intentmux/counters")
164+
async def runtime_counters(request: Request) -> JSONResponse:
165+
auth_error = require_diagnostic_auth(request)
166+
if auth_error is not None:
167+
return auth_error
168+
return JSONResponse(route_counters.snapshot())
169+
148170
@app.post("/v1/intentmux/decision")
149171
async def route_decision(request: Request) -> Any:
150172
auth_error = require_inbound_auth(request)
@@ -235,6 +257,7 @@ async def chat_completions(request: Request) -> Response:
235257
format_signals=format_signals,
236258
audit_metadata=audit_metadata,
237259
audit_logger=audit_logger,
260+
route_counters=route_counters,
238261
)
239262
return upstream_error_response(
240263
request_id=request_id,
@@ -261,6 +284,7 @@ async def chat_completions(request: Request) -> Response:
261284
format_signals=format_signals,
262285
audit_metadata=audit_metadata,
263286
audit_logger=audit_logger,
287+
route_counters=route_counters,
264288
)
265289
await stream_context.__aexit__(None, None, None)
266290
return upstream_error_response(
@@ -288,6 +312,7 @@ async def chat_completions(request: Request) -> Response:
288312
format_signals=format_signals,
289313
audit_metadata=audit_metadata,
290314
audit_logger=audit_logger,
315+
route_counters=route_counters,
291316
),
292317
status_code=upstream.status_code,
293318
headers=headers,
@@ -314,6 +339,7 @@ async def chat_completions(request: Request) -> Response:
314339
format_signals=format_signals,
315340
audit_metadata=audit_metadata,
316341
audit_logger=audit_logger,
342+
route_counters=route_counters,
317343
)
318344
return upstream_error_response(
319345
request_id=request_id,
@@ -339,6 +365,7 @@ async def chat_completions(request: Request) -> Response:
339365
format_signals=format_signals,
340366
audit_metadata=audit_metadata,
341367
audit_logger=audit_logger,
368+
route_counters=route_counters,
342369
)
343370
return upstream_error_response(
344371
request_id=request_id,
@@ -364,6 +391,7 @@ async def chat_completions(request: Request) -> Response:
364391
audit_metadata=audit_metadata,
365392
usage=usage_from_response_content(upstream.content),
366393
audit_logger=audit_logger,
394+
route_counters=route_counters,
367395
)
368396
return Response(
369397
content=upstream.content,
@@ -401,6 +429,117 @@ def route_audit_metadata(settings: RouterSettings) -> dict[str, str]:
401429
return metadata
402430

403431

432+
def runtime_status_payload(settings: RouterSettings) -> dict[str, Any]:
433+
return {
434+
"cloud_mode": settings.cloud_mode,
435+
"config": runtime_config_status(settings),
436+
"routing": runtime_routing_status(settings),
437+
"routes": runtime_route_status(settings),
438+
"hard_rules": runtime_hard_rule_status(settings),
439+
"warnings": runtime_status_warnings(settings),
440+
}
441+
442+
443+
def runtime_config_status(settings: RouterSettings) -> dict[str, Any]:
444+
payload: dict[str, Any] = {
445+
"config_source": settings.config_source,
446+
"config_sha256": settings.config_sha256,
447+
"route_bank_sha256": settings.route_bank_sha256,
448+
"runtime_config_exists": settings.runtime_config_exists,
449+
"route_bank_loaded": settings.route_bank_loaded,
450+
"audit_log_enabled": settings.audit_log_enabled,
451+
"access_log": settings.access_log,
452+
"prompt_log_mode": settings.prompt_log_mode,
453+
}
454+
if not settings.cloud_mode:
455+
payload.update(
456+
{
457+
"config_path": settings.config_path,
458+
"runtime_home": settings.runtime_home,
459+
"audit_log_dir": settings.audit_log_dir,
460+
"prompt_log_dir": settings.prompt_log_dir,
461+
"route_bank_path": settings.route_bank_path,
462+
}
463+
)
464+
return payload
465+
466+
467+
def runtime_routing_status(settings: RouterSettings) -> dict[str, Any]:
468+
return {
469+
"entry_model": settings.entry_model,
470+
"entry_model_aliases": sorted(settings.entry_model_aliases),
471+
"fallback_route_id": settings.fallback_route_id,
472+
"route_id_aliases": dict(sorted(settings.route_id_aliases.items())),
473+
"route_kernel": settings.route_kernel,
474+
"aurelio_router": settings.aurelio_router,
475+
"aurelio_hybrid_alpha": settings.aurelio_hybrid_alpha,
476+
"threshold": settings.threshold,
477+
"margin": settings.margin,
478+
"agent_signal_enabled": settings.agent_signal_enabled,
479+
"agent_signal_route_id": settings.effective_agent_signal_route_id,
480+
"agent_signal_min_input_chars": settings.agent_signal_min_input_chars,
481+
"agent_signal_min_message_count": settings.agent_signal_min_message_count,
482+
}
483+
484+
485+
def runtime_route_status(settings: RouterSettings) -> dict[str, Any]:
486+
return {
487+
route_id: runtime_route_entry(route_id, route.target_model, len(route.utterances), settings)
488+
for route_id, route in sorted(settings.routes.items())
489+
}
490+
491+
492+
def runtime_route_entry(
493+
route_id: str,
494+
target_model: str | None,
495+
utterance_count: int,
496+
settings: RouterSettings,
497+
) -> dict[str, Any]:
498+
entry: dict[str, Any] = {
499+
"utterance_count": utterance_count,
500+
"target_model_configured": bool(target_model),
501+
}
502+
if not settings.cloud_mode:
503+
entry["target_model"] = target_model
504+
else:
505+
entry["target_model_sha256"] = stable_sha256(target_model)
506+
return entry
507+
508+
509+
def runtime_hard_rule_status(settings: RouterSettings) -> list[dict[str, Any]]:
510+
rows: list[dict[str, Any]] = []
511+
for hard_rule in settings.hard_rules:
512+
row: dict[str, Any] = {
513+
"route_id": hard_rule.route_id,
514+
"keyword_count": len(hard_rule.keywords),
515+
}
516+
if settings.cloud_mode:
517+
row["keyword_sha256s"] = [
518+
stable_sha256(keyword) for keyword in hard_rule.keywords
519+
]
520+
else:
521+
row["keywords"] = list(hard_rule.keywords)
522+
rows.append(row)
523+
return rows
524+
525+
526+
def runtime_status_warnings(settings: RouterSettings) -> list[str]:
527+
warnings: list[str] = []
528+
if settings.config_source == "repo_default" and not settings.runtime_config_exists:
529+
warnings.append("runtime_config_missing")
530+
if settings.placeholder_target_models:
531+
warnings.append("placeholder_targets")
532+
if settings.cloud_mode and settings.prompt_log_mode == "off":
533+
warnings.append("prompt_review_log_disabled")
534+
return warnings
535+
536+
537+
def stable_sha256(value: str | None) -> str | None:
538+
if not value:
539+
return None
540+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
541+
542+
404543
def usage_from_response_content(content: bytes) -> dict[str, int] | None:
405544
try:
406545
payload = json.loads(content)
@@ -434,6 +573,7 @@ async def stream_with_context(
434573
format_signals: dict[str, Any] | None = None,
435574
audit_metadata: dict[str, Any] | None = None,
436575
audit_logger: AuditLogger | None = None,
576+
route_counters: RouteCounters | None = None,
437577
):
438578
if upstream_started_ms is None:
439579
upstream_started_ms = started_ms

0 commit comments

Comments
 (0)