# 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ấu `metadata_incomplete=True` (không đoán bừa). ## Config (trong `config.json`, mục `routing`) ```jsonc "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/.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_env` củ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 ```bash # 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).