EPIC R03 (Team Duy) — Model Providers & Routing. All six tasks done.
R03-T02 — Provider catalogue
domain/models/provider_descriptor.py ProviderDescriptor (frozen), WireProtocol, AuthKind
infrastructure/providers/provider_registry.py
thread-safe registry: id/alias lookup, dynamic
lookup by model id, adapter selection by protocol
providers/factory.py drops its own _REGISTRY table and delegates to the
registry, still raising ProviderError for callers
R03-T03 — RoutingApplicationService (pure Python, 4 modes)
application/model_routing/routing_models.py
RoutingMode (off/auto/manual/fallback),
RoutingRequest (immutable snapshot), RouteEvaluation,
RoutingOutcome
application/model_routing/routing_application_service.py
the single decision flow, reached through two narrow
ports plus a caller-supplied confirm callback, so no
Qt import is needed
application/model_routing/core_routing_adapter.py
binds the ports to core/routing and AppContext
Fallback is a new resilience mode: keep the selected model while it can serve the turn,
re-route only when it cannot. Wired end to end through config.py, state.py,
ui/routing_toggle.py and i18n.py (EN/JA/VI).
R03-T04 / T05 — Remove the duplicated routing flow
ui/chat_panel.py (#L638), ui/co4e_tab.py, ui/folder_tab.py each drop ~35 lines of copied
logic and call the shared service; the widgets now only build a RoutingRequest, host the
Manual-mode modal and render the outcome.
R03-T06 — Token usage as an event
infrastructure/telemetry/usage_sink.py UsageEvent + UsageEventSink protocol, with tracker,
in-memory and composite sinks
providers/openai_compat.py, providers/anthropic.py
publish a UsageEvent instead of writing to the
usage tracker themselves
core/usage_tracker.py adds current_context() so a sink can borrow and
restore a thread's attribution
R03-T01 — Contract tests
tests/contracts/test_providers.py parametrises over every provider in the registry: chat()
signature, canonical assistant message, normalised tool calls, response closed, tool schema
translation, ProviderError, list_models/test_connection, one UsageEvent per turn.
Test infrastructure fix (required to verify any of the above): tests/conftest.py used to put
the repository's PARENT directory on sys.path, so `import cowork_local.*` resolved against
whichever sibling folder happened to carry that name — on a dev machine, an unrelated older
checkout. The suite reported green while exercising different code. The conftest now binds
this checkout to the cowork_local name in sys.modules.
Verification
pytest tests/ 236 passed in ~1.8s (102 before this change)
scripts/check_imports.py PASS, 0 forbidden imports in domain/ and application/
new production files largest is 288 lines, all under the 400 LOC ceiling
new tests 134 (50 contract, 70 unit, 14 integration), all offline
scripts/run_quality_gate.py does not exist yet (R10-T02), so DoD item 7 was covered by
check_imports.py plus the full suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33 lines
1.2 KiB
Python
33 lines
1.2 KiB
Python
"""Build a provider instance from the application config.
|
|
|
|
Kept as the historic entry point (``providers.build_provider``) that call sites
|
|
across the app already import, but it no longer owns a provider table of its
|
|
own: since R03-T02 the catalogue lives in
|
|
``infrastructure/providers/provider_registry.py`` so provider ids, wire
|
|
protocols, default models and capabilities are declared exactly once.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict
|
|
|
|
from .base import Provider, ProviderError
|
|
|
|
|
|
def build_provider(name: str, conf: Dict[str, Any]) -> Provider:
|
|
"""Construct the adapter registered for ``name``.
|
|
|
|
Delegates to the central registry and translates its lookup failure into
|
|
:class:`ProviderError`, because every existing call site (chat turns,
|
|
Settings' connection test, the routing prober) already handles that type —
|
|
changing the exception would ripple into unrelated error handling.
|
|
"""
|
|
from ..infrastructure.providers.provider_registry import (
|
|
ProviderNotFoundError,
|
|
default_registry,
|
|
)
|
|
|
|
try:
|
|
return default_registry().build(name, conf)
|
|
except ProviderNotFoundError as exc:
|
|
raise ProviderError(f"Unsupported provider: {name}") from exc
|