Auto Model Assessment & Routing
Tự động đánh giá từng model (provider/model do user cấu hình), chấm điểm phù hợp cho mỗi loại task, rồi định tuyến mỗi lượt chat/agent tới model phù hợp nhất — theo 3 chế độ Off / Auto / Manual bật ngay trên màn hình chat (Cowork, Co4E, AI-Edit).
Module này được xây dựng để hoà vào đúng stack sẵn có của Cowork-Local (PySide6 desktop app), thay vì dựng một service FastAPI riêng:
| Bản mô tả gốc (đề bài) | Hiện thực trong app này |
|---|---|
| Config YAML | Config JSON ~/.cowork_local/config.json (chuẩn của app) + file assessment riêng |
| FastAPI REST endpoints | RoutingService (Python facade) — các method ánh xạ 1-1 với endpoint |
httpx async + asyncio.Semaphore |
Tái dùng providers/ (requests) + ThreadPoolExecutor với semaphore theo từng provider |
| APScheduler | QTimer (giống core/task_scheduler.py) — không thêm dependency |
clients.py (Anthropic/OpenAI) |
AppProbeClient bọc AppContext.build_provider_for (đã có sẵn TLS-trust, retry 429, gateway) |
Kiến trúc
core/routing/
models.py # Pydantic v2: ModelMetadata, ProbeResult, ModelAssessment,
# SwitchDecision, PendingSwitch, TaskType/Policy/SwitchMode
store.py # AssessmentStore: JSON, atomic write (temp+rename), history backup
metadata.py # STATIC_METADATA + enrich() (dùng lại core/model_pricing cho giá)
clients.py # AppProbeClient (bọc Provider có sẵn) + ProbeClient protocol
prober.py # BENCHMARK_TASKS, probe_model(), make_judge(), semaphore/provider
scorer.py # compute_fit_score() + POLICY_WEIGHTS
selector.py # rank_models()/best_model() — tính lại fit theo policy, không probe lại
classifier.py # classify(prompt) -> TaskType (heuristic, fallback LLM tuỳ chọn)
switch_controller.py # decide() (thuần) + PendingSwitchRegistry (TTL, idempotent)
orchestrator.py # check_and_update(): enrich -> probe -> score -> store
service.py # RoutingService — facade UI gọi
scheduler.py # RoutingScheduler (QTimer): reassess định kỳ + dọn pending hết hạn
Công thức fit score
fit = w_quality * quality
+ w_cost * 1/(1 + cost)
+ w_latency * 1/(1 + latency_s)
POLICY_WEIGHTS (mỗi hàng cộng = 1.0):
| Policy | quality | cost | latency |
|---|---|---|---|
quality |
0.80 | 0.10 | 0.10 |
cost |
0.20 | 0.70 | 0.10 |
latency |
0.20 | 0.10 | 0.70 |
balanced |
0.50 | 0.25 | 0.25 |
- Probe fail → fit = 0 (model không dùng được thì không bao giờ được chọn).
- Giá không rõ → để
None, đánh dấumetadata_incomplete=True(không đoán bừa).
Config (trong config.json, mục routing)
"routing": {
"switch_mode": "off", // mặc định toàn cục: "off" | "auto" | "manual"
"policy": "balanced", // "quality" | "cost" | "latency" | "balanced"
"min_score_gain": 0.05, // chỉ chuyển nếu model mới hơn model hiện tại ≥ ngưỡng này
"confirm_timeout_sec": 60, // (manual) hết giờ chờ confirm → giữ model hiện tại
"reassess_interval_hours": 24, // lịch reassess; 0 = tắt
"per_provider_concurrency": 2, // số probe song song tối đa mỗi provider (chống rate limit)
"judge_provider": "", // provider của judge ("" → active provider)
"judge_model": "", // model chấm điểm cố định ("" → default rẻ theo provider)
"candidates": [ // model muốn đánh giá; rỗng → tự lấy model đang cấu hình
{"provider": "anthropic", "model_id": "claude-opus-4-8", "tier": "powerful"},
{"provider": "anthropic", "model_id": "claude-haiku-4-5", "tier": "fast"}
],
"auto_reassess_on_add": true, // thêm model mới → reassess ngay
"surface_modes": { // toggle Off/Auto/Manual của TỪNG màn hình ("" = theo switch_mode)
"cowork": "", "co4e": "", "ai_edit": ""
}
}
Kết quả assessment KHÔNG nằm trong config.json mà ở file riêng:
~/.cowork_local/assessments.json (+ backup lịch sử ở assessments_history/<timestamp>.json).
Toggle Off / Auto / Manual (trên màn hình chat)
Mỗi màn hình chat có một toggle nhỏ cạnh ô chọn model:
- Off — tắt định tuyến, luôn dùng model đang chọn.
- Auto — tự động chuyển sang model phù hợp nhất (nếu
gain ≥ min_score_gain), chạy luôn, hiện dòng thông báo↪ Auto-routed to …. - Manual — hiện hộp thoại xác nhận (có đếm ngược
confirm_timeout_sec); user đồng ý mới chuyển, từ chối / hết giờ thì giữ model hiện tại.
Toggle được lưu riêng cho từng màn hình (surface_modes) và ghi đè switch_mode toàn cục.
Logical API (RoutingService)
| Method | Tương đương REST trong đề bài |
|---|---|
reassess(policy=None) / reassess_background() |
POST /models/reassess |
best_for(task_type, policy) |
GET /models/best |
status() / assessments() |
GET /models/assessments |
add_candidate(provider, model_id, tier) |
POST /models/add (tự trigger reassess) |
route(surface, prompt, provider, model) |
phần quyết định của POST /task/execute |
create_pending() / resolve_pending(id, approve, run) |
POST /task/confirm-switch (idempotent) |
get_routing_config() / update_routing_config(**) |
GET/PATCH /routing/config |
Bảo mật & chi phí
- API key đọc từ env (qua
api_key_envcủa provider) — không ghi key vào config/log. - Probe tốn tiền → chỉ chạy theo lịch / khi thêm model / khi bấm "Reassess now". Mỗi lần reassess ghi log số lượng API call.
- Idempotent — reassess ổn định (chỉ latency dao động ~µs, dưới xa
min_score_gain); ghi atomic nên ngắt giữa chừng không hỏng config. - Không gọi API thật trong test —
clients.py/judge()được mock hoàn toàn.
Chạy test
# từ thư mục cha của package (…/cowork_local_20260722)
python -m pytest cowork_local/tests/routing/ -q
Bao phủ: scorer, store (atomic + history), selector, switch_controller
(Auto/Manual/Off, timeout, idempotent), orchestrator (mock client), classifier,
và service (end-to-end reassess → route → confirm).