{ "task": 7, "title": "Make telemetry optional per-provider and replace global account-refusal tracking with per-provider tracking", "file": "src/dispatcher.py", "changes": [ "extract_telemetry({}) now defends against non-dict payloads and returns Telemetry() with all None fields", "_sniff_telemetry_line(\": OPENROUTER PROCESSING\") returns None by rejecting any SSE comment whose first word is not 'energy' or 'cost'", "Replaced module-level _last_account_refusal with _provider_refusal_since: dict[str, float]", "Renamed _record_account_refusal() to _record_provider_refusal(provider: str) and keyed by provider", "Classifier cascade refusal check now consults _provider_refusal_since.get(cfg.dispatch_settings.default_provider) with cfg.classifier.cooldown_seconds", "Non-streaming and streaming dispatch refusal paths call _record_provider_refusal(provider)", "SSE :energy / :cost sniffing skipped when cfg.dispatch_providers[provider].has_energy_telemetry is false", "Added module _self_check() verifying extract_telemetry({}) and _sniff_telemetry_line behavior at import time" ], "verification": { "py_compile": "passed", "extract_telemetry_empty": "passed", "sniff_openrouter_keepalive": "passed", "targeted_tests": "expected failure: tests/test_classifier_cascade.py and tests/test_local_dispatch_fallback.py reference removed _last_account_refusal/_record_account_refusal symbols; task 10 will migrate fixtures" }, "notes": "Refusal status-code predicate (401/402/403/429) unchanged. Cross-provider runner-up failover not implemented. SSE response contract to clients unchanged." }