feat(R03): unify provider catalogue, routing decisions and usage telemetry
EPIC R03 (Team Duy) - one provider catalogue, one routing flow, one usage seam.
R03-T01 tests/contracts/test_providers.py
29 contract tests every provider must satisfy: canonical assistant message,
streamed text == returned content, reasoning never joins the answer, parsed
tool arguments, ProviderError for every failure. Real adapters exercised
offline by stubbing Provider._request.
R03-T02 domain/models/provider_descriptor.py
infrastructure/providers/provider_registry.py
Provider facts declared once (was split across providers/factory.py,
DEFAULT_CONFIG and PROVIDER_LABELS). ProviderRegistry.build() also stamps the
descriptor id onto the instance, so ollama/github_copilot/codex usage is no
longer all attributed to "openai_compat", and never mutates the caller config.
R03-T03 application/model_routing/routing_application_service.py
Pure-Python routing policy with four modes: Off, Auto, Manual and the new
Fallback (switch only AFTER the current model fails). Depends on a RoutingPort
protocol; production wires the existing core.routing engine underneath.
R03-T04/T05 ui/chat_panel.py, ui/co4e_tab.py, ui/folder_tab.py
Three near-identical routing copies (~40 lines each) replaced by a call to
ctx.routing_application() plus a confirm callback. Mode vocabulary now lives
in one place (normalize_mode/is_valid_mode) instead of four literal tuples.
R03-T06 infrastructure/telemetry/usage_sink.py
Token usage extracted from both providers into UsageEvent + UsageEventSink.
Estimation pinned against core.usage_tracker so no recorded number changes.
Also fixes a deadlock introduced while wiring AppContext: routing_application()
held _routing_lock and called routing(), which takes the same non-reentrant lock.
Suite: 186 passed, 1.22s. check_imports: PASS. All new files < 400 LOC.
2 pre-existing failures remain in test_config_security.py (EPIC R02/Team Nam).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-14
@@ -292,21 +292,19 @@ class AnthropicProvider(Provider):
|
||||
args = {"_raw": b["json"]}
|
||||
tool_calls.append({"id": b["id"], "name": b["name"], "arguments": args})
|
||||
|
||||
# Dashboard usage event — real counts from the stream's usage events,
|
||||
# else a ~4 chars/token estimate. Never breaks the turn.
|
||||
try:
|
||||
from ..core import usage_tracker as ut
|
||||
# Dashboard usage event — real counts from the stream's usage events
|
||||
# (input arrives on message_start, output on message_delta), else a
|
||||
# ~4 chars/token estimate. Delivery is the sink's job (R03-T06), so this
|
||||
# only translates Anthropic's wire shape into a canonical UsageEvent.
|
||||
from ..infrastructure.telemetry import usage_sink as telemetry
|
||||
|
||||
if usage_seen:
|
||||
ut.record(self.name, self.model, usage_seen.get("in", 0),
|
||||
usage_seen.get("out", 0), usage_seen.get("cache", 0))
|
||||
else:
|
||||
sent = json.dumps(payload.get("messages", []), ensure_ascii=False)
|
||||
got = "".join(text_parts) + "".join(b["json"] for b in blocks.values())
|
||||
ut.record(self.name, self.model, ut.estimate_tokens(sent),
|
||||
ut.estimate_tokens(got), 0, estimated=True)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
if usage_seen:
|
||||
event = telemetry.anthropic_usage_event(self.name, self.model, usage_seen)
|
||||
else:
|
||||
sent = json.dumps(payload.get("messages", []), ensure_ascii=False)
|
||||
got = "".join(text_parts) + "".join(b["json"] for b in blocks.values())
|
||||
event = telemetry.estimated_event(self.name, self.model, sent, got)
|
||||
self._emit_usage(event)
|
||||
|
||||
return {"role": "assistant", "content": "".join(text_parts), "tool_calls": tool_calls}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user