From 816df7f0d1fb726f9703ea3577ef58153401124d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 5 Jul 2026 20:29:35 -0400 Subject: [PATCH] conductor(plan): vendor_refresh_xai_rename_20260705 implementation plan 4 phases, 11 tasks: (1) full grok->xai rename across 5 layers, (2) per-vendor registry sync for 7 vendors against models.dev, (3) cost_tracker verification + full suite batch, (4) docs sync. TDD red-first per task. Per-vendor commits in Phase 2 for safe rollback. --- .../plan.md | 1066 +++++++++++++++++ 1 file changed, 1066 insertions(+) create mode 100644 conductor/tracks/vendor_refresh_xai_rename_20260705/plan.md diff --git a/conductor/tracks/vendor_refresh_xai_rename_20260705/plan.md b/conductor/tracks/vendor_refresh_xai_rename_20260705/plan.md new file mode 100644 index 00000000..fb9813dd --- /dev/null +++ b/conductor/tracks/vendor_refresh_xai_rename_20260705/plan.md @@ -0,0 +1,1066 @@ +# Vendor Refresh + xAI Rename Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename the `grok` vendor to `xai` across all 5 layers (public identifiers, internal function names, dispatch branches, cost_tracker regex, tests + docs) and fully sync the `VendorCapabilities` registry for all 7 models.dev-mapped vendors against `https://models.dev/labs/` canonical model lists. + +**Architecture:** The `VendorCapabilities` registry (`src/ai_client.py:228-260`) is a `dict[tuple[str, str], VendorCapabilities]` keyed by `(vendor, model)`. The rename swaps the vendor string from `'grok'` to `'xai'` in all registry entries, all function names (`_send_grok` → `_send_xai`, etc.), all dispatch branches, the `provider_state` history key, and the credentials.toml key. The sync replaces each vendor's `register()` call block with a fresh set pulled from models.dev. The `grok` string survives only as a model name (e.g., `grok-4.3`) within `vendor='xai'` entries — which is correct, since Grok is the model brand sold by the xAI vendor. Hard cutover on the credentials key (no backward-compatible fallback; clear error message naming the `[grok]` → `[xai]` rename). + +**Tech Stack:** Python 3.11+, 1-space indentation, CRLF line endings, `@dataclass(frozen=True)` `VendorCapabilities`, `register()` / `get_capabilities()` / `list_models_for_vendor()` registry API, `provider_state.ProviderHistory` for per-vendor history isolation, pytest with `live_gui` fixture for integration tests. + +**Canonical source:** `https://models.dev/labs/` — the lab page lists each model with context window, reasoning/tool/structured flags, and pricing. Pricing rule: when models.dev shows `$0.00 / $0.00` (meaning "no canonical lab-direct price; provider-dependent"), set `cost_input_per_mtok=0.0, cost_output_per_mtok=0.0` with a `notes=` field explaining the pricing is provider-dependent. Do NOT invent pricing. + +**Edit tool mandate:** Use `manual-slop_edit_file` (with `old_string`/`new_string`), `manual-slop_set_file_slice`, or `manual-slop_py_update_definition` for all edits. NEVER use the native `edit` tool on Python files — it destroys 1-space indentation. All Python code in this plan uses 1-space indentation. + +--- + +## Phase 1: Full `grok` → `xai` rename (all 5 layers, one commit) + +Focus: Rename the vendor string and all derived identifiers across `src/`, `tests/`, and `docs/` in a single atomic commit. The `grok` string survives only as a model name within `vendor='xai'` registry entries (dealt with in Phase 2 when the registry is synced). + +### Task 1.1: Full rename across src + tests + docs + +**Files:** +- Modify: `src/ai_client.py` (lines 62, 152, 267-270, 612, 634, 996, 2630-2727, 3363, 3424-3426) +- Modify: `src/provider_state.py:4,68` +- Modify: `src/cost_tracker.py:53-55` (regex strings only; the full model sync is Phase 3) +- Rename: `tests/test_grok_provider.py` → `tests/test_xai_provider.py` +- Modify: `tests/test_vendor_capabilities.py:88-90` +- Modify: `tests/test_providers_source_of_truth.py:12` +- Modify: `tests/test_provider_curation.py:6` +- Modify: `tests/test_provider_state.py:20,31,32,86` +- Modify: `tests/test_provider_state_migration.py:16` +- Modify: `docs/guide_ai_client.md` (multiple lines) +- Modify: `docs/guide_models.md:59` +- Modify: `docs/Readme.md:34,39` + +- [ ] **Step 1: Write the failing tests (update test files to reference `xai`)** + +First, rename the test file via `git mv`: + +```bash +git mv tests/test_grok_provider.py tests/test_xai_provider.py +``` + +Then update `tests/test_xai_provider.py` — replace ALL `grok` vendor references with `xai` (but keep `grok-2` as a model name for now; the model sync is Phase 2). Use `manual-slop_edit_file` with `replace_all: false` for each distinct replacement. The file is ~60 lines; read it first with `manual-slop_read_file`, then apply these replacements: + +1. `_reset_grok_state` → `_reset_xai_state`, `_grok_client` → `_xai_client`, `_grok_history` → `_xai_history` (fixture, lines 7-12) +2. `test_send_grok_uses_xai_endpoint` → `test_send_xai_uses_xai_endpoint`, `set_provider("grok", "grok-2")` → `set_provider("xai", "grok-2")`, `_ensure_grok_client` → `_ensure_xai_client`, `_send_grok` → `_send_xai`, `"hi from grok"` → `"hi from xai"` (lines 14-22) +3. `test_grok_2_vision_supports_image` → `test_grok_2_vision_supports_image` (keep — tests the model `grok-2-vision` which still exists until Phase 2; update `get_capabilities("grok", "grok-2-vision")` → `get_capabilities("xai", "grok-2-vision")` (line 27) +4. `test_grok_web_search_adds_search_parameters_to_extra_body` → `test_xai_web_search_adds_search_parameters_to_extra_body`, `_ensure_grok_client` → `_ensure_xai_client`, `_send_grok` → `_send_xai` (lines 29-43) +5. `test_grok_x_search_adds_x_source_to_extra_body` → `test_xai_x_search_adds_x_source_to_extra_body`, `_ensure_grok_client` → `_ensure_xai_client`, `_send_grok` → `_send_xai` (lines 45-60) + +Then update the other 5 test files: + +`tests/test_vendor_capabilities.py` (lines 88-90): replace `get_capabilities('grok', 'grok-2-vision')` → `get_capabilities('xai', 'grok-2-vision')`, `caps_grok_v` → `caps_xai_v`. + +`tests/test_providers_source_of_truth.py` (line 12): replace `"grok"` → `"xai"` in the `EXPECTED_PROVIDERS` list. + +`tests/test_provider_curation.py` (line 6): replace `'grok'` → `'xai'` in the `expected_providers` list. + +`tests/test_provider_state.py` (line 20): replace `"grok"` → `"xai"` in `EXPECTED_PROVIDERS` tuple. Lines 31-32: `get_history("grok")` → `get_history("xai")`, `g1`/`g2` variable names can stay. Line 86: `get_history("grok")` → `get_history("xai")`. + +`tests/test_provider_state_migration.py` (line 16): replace `"grok"` → `"xai"` in `EXPECTED_PROVIDERS` tuple. + +- [ ] **Step 2: Run tests to verify they fail (Red phase)** + +Run: `uv run pytest tests/test_xai_provider.py tests/test_vendor_capabilities.py tests/test_providers_source_of_truth.py tests/test_provider_curation.py tests/test_provider_state.py tests/test_provider_state_migration.py -v 2>&1 | Select-Object -Last 30` + +Wait — per workflow.md §"Tier 2 Autonomous Sandbox" convention 2: NEVER filter test output. Redirect to a log file instead: + +```bash +uv run pytest tests/test_xai_provider.py tests/test_vendor_capabilities.py tests/test_providers_source_of_truth.py tests/test_provider_curation.py tests/test_provider_state.py tests/test_provider_state_migration.py -v > tests/artifacts/vendor_rename_phase1_red.log 2>&1 +``` + +Then read the log with `manual-slop_read_file` or grep for `FAILED`/`Error`. + +Expected: FAIL. The test files reference `_send_xai`, `_ensure_xai_client`, `get_capabilities("xai", ...)`, `get_history("xai")`, `PROVIDERS` with `"xai"` — but `src/` still has `grok`. The failures will be `AttributeError` (no `_send_xai`), `KeyError` (no `"xai"` in provider_state), `AssertionError` (PROVIDERS list mismatch). + +- [ ] **Step 3: Update `src/provider_state.py` (Layer 1 — public identifiers)** + +`src/provider_state.py:4` — update the docstring: `grok` → `xai` in the list `(anthropic/deepseek/minimax/qwen/grok/llama)`. + +`src/provider_state.py:68` — replace: +```python + "grok": ProviderHistory(), +``` +with: +```python + "xai": ProviderHistory(), +``` + +Use `manual-slop_edit_file` with `old_string=' "grok": ProviderHistory(),'` and `new_string=' "xai": ProviderHistory(),'`. + +- [ ] **Step 4: Update `src/ai_client.py` Layer 1 (public identifiers)** + +`ai_client.py:62` — `PROVIDERS` list. Replace: +```python +PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] +``` +with: +```python +PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "xai", "llama"] +``` + +`ai_client.py:267-270` — the 4 `vendor='grok'` registry entries. Replace `vendor='grok'` with `vendor='xai'` in all 4 lines. (The model IDs `grok-2`, `grok-2-vision`, `grok-beta` stay for now — Phase 2 Task 2.6 replaces them with the current xAI model set.) + +Use `manual-slop_edit_file` with `replace_all: true` for `old_string="vendor='grok'"` and `new_string="vendor='xai'"`. + +`ai_client.py:2635` — credentials read. Replace: +```python + api_key = creds.get("grok", {}).get("api_key") + if not api_key: + raise ValueError("Grok API key not found in credentials.toml") +``` +with: +```python + api_key = creds.get("xai", {}).get("api_key") + if not api_key: + raise ValueError("xAI API key not found in credentials.toml — the [grok] section was renamed to [xai] in the vendor refresh track. Update your credentials.toml.") +``` + +- [ ] **Step 5: Update `src/ai_client.py` Layer 2 (internal function names)** + +`ai_client.py:152` — global variable. Replace `_grok_client: Any = None` with `_xai_client: Any = None` (preserve alignment column). + +`ai_client.py:612` — reset line. Replace `_grok_client = None` with `_xai_client = None`. + +`ai_client.py:2630-2639` — `_ensure_grok_client` → `_ensure_xai_client`. Use `manual-slop_py_update_definition` with `name="_ensure_grok_client"` and `new_content` containing the full function with `_xai_client`/`_ensure_xai_client` names. The `base_url="https://api.x.ai/v1"` stays. The body: + +```python +def _ensure_xai_client() -> Any: + global _xai_client + if _xai_client is None: + openai = _require_warmed("openai") + creds = _load_credentials() + api_key = creds.get("xai", {}).get("api_key") + if not api_key: + raise ValueError("xAI API key not found in credentials.toml — the [grok] section was renamed to [xai] in the vendor refresh track. Update your credentials.toml.") + _xai_client = openai.OpenAI(api_key=api_key, base_url="https://api.x.ai/v1") + return _xai_client +``` + +`ai_client.py:2641-2724` — `_send_grok` → `_send_xai`. Use `manual-slop_py_update_definition` with `name="_send_grok"` and `new_content` containing the full function renamed. Inside: `_ensure_grok_client()` → `_ensure_xai_client()`, `get_capabilities("grok", _model)` → `get_capabilities("xai", _model)`, `provider_state.get_history("grok")` → `provider_state.get_history("xai")`, `_build_grok_request` → `_build_xai_request`, `vendor_name="grok"` → `vendor_name="xai"`, `source="ai_client.grok"` → `source="ai_client.xai"`. Update the docstring: "Dispatches queries to Grok (x.ai)" → "Dispatches queries to xAI (api.x.ai)", "Grok history using _grok_history_lock" → "xAI history using provider_state", "Calls: _ensure_grok_client" → "Calls: _ensure_xai_client", `[I:_ensure_grok_client]` → `[I:_ensure_xai_client]`. + +`ai_client.py:2726-2727` — `_list_grok_models` → `_list_xai_models`: +```python +def _list_xai_models() -> list[str]: + return list_models_for_vendor("xai") +``` + +- [ ] **Step 6: Update `src/ai_client.py` Layer 3 (dispatch branches)** + +`ai_client.py:634` — `list_models()` dispatch. Replace: +```python + elif provider == "grok": return _list_grok_models() +``` +with: +```python + elif provider == "xai": return _list_xai_models() +``` + +`ai_client.py:3424-3426` — `send()` dispatch. Replace: +```python + elif p == "grok": + res = _send_grok( +``` +with: +```python + elif p == "xai": + res = _send_xai( +``` + +`ai_client.py:996` — docstring caller list. Replace `_send_grok` → `_send_xai` in the "Called by" line. + +`ai_client.py:3363` — docstring caller list. Replace `_send_grok` → `_send_xai`. + +- [ ] **Step 7: Update `src/cost_tracker.py` (Layer 4 — regex strings only)** + +`cost_tracker.py:53-55` — these 3 lines match stale model IDs. For Phase 1, update the regex strings to match the new xAI model IDs. The pricing values will be finalized in Phase 3 Task 3.1; for now, use placeholder pricing from models.dev (grok-4.20 variants are `$1.25/$2.50`; grok-4.3 and grok-build-0.1 are `$0.00` on models.dev — provider-dependent). + +Replace these 3 lines: +```python + (r"grok-2", {"input_per_mtok": 2.00, "output_per_mtok": 10.00}), + (r"grok-2-vision", {"input_per_mtok": 2.00, "output_per_mtok": 10.00}), + (r"grok-beta", {"input_per_mtok": 5.00, "output_per_mtok": 15.00}), +``` +with: +```python + (r"grok-4\.20-0309-non-reasoning", {"input_per_mtok": 1.25, "output_per_mtok": 2.50}), + (r"grok-4\.20-0309-reasoning", {"input_per_mtok": 1.25, "output_per_mtok": 2.50}), + (r"grok-4\.3", {"input_per_mtok": 0.0, "output_per_mtok": 0.0}), + (r"grok-build-0\.1", {"input_per_mtok": 0.0, "output_per_mtok": 0.0}), +``` + +Note: the order matters in cost_tracker (first match wins). Put the longer/more-specific patterns first. `grok-4.20-0309-non-reasoning` and `grok-4.20-0309-reasoning` are more specific than `grok-4.3`, so they go first. Phase 3 Task 3.1 will verify the regex order and finalize pricing. + +- [ ] **Step 8: Update docs (Layer 5)** + +`docs/guide_ai_client.md` — use `manual-slop_edit_file` with `replace_all: true` for the following replacements (each applied across the whole file): +- `_send_grok` → `_send_xai` +- `_ensure_grok_client` → `_ensure_xai_client` +- `_grok_history_lock` → (remove; the lock is now in provider_state — replace with `provider_state.get_history("xai").lock` or just remove the stale reference) +- In the provider list strings: `"grok"` → `"xai"` (in the `| "grok" |` table cell at line 55, and in the "8 providers" mentions at lines 24, 34, 39) +- `get_capabilities("grok"` → `get_capabilities("xai"` (line 137, 587) +- `vendor_name="grok"` → `vendor_name="xai"` (line 587 area) + +Read the file first with `manual-slop_read_file` to find all `grok` occurrences, then apply targeted edits. The `grok` model-name references in capability descriptions (`web_search` — "e.g., grok-2, gemini-grounded" at line 677; `x_search` — "grok-specific" at line 678) should be updated to reference current xAI models: "e.g., grok-4.3, gemini-grounded" and "xai-specific". + +`docs/guide_models.md:59` — replace `"grok"` → `"xai"` in the `PROVIDERS` description. + +`docs/Readme.md:34,39` — replace `"grok"` → `"xai"` in the provider list mentions (2 occurrences). + +- [ ] **Step 9: Run tests to verify they pass (Green phase)** + +Run the batch: +```bash +uv run pytest tests/test_xai_provider.py tests/test_vendor_capabilities.py tests/test_providers_source_of_truth.py tests/test_provider_curation.py tests/test_provider_state.py tests/test_provider_state_migration.py > tests/artifacts/vendor_rename_phase1_green.log 2>&1 +``` + +Read the log. Expected: ALL PASS. If any fail, do NOT loop — read the log, predict the failure from the source, fix, re-run once. + +- [ ] **Step 10: Grep verification (V1)** + +Run these greps and verify the output matches expectations: + +```bash +# No grok vendor string in src/ (only model names like grok-4.3, grok-4.20-*) +# This should return ONLY model-name matches inside vendor='xai' entries and cost_tracker regex +Select-String -Path "src\*.py" -Pattern "grok" -CaseSensitive +``` + +Expected matches in `src/ai_client.py`: only `model='grok-2'`, `model='grok-2-vision'`, `model='grok-beta'` (these are replaced in Phase 2 Task 2.6) and the cost_tracker regex patterns `grok-4.20-*`, `grok-4.3`, `grok-build-0.1`. NO `vendor='grok'`, NO `_send_grok`, NO `_grok_client`, NO `"grok"` in PROVIDERS or provider_state. + +Expected matches in `src/cost_tracker.py`: only the new regex patterns. + +Expected matches in `src/provider_state.py`: none (the `"grok"` key is now `"xai"`). + +```bash +# No grok vendor string in tests/ (only model names) +Select-String -Path "tests\test_*.py" -Pattern "grok" -CaseSensitive +``` + +Expected: only model-name references in `test_xai_provider.py` (e.g., `"grok-2"` as the model argument to `set_provider("xai", "grok-2")`) and `test_vendor_capabilities.py` (`'grok-2-vision'` as the model argument to `get_capabilities('xai', 'grok-2-vision')`). These model-name references are replaced in Phase 2 when the registry is synced. + +```bash +# No grok vendor string in the 3 target docs +Select-String -Path "docs\guide_ai_client.md","docs\guide_models.md","docs\Readme.md" -Pattern "grok" -CaseSensitive +``` + +Expected: only model-name references (if any remain after the doc update). The vendor-name references should all be `xai`. + +- [ ] **Step 11: Commit** + +```bash +git add src/ai_client.py src/cost_tracker.py src/provider_state.py tests/test_xai_provider.py tests/test_vendor_capabilities.py tests/test_providers_source_of_truth.py tests/test_provider_curation.py tests/test_provider_state.py tests/test_provider_state_migration.py docs/guide_ai_client.md docs/guide_models.md docs/Readme.md +git commit -m "refactor(ai_client): rename grok vendor to xai across all 5 layers + +xAI is the vendor (api.x.ai); Grok is a model brand. The code named the +vendor after the model brand — a misnomer. Renamed across public +identifiers (PROVIDERS, VendorCapabilities, provider_state, creds key), +internal functions (_send_grok->_send_xai, _ensure_grok_client->_ensure_xai_client, +_list_grok_models->_list_xai_models), dispatch branches, cost_tracker regex, +tests (test_grok_provider.py -> test_xai_provider.py), and docs. + +Hard cutover on credentials.toml key: [grok] -> [xai]. Error message +names the rename. No backward-compatible fallback. + +The grok string survives only as a model name (grok-2, grok-2-vision, +grok-beta) in vendor='xai' registry entries — replaced with current +xAI models (grok-4.3, grok-build-0.1, grok-4.20-*) in Phase 2." +``` + +- [ ] **Step 12: Attach git note** + +```bash +$sha = git log -1 --format="%H" +git notes add -m "Task 1.1: Full grok->xai rename across 5 layers. +Files: src/ai_client.py, src/cost_tracker.py, src/provider_state.py, tests/test_xai_provider.py (renamed from test_grok_provider.py), tests/test_vendor_capabilities.py, tests/test_providers_source_of_truth.py, tests/test_provider_curation.py, tests/test_provider_state.py, tests/test_provider_state_migration.py, docs/guide_ai_client.md, docs/guide_models.md, docs/Readme.md. +Verification: 6 test files pass in batch; grep confirms no grok vendor string in src/ or tests/ (only model-name references in vendor='xai' entries). +Phase 2 will replace the stale grok-2/grok-2-vision/grok-beta model entries with current xAI models." $sha +``` + +- [ ] **Step 13: Update state.toml** + +Mark `t1_1` as `completed` with the commit SHA (first 7 chars). Set `current_phase = 2`. + +--- + +## Phase 2: Per-vendor registry sync (7 tasks, 7 commits) + +Focus: For each of the 7 models.dev-mapped vendors, replace the `register()` call block with a fresh set pulled from `https://models.dev/labs/`. One commit per vendor for safe rollback. Each task follows TDD: write a test asserting the expected model set (Red), replace the register calls (Green), commit. + +The sync rule: for each vendor, the registry contains (a) one wildcard `*` entry as fallback and (b) one entry per canonical model on the lab page. Pricing: use models.dev's listed price when non-zero; when models.dev shows `$0.00 / $0.00`, set `cost_input_per_mtok=0.0, cost_output_per_mtok=0.0` with a `notes=` field. + +### Task 2.1: Sync gemini registry (google lab, 24 models) + +**Files:** +- Modify: `src/ai_client.py:278-282` (the 5 gemini register calls → 25 calls: wildcard + 24) +- Test: `tests/test_vendor_capabilities.py` (add `test_gemini_models_synced`) + +- [ ] **Step 1: Write the failing test** + +Add to `tests/test_vendor_capabilities.py`: + +```python +def test_gemini_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("gemini") + expected = [ + "gemini-2.0-flash", "gemini-2.0-flash-lite", + "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.5-flash-image", + "gemini-2.5-flash-tts", "gemini-2.5-pro", "gemini-2.5-pro-tts", + "gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3-pro-image-preview", + "gemini-3.1-flash-lite", "gemini-3.1-flash-lite-preview", "gemini-3.1-pro-preview", + "gemini-3.1-pro-preview-customtools", "gemini-3.1-flash-image-preview", + "gemini-3.5-flash", + "gemini-flash-latest", "gemini-flash-lite-latest", + "gemini-embedding-001", + "gemma-4-26b-a4b-it", "gemma-4-31b-it", "gemma-4-e2b-it", "gemma-4-e4b-it", + ] + assert set(models) == set(expected), f"gemini models mismatch: {set(models) ^ set(expected)}" +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_gemini_models_synced -v > tests/artifacts/vendor_sync_gemini_red.log 2>&1 +``` + +Expected: FAIL — the current registry has only 4 gemini models; the test expects 24. + +- [ ] **Step 3: Replace the gemini register calls** + +In `src/ai_client.py`, find the gemini register block (lines ~278-282, starting with `register(VendorCapabilities(vendor='gemini', model='*'`). Use `manual-slop_get_file_slice` to read the exact current lines, then use `manual-slop_set_file_slice` to replace the entire block with the 25 new calls. + +The new gemini register block (1-space indent, preserve alignment with surrounding `register(...)` calls): + +```python +register(VendorCapabilities(vendor='gemini', model='*', context_window=1048576, cost_input_per_mtok=0.30, cost_output_per_mtok=2.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, notes='Gemini wildcard: flash-latest defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='gemini', model='gemini-3.5-flash', context_window=1048576, cost_input_per_mtok=1.50, cost_output_per_mtok=9.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-pro-preview', context_window=1048576, cost_input_per_mtok=2.00, cost_output_per_mtok=12.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-pro-preview-customtools', context_window=1048576, cost_input_per_mtok=2.00, cost_output_per_mtok=12.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-flash-lite', context_window=1048576, cost_input_per_mtok=0.25, cost_output_per_mtok=1.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-flash-lite-preview', context_window=1048576, cost_input_per_mtok=0.25, cost_output_per_mtok=1.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-flash-image-preview', context_window=65536, cost_input_per_mtok=0.50, cost_output_per_mtok=3.00, caching=True, vision=True, grounding=True, structured_output=True, notes='Nano Banana 2: image generation model')) +register(VendorCapabilities(vendor='gemini', model='gemini-3-flash-preview', context_window=1048576, cost_input_per_mtok=0.50, cost_output_per_mtok=3.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3-pro-preview', context_window=1048576, cost_input_per_mtok=1.25, cost_output_per_mtok=12.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3-pro-image-preview', context_window=65536, cost_input_per_mtok=2.00, cost_output_per_mtok=12.00, caching=True, vision=True, grounding=True, structured_output=True, notes='Nano Banana Pro: image generation model')) +register(VendorCapabilities(vendor='gemini', model='gemini-flash-latest', context_window=1048576, cost_input_per_mtok=0.30, cost_output_per_mtok=2.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-flash-lite-latest', context_window=1048576, cost_input_per_mtok=0.10, cost_output_per_mtok=0.40, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash', context_window=1048576, cost_input_per_mtok=0.21, cost_output_per_mtok=1.80, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash-lite', context_window=1048576, cost_input_per_mtok=0.07, cost_output_per_mtok=0.28, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash-image', context_window=32768, cost_input_per_mtok=0.30, cost_output_per_mtok=2.50, caching=True, vision=True, grounding=True, structured_output=True, notes='Nano Banana: image generation model')) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash-tts', context_window=32768, cost_input_per_mtok=0.50, cost_output_per_mtok=10.00, caching=True, structured_output=True, notes='TTS model')) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-pro', context_window=1048576, cost_input_per_mtok=0.87, cost_output_per_mtok=7.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, reasoning=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-pro-tts', context_window=32768, cost_input_per_mtok=1.00, cost_output_per_mtok=20.00, caching=True, structured_output=True, notes='TTS model')) +register(VendorCapabilities(vendor='gemini', model='gemini-2.0-flash', context_window=1048576, cost_input_per_mtok=0.10, cost_output_per_mtok=0.40, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.0-flash-lite', context_window=1048576, cost_input_per_mtok=0.05, cost_output_per_mtok=0.21, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-embedding-001', context_window=2048, cost_input_per_mtok=0.15, cost_output_per_mtok=0.0, notes='Embedding model; output cost is 0')) +register(VendorCapabilities(vendor='gemini', model='gemma-4-26b-a4b-it', context_window=262144, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='gemini', model='gemma-4-31b-it', context_window=262144, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='gemini', model='gemma-4-e2b-it', context_window=131072, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='gemini', model='gemma-4-e4b-it', context_window=131072, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_gemini_models_synced -v > tests/artifacts/vendor_sync_gemini_green.log 2>&1 +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync gemini registry to models.dev google lab (24 models) + +Updated pricing (e.g., gemini-2.5-flash $0.15/$0.60 -> $0.21/$1.80, +gemini-3.1-pro-preview $3.50/$10.50 -> $2.00/$12.00). Added 20 new +models (gemini-3.5-flash, gemini-3-pro-preview, gemini-2.5-pro, Gemma 4 +variants, TTS models, image models, embedding-001, latest aliases)." +``` + +- [ ] **Step 6: Attach git note + update state.toml** + +Mark `t2_1` as completed with the commit SHA. + +### Task 2.2: Sync anthropic registry (anthropic lab, 22 models) + +**Files:** +- Modify: `src/ai_client.py` (the anthropic register block, ~12 calls → 23 calls: wildcard + 22) +- Test: `tests/test_vendor_capabilities.py` (add `test_anthropic_models_synced`) + +- [ ] **Step 1: Write the failing test** + +Add to `tests/test_vendor_capabilities.py`: + +```python +def test_anthropic_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("anthropic") + expected = [ + "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", + "claude-3-7-sonnet-20250219", "claude-3-haiku-20240307", + "claude-sonnet-4-20250514", "claude-sonnet-4-0", + "claude-sonnet-4-5-20250929", "claude-sonnet-4-5", + "claude-sonnet-4-6", "claude-sonnet-5", + "claude-opus-4-20250514", "claude-opus-4-0", + "claude-opus-4-1-20250805", "claude-opus-4-1", + "claude-opus-4-5-20251101", "claude-opus-4-5", + "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", + "claude-haiku-4-5-20251001", "claude-haiku-4-5", + "claude-fable-5", + ] + assert set(models) == set(expected), f"anthropic models mismatch: {set(models) ^ set(expected)}" +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_anthropic_models_synced -v > tests/artifacts/vendor_sync_anthropic_red.log 2>&1 +``` + +Expected: FAIL — current registry has 11 anthropic models; test expects 22. + +- [ ] **Step 3: Replace the anthropic register calls** + +Use `manual-slop_get_file_slice` to read the current anthropic block, then `manual-slop_set_file_slice` to replace it. The new block (wildcard + 22 models). Key pricing updates from models.dev: +- `claude-opus-4-6`: `$5.00/$25.00` (was `$15.00/$75.00` — stale opus-4-1 pricing) +- `claude-sonnet-5`, `claude-fable-5`, `claude-opus-4-7`, `claude-opus-4-8`: 1,000,000 context (newer models) +- `claude-3-5-haiku-20241022`: `$0.80/$4.00` (add) +- `claude-3-haiku-20240307`: `$0.25/$1.25` (add) + +```python +register(VendorCapabilities(vendor='anthropic', model='*', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True, notes='Anthropic wildcard: Sonnet defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-5', context_window=1000000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-fable-5', context_window=1000000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-8', context_window=1000000, cost_input_per_mtok=5.00, cost_output_per_mtok=25.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-7', context_window=1000000, cost_input_per_mtok=5.00, cost_output_per_mtok=25.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-4-6', context_window=1000000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-6', context_window=1000000, cost_input_per_mtok=5.00, cost_output_per_mtok=25.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-5', context_window=200000, cost_input_per_mtok=5.00, cost_output_per_mtok=25.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-5-20251101', context_window=200000, cost_input_per_mtok=5.00, cost_output_per_mtok=25.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-haiku-4-5', context_window=200000, cost_input_per_mtok=1.00, cost_output_per_mtok=5.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-haiku-4-5-20251001', context_window=200000, cost_input_per_mtok=1.00, cost_output_per_mtok=5.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-4-5', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-4-5-20250929', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-1', context_window=200000, cost_input_per_mtok=15.00, cost_output_per_mtok=75.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-1-20250805', context_window=200000, cost_input_per_mtok=15.00, cost_output_per_mtok=75.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4', context_window=200000, cost_input_per_mtok=15.00, cost_output_per_mtok=75.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-opus-4-20250514', context_window=200000, cost_input_per_mtok=15.00, cost_output_per_mtok=75.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-4', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-sonnet-4-20250514', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-3-7-sonnet-20250219', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-3-5-haiku-20241022', context_window=200000, cost_input_per_mtok=0.80, cost_output_per_mtok=4.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-3-5-sonnet-20241022', context_window=200000, cost_input_per_mtok=3.00, cost_output_per_mtok=15.00, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +register(VendorCapabilities(vendor='anthropic', model='claude-3-haiku-20240307', context_window=200000, cost_input_per_mtok=0.25, cost_output_per_mtok=1.25, caching=True, structured_output=True, file_search=True, mcp_support=True, computer_use=True)) +``` + +Note: the wildcard stays at 200,000 context (the Sonnet default). The newer 4.6/4.7/4.8/5 models have 1,000,000 context and are explicitly listed. The "latest" aliases (e.g., `claude-opus-4-5` without a date) are separate entries from their dated versions (e.g., `claude-opus-4-5-20251101`) — both are on models.dev. + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_anthropic_models_synced -v > tests/artifacts/vendor_sync_anthropic_green.log 2>&1 +``` + +Expected: PASS. + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync anthropic registry to models.dev (22 models) + +Updated pricing (claude-opus-4-6 $15/$75 -> $5/$25). Added 10 new +models (claude-sonnet-5, claude-fable-5, claude-opus-4-7/4-8, latest +aliases, claude-3-5-haiku, claude-3-haiku, claude-3-7-sonnet). Newer +4.6/4.7/4.8/5 models have 1M context." +``` + +Mark `t2_2` as completed. + +### Task 2.3: Sync deepseek registry (deepseek lab, 5 models) + +**Files:** +- Modify: `src/ai_client.py` (deepseek register block, 4 calls → 6 calls: wildcard + 5) +- Test: `tests/test_vendor_capabilities.py` (add `test_deepseek_models_synced`) + +- [ ] **Step 1: Write the failing test** + +```python +def test_deepseek_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("deepseek") + expected = ["deepseek-chat", "deepseek-r1", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro"] + assert set(models) == set(expected), f"deepseek models mismatch: {set(models) ^ set(expected)}" +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_deepseek_models_synced -v > tests/artifacts/vendor_sync_deepseek_red.log 2>&1 +``` + +Expected: FAIL — current has `deepseek-v3` (not in expected); missing `deepseek-chat`, `deepseek-v4-flash`, `deepseek-v4-pro`. + +- [ ] **Step 3: Replace the deepseek register calls** + +```python +register(VendorCapabilities(vendor='deepseek', model='*', context_window=1000000, cost_input_per_mtok=0.14, cost_output_per_mtok=0.28, reasoning=True, structured_output=True, notes='DeepSeek wildcard: V3.2 chat defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='deepseek', model='deepseek-v4-flash', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='deepseek', model='deepseek-v4-pro', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='deepseek', model='deepseek-chat', context_window=1000000, cost_input_per_mtok=0.14, cost_output_per_mtok=0.28, structured_output=True)) +register(VendorCapabilities(vendor='deepseek', model='deepseek-reasoner', context_window=1000000, cost_input_per_mtok=0.14, cost_output_per_mtok=0.28, reasoning=True, structured_output=True)) +register(VendorCapabilities(vendor='deepseek', model='deepseek-r1', context_window=128000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, notes='Open weight; pricing provider-dependent')) +``` + +Key changes: `deepseek-v3` dropped (superseded by `deepseek-chat`); `deepseek-reasoner` context 32768 → 1000000, pricing `$0.55/$2.19` → `$0.14/$0.28`; added `deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-chat`. Wildcard context 32768 → 1000000. + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_deepseek_models_synced -v > tests/artifacts/vendor_sync_deepseek_green.log 2>&1 +``` + +Expected: PASS. + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync deepseek registry to models.dev (5 models) + +Dropped deepseek-v3 (superseded by deepseek-chat). Updated deepseek-reasoner +context 32K -> 1M, pricing $0.55/$2.19 -> $0.14/$0.28. Added deepseek-v4-flash, +deepseek-v4-pro, deepseek-chat. Wildcard context 32K -> 1M." +``` + +Mark `t2_3` as completed. + +### Task 2.4: Sync minimax registry (minimax lab, 7 models) + +**Files:** +- Modify: `src/ai_client.py` (minimax register block, 5 calls → 8 calls: wildcard + 7) +- Test: `tests/test_vendor_capabilities.py` (add `test_minimax_models_synced`) + +- [ ] **Step 1: Write the failing test** + +```python +def test_minimax_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("minimax") + expected = ["MiniMax-M2", "MiniMax-M2.1", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M3"] + assert set(models) == set(expected), f"minimax models mismatch: {set(models) ^ set(expected)}" +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_minimax_models_synced -v > tests/artifacts/vendor_sync_minimax_red.log 2>&1 +``` + +Expected: FAIL — missing `MiniMax-M3`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed`. + +- [ ] **Step 3: Replace the minimax register calls** + +```python +register(VendorCapabilities(vendor='minimax', model='*', context_window=204800, cost_input_per_mtok=0.20, cost_output_per_mtok=1.00, notes='MiniMax wildcard: M2.7 defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M3', context_window=512000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.7', context_window=204800, cost_input_per_mtok=0.18, cost_output_per_mtok=0.72, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.7-highspeed', context_window=204800, cost_input_per_mtok=0.33, cost_output_per_mtok=1.32, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.5', context_window=204800, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.5-highspeed', context_window=204800, cost_input_per_mtok=0.19, cost_output_per_mtok=1.24, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.1', context_window=204800, cost_input_per_mtok=0.27, cost_output_per_mtok=1.10, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2', context_window=196608, cost_input_per_mtok=0.20, cost_output_per_mtok=1.00, reasoning=False)) +``` + +Key changes: added `MiniMax-M3` (512K context), `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed`; updated pricing (M2.7 `$0.20/$0.20` → `$0.18/$0.72`; M2.1 `$0.20/$0.20` → `$0.27/$1.10`; M2 `$0.20/$0.20` → `$0.20/$1.00`); context windows 131072 → 204800 (M2.7/M2.5/M2.1) and 196608 (M2). Wildcard context 131072 → 204800. Note: `MiniMax-M2.5` is `$0.00` on models.dev (open weight) — set to 0.0 with note. The existing `test_v2_per_model_population` test in `test_vendor_capabilities.py` asserts `MiniMax-M2.5` has `reasoning=True` (already correct) and `MiniMax-M2` has `reasoning=False` (already correct). + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_minimax_models_synced tests/test_vendor_capabilities.py::test_v2_per_model_population -v > tests/artifacts/vendor_sync_minimax_green.log 2>&1 +``` + +Expected: PASS both. + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync minimax registry to models.dev (7 models) + +Added MiniMax-M3 (512K ctx), M2.7-highspeed, M2.5-highspeed. Updated +pricing (M2.7 $0.20->$0.18/$0.72, M2.1 $0.20->$0.27/$1.10). Context +windows 131K->204K (M2.7/M2.5/M2.1), 196K (M2)." +``` + +Mark `t2_4` as completed. + +### Task 2.5: Sync qwen registry (alibaba lab, 32 models) + +**Files:** +- Modify: `src/ai_client.py` (qwen register block, 8 calls → 33 calls: wildcard + 32) +- Test: `tests/test_vendor_capabilities.py` (add `test_qwen_models_synced`) + +- [ ] **Step 1: Write the failing test** + +```python +def test_qwen_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("qwen") + expected = [ + "qwen-flash", "qwen-turbo", "qwen-plus", "qwen-max", + "qwen-vl-plus", "qwen-vl-max", "qwen-omni-turbo", + "qwen3-max", "qwen3-vl-plus", "qwen3-235b-a22b", "qwen3-32b", + "qwen3-coder-flash", "qwen3-coder-plus", + "qwen3-coder-30b-a3b-instruct", "qwen3-coder-480b-a35b-instruct", + "qwen3-next-80b-a3b-thinking", "qwen3-next-80b-a3b-instruct", + "qwen3.5-plus", "qwen3.5-9b", "qwen3.5-27b", "qwen3.5-35b-a3b", + "qwen3.5-122b-a10b", "qwen3.5-397b-a17b", + "qwen3.6-flash", "qwen3.6-27b", "qwen3.6-plus", "qwen3.6-35b-a3b", + "qwen3.6-max-preview", + "qwen3.7-plus", "qwen3.7-max", + "qwq-plus", + "qwen2.5-vl-72b-instruct", + ] + assert set(models) == set(expected), f"qwen models mismatch: {set(models) ^ set(expected)}" +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_qwen_models_synced -v > tests/artifacts/vendor_sync_qwen_red.log 2>&1 +``` + +Expected: FAIL — current has 7 qwen models; test expects 32. Also `qwen-long` is in current but not in expected (dropped — superseded). + +- [ ] **Step 3: Replace the qwen register calls** + +This is the largest block. The 33 register calls (wildcard + 32). Pricing from models.dev. Note: `qwen-long` is dropped (not on current models.dev list). `qwen-audio` is NOT on the current models.dev alibaba lab page either — check: the alibaba lab page lists `qwen-omni-turbo` (audio-capable) but not `qwen-audio` as a separate model. Drop `qwen-audio`. Update `qwen-turbo` pricing `$0.05/$0.10` → `$0.05/$0.20`. Update `qwen-plus` pricing `$0.40/$1.20` → `$0.26/$0.78`. Update `qwen-max` pricing `$2.00/$6.00` → `$1.60/$6.40`. + +```python +register(VendorCapabilities(vendor='qwen', model='*', context_window=1000000, cost_input_per_mtok=0.05, cost_output_per_mtok=0.20, notes='Qwen wildcard: flash defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='qwen', model='qwen3.7-plus', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen3.7-max', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen3.6-flash', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, notes='pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen3.6-27b', context_window=262144, cost_input_per_mtok=0.20, cost_output_per_mtok=1.50, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3.6-plus', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen3.6-35b-a3b', context_window=262144, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen3.6-max-preview', context_window=262144, cost_input_per_mtok=1.04, cost_output_per_mtok=6.24)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-plus', context_window=1000000, cost_input_per_mtok=0.12, cost_output_per_mtok=0.69)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-9b', context_window=262144, cost_input_per_mtok=0.10, cost_output_per_mtok=0.15, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-27b', context_window=262144, cost_input_per_mtok=0.09, cost_output_per_mtok=0.69, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-35b-a3b', context_window=262144, cost_input_per_mtok=0.06, cost_output_per_mtok=0.46, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-122b-a10b', context_window=262144, cost_input_per_mtok=0.12, cost_output_per_mtok=0.92, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3.5-397b-a17b', context_window=262144, cost_input_per_mtok=0.17, cost_output_per_mtok=1.03, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3-max', context_window=262144, cost_input_per_mtok=0.36, cost_output_per_mtok=1.43)) +register(VendorCapabilities(vendor='qwen', model='qwen3-vl-plus', context_window=262144, cost_input_per_mtok=0.20, cost_output_per_mtok=1.60, vision=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3-235b-a22b', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.80, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3-32b', context_window=131072, cost_input_per_mtok=0.08, cost_output_per_mtok=0.28, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3-coder-flash', context_window=1000000, cost_input_per_mtok=0.20, cost_output_per_mtok=0.97)) +register(VendorCapabilities(vendor='qwen', model='qwen3-coder-plus', context_window=1048576, cost_input_per_mtok=0.65, cost_output_per_mtok=3.25)) +register(VendorCapabilities(vendor='qwen', model='qwen3-coder-30b-a3b-instruct', context_window=262144, cost_input_per_mtok=0.05, cost_output_per_mtok=0.22)) +register(VendorCapabilities(vendor='qwen', model='qwen3-coder-480b-a35b-instruct', context_window=262144, cost_input_per_mtok=0.30, cost_output_per_mtok=1.30)) +register(VendorCapabilities(vendor='qwen', model='qwen3-next-80b-a3b-thinking', context_window=131072, cost_input_per_mtok=0.10, cost_output_per_mtok=0.78, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen3-next-80b-a3b-instruct', context_window=131072, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +register(VendorCapabilities(vendor='qwen', model='qwen-plus', context_window=1000000, cost_input_per_mtok=0.26, cost_output_per_mtok=0.78)) +register(VendorCapabilities(vendor='qwen', model='qwen-flash', context_window=1000000, cost_input_per_mtok=0.05, cost_output_per_mtok=0.40, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen-turbo', context_window=1000000, cost_input_per_mtok=0.05, cost_output_per_mtok=0.20, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen-max', context_window=32768, cost_input_per_mtok=1.60, cost_output_per_mtok=6.40)) +register(VendorCapabilities(vendor='qwen', model='qwen-vl-plus', context_window=131072, cost_input_per_mtok=0.21, cost_output_per_mtok=0.63, vision=True)) +register(VendorCapabilities(vendor='qwen', model='qwen-vl-max', context_window=131072, cost_input_per_mtok=0.80, cost_output_per_mtok=3.20, vision=True)) +register(VendorCapabilities(vendor='qwen', model='qwen-omni-turbo', context_window=32768, cost_input_per_mtok=0.07, cost_output_per_mtok=0.27, audio=True, vision=True, notes='Omni model: audio + vision input')) +register(VendorCapabilities(vendor='qwen', model='qwq-plus', context_window=131072, cost_input_per_mtok=0.80, cost_output_per_mtok=2.40, reasoning=True)) +register(VendorCapabilities(vendor='qwen', model='qwen2.5-vl-72b-instruct', context_window=131072, cost_input_per_mtok=0.13, cost_output_per_mtok=0.40, vision=True)) +``` + +Note: the existing `test_v2_per_model_population` test asserts `qwen-audio` has `audio=True` and `qwen-long` has `caching=True`. Both models are dropped in this sync. Update that test: remove the `caps_qwen_audio` and `caps_qwen_long` assertions (lines referencing `qwen-audio` and `qwen-long` in `test_vendor_capabilities.py:94-97`). The `qwen-omni-turbo` entry has `audio=True` — if the test wants to keep an audio assertion, point it at `qwen-omni-turbo`. + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_qwen_models_synced tests/test_vendor_capabilities.py::test_v2_per_model_population -v > tests/artifacts/vendor_sync_qwen_green.log 2>&1 +``` + +Expected: PASS both (after updating `test_v2_per_model_population` to remove the dropped-model assertions). + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync qwen registry to models.dev alibaba lab (32 models) + +Dropped qwen-long (superseded) and qwen-audio (not on models.dev alibaba +lab; qwen-omni-turbo covers audio). Updated pricing (qwen-plus $0.40/$1.20 +-> $0.26/$0.78, qwen-max $2.00/$6.00 -> $1.60/$6.40). Added 24 new models +(qwen3.7-plus/max, qwen3.6 family, qwen3.5 family, qwen3-max, qwen3-coder-* +variants, qwen-flash, qwq-plus, qwen2.5-vl-72b-instruct)." +``` + +Mark `t2_5` as completed. + +### Task 2.6: Sync xai registry (xai lab, 4 models) + +**Files:** +- Modify: `src/ai_client.py` (the xai register block — currently 4 stale entries → 5 calls: wildcard + 4) +- Test: `tests/test_vendor_capabilities.py` (add `test_xai_models_synced`) +- Test: `tests/test_xai_provider.py` (update model IDs in existing tests from `grok-2`/`grok-2-vision` to current models) + +- [ ] **Step 1: Write the failing test** + +Add to `tests/test_vendor_capabilities.py`: + +```python +def test_xai_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("xai") + expected = ["grok-4.20-0309-non-reasoning", "grok-4.20-0309-reasoning", "grok-4.3", "grok-build-0.1"] + assert set(models) == set(expected), f"xai models mismatch: {set(models) ^ set(expected)}" +``` + +Also update `test_v2_per_model_population` in the same file: replace the `caps_xai_v` block (currently asserting `get_capabilities('xai', 'grok-2-vision')` has `vision=True`, `web_search=True`, `x_search=True`). The new xAI models don't have vision on models.dev. Update to assert a current capability: + +```python + caps_xai_43 = get_capabilities('xai', 'grok-4.3') + assert caps_xai_43.web_search is True + assert caps_xai_43.x_search is True + assert caps_xai_43.reasoning is True +``` + +And update `tests/test_xai_provider.py`: replace `set_provider("xai", "grok-2")` → `set_provider("xai", "grok-4.3")`, `"hi from xai"` stays, the `test_grok_2_vision_supports_image` test → `test_grok_4_3_supports_web_search` (since no current xAI model has vision): + +```python +def test_grok_4_3_supports_web_search() -> None: + from src.ai_client import get_capabilities + caps = get_capabilities("xai", "grok-4.3") + assert caps.web_search is True + assert caps.x_search is True +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_xai_models_synced tests/test_xai_provider.py -v > tests/artifacts/vendor_sync_xai_red.log 2>&1 +``` + +Expected: FAIL — current registry has `grok-2`, `grok-2-vision`, `grok-beta` (stale); test expects `grok-4.3`, `grok-build-0.1`, `grok-4.20-0309-non-reasoning`, `grok-4.20-0309-reasoning`. + +- [ ] **Step 3: Replace the xai register calls** + +Replace the 4 stale `vendor='xai'` entries (which were `vendor='grok'` before Phase 1) with: + +```python +register(VendorCapabilities(vendor='xai', model='*', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, web_search=True, x_search=True, notes='xAI wildcard: grok-4.3 defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='xai', model='grok-4.3', context_window=1000000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, web_search=True, x_search=True, notes='pricing provider-dependent; see https://docs.x.ai/docs/models')) +register(VendorCapabilities(vendor='xai', model='grok-build-0.1', context_window=256000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, reasoning=True, structured_output=True, notes='Coding model; pricing provider-dependent')) +register(VendorCapabilities(vendor='xai', model='grok-4.20-0309-non-reasoning', context_window=1000000, cost_input_per_mtok=1.25, cost_output_per_mtok=2.50, structured_output=True, web_search=True, x_search=True)) +register(VendorCapabilities(vendor='xai', model='grok-4.20-0309-reasoning', context_window=1000000, cost_input_per_mtok=1.25, cost_output_per_mtok=2.50, reasoning=True, structured_output=True, web_search=True, x_search=True)) +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_xai_models_synced tests/test_vendor_capabilities.py::test_v2_per_model_population tests/test_xai_provider.py -v > tests/artifacts/vendor_sync_xai_green.log 2>&1 +``` + +Expected: PASS all. + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py tests/test_xai_provider.py +git commit -m "feat(ai_client): sync xai registry to models.dev xai lab (4 models) + +Replaced stale grok-2/grok-2-vision/grok-beta with current xAI models: +grok-4.3 (1M ctx, reasoning, web/x search), grok-build-0.1 (256K ctx, +coding), grok-4.20-0309-non-reasoning and grok-4.20-0309-reasoning (1M +ctx, \$1.25/\$2.50). Updated test_xai_provider.py to use grok-4.3 instead +of grok-2. Updated test_v2_per_model_population xai assertion to test +web_search/reasoning on grok-4.3 (no current xAI model has vision)." +``` + +Mark `t2_6` as completed. + +### Task 2.7: Sync llama registry (meta lab, 3 models) + +**Files:** +- Modify: `src/ai_client.py` (llama register block, 9 calls → 4 calls: wildcard + 3) +- Test: `tests/test_vendor_capabilities.py` (add `test_llama_models_synced`, update `test_v2_per_model_population`) + +- [ ] **Step 1: Write the failing test** + +```python +def test_llama_models_synced() -> None: + from src.ai_client import list_models_for_vendor + models = list_models_for_vendor("llama") + expected = ["llama-3.3-70b-instruct", "llama-4-maverick-17b-instruct", "llama-4-scout-17b-instruct"] + assert set(models) == set(expected), f"llama models mismatch: {set(models) ^ set(expected)}" +``` + +Also update `test_v2_per_model_population`: the current test asserts `get_capabilities('llama', 'llama-3.1-405b-reasoning')` has `reasoning=True` and `get_capabilities('llama', 'llama-3.1-8b-instant')` has `reasoning=False`. Both models are dropped. Replace with assertions on the new models: + +```python + caps_llama_maverick = get_capabilities('llama', 'llama-4-maverick-17b-instruct') + assert caps_llama_maverick.context_window == 1000000 + caps_llama_scout = get_capabilities('llama', 'llama-4-scout-17b-instruct') + assert caps_llama_scout.context_window == 3500000 +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_llama_models_synced tests/test_vendor_capabilities.py::test_v2_per_model_population -v > tests/artifacts/vendor_sync_llama_red.log 2>&1 +``` + +Expected: FAIL — current has 8 llama models; test expects 3. + +- [ ] **Step 3: Replace the llama register calls** + +Replace the 9 current llama entries with: + +```python +register(VendorCapabilities(vendor='llama', model='*', context_window=128000, notes='Llama wildcard: 70B defaults. Per-model variations below.')) +register(VendorCapabilities(vendor='llama', model='llama-4-maverick-17b-instruct', context_window=1000000, cost_input_per_mtok=0.12, cost_output_per_mtok=0.60)) +register(VendorCapabilities(vendor='llama', model='llama-4-scout-17b-instruct', context_window=3500000, cost_input_per_mtok=0.17, cost_output_per_mtok=0.66)) +register(VendorCapabilities(vendor='llama', model='llama-3.3-70b-instruct', context_window=128000, cost_input_per_mtok=0.0, cost_output_per_mtok=0.0, notes='Open weight; pricing provider-dependent')) +``` + +All 8 stale Llama 3.x entries dropped. Note: the `llama` vendor dispatches to Ollama/OpenRouter/custom URLs, so dropped models are still callable via the wildcard fallback if the user types the model ID — they just won't appear in the model picker list. + +- [ ] **Step 4: Run test to verify it passes** + +```bash +uv run pytest tests/test_vendor_capabilities.py::test_llama_models_synced tests/test_vendor_capabilities.py::test_v2_per_model_population -v > tests/artifacts/vendor_sync_llama_green.log 2>&1 +``` + +Expected: PASS both. + +- [ ] **Step 5: Commit + git note + update state.toml** + +```bash +git add src/ai_client.py tests/test_vendor_capabilities.py +git commit -m "feat(ai_client): sync llama registry to models.dev meta lab (3 models) + +Dropped 8 stale Llama 3.x entries (llama-3.1-*, llama-3.2-*, llama-3.3-70b-specdec). +Added llama-4-maverick-17b-instruct (1M ctx, \$0.12/\$0.60) and +llama-4-scout-17b-instruct (3.5M ctx, \$0.17/\$0.66). Kept +llama-3.3-70b-instruct (open weight). Dropped models still callable via +wildcard fallback." +``` + +Mark `t2_7` as completed. Phase 2 complete — run the full `test_vendor_capabilities.py` batch to confirm all 7 vendor sync tests pass together. + +--- + +## Phase 3: Cost tracker regex + full verification + +Focus: Verify the cost_tracker regex from Phase 1 matches the new xAI model IDs with correct pricing, then run the full test suite batch to confirm no regressions. + +### Task 3.1: Verify and finalize cost_tracker regex + +**Files:** +- Modify: `src/cost_tracker.py:53-56` (the xAI regex patterns — already updated in Phase 1 Step 7; verify pricing matches the registry) +- Test: `tests/test_cost_tracker.py` (if it exists — check with `manual-slop_search_files`) + +- [ ] **Step 1: Check for existing cost_tracker tests** + +```bash +# Use manual-slop_search_files or glob +``` + +Search for `tests/test_cost_tracker*.py`. If it exists, read it to understand the test pattern. If not, create `tests/test_cost_tracker.py`. + +- [ ] **Step 2: Write/verify the failing test** + +If `tests/test_cost_tracker.py` exists, add these assertions. If not, create it: + +```python +from src.cost_tracker import estimate_cost + +def test_cost_tracker_grok_4_3() -> None: + # grok-4.3 is $0.00/$0.00 on models.dev (provider-dependent) + cost = estimate_cost("grok-4.3", 1000, 500) + assert cost == 0.0 + +def test_cost_tracker_grok_4_20_reasoning() -> None: + # grok-4.20-0309-reasoning is $1.25/$2.50 + cost = estimate_cost("grok-4.20-0309-reasoning", 1000000, 1000000) + assert cost == 1.25 + 2.50 + +def test_cost_tracker_grok_4_20_non_reasoning() -> None: + # grok-4.20-0309-non-reasoning is $1.25/$2.50 + cost = estimate_cost("grok-4.20-0309-non-reasoning", 1000000, 1000000) + assert cost == 1.25 + 2.50 + +def test_cost_tracker_grok_2_returns_zero() -> None: + # grok-2 pattern removed; should return 0 (no match) + cost = estimate_cost("grok-2", 1000000, 1000000) + assert cost == 0.0 + +def test_cost_tracker_grok_beta_returns_zero() -> None: + # grok-beta pattern removed; should return 0 (no match) + cost = estimate_cost("grok-beta", 1000000, 1000000) + assert cost == 0.0 +``` + +- [ ] **Step 3: Run test** + +```bash +uv run pytest tests/test_cost_tracker.py -v > tests/artifacts/vendor_cost_tracker.log 2>&1 +``` + +Expected: PASS. If the regex from Phase 1 Step 7 is correct, all pass. If `grok-4.20-0309-non-reasoning` fails (regex order issue — `grok-4.3` matching before the longer pattern), reorder the patterns so longer/more-specific patterns come first. + +- [ ] **Step 4: Commit (if changes needed)** + +If the test passes without changes to `cost_tracker.py`, just commit the test file. If reordering was needed, commit both. + +```bash +git add tests/test_cost_tracker.py src/cost_tracker.py +git commit -m "test(cost_tracker): verify xAI regex matches new model IDs + +Tests grok-4.3 (\$0), grok-4.20-reasoning/non-reasoning (\$1.25/\$2.50), +and confirms grok-2/grok-beta patterns are removed (return 0)." +``` + +Mark `t3_1` as completed. + +### Task 3.2: Full test suite batch verification + +**Files:** None (verification only) + +- [ ] **Step 1: Run the full test suite batch** + +Per workflow.md §"Tier 2 Autonomous Sandbox" convention 2: redirect to a log file, never filter output. + +```bash +uv run pytest tests/ > tests/artifacts/vendor_full_suite.log 2>&1 +``` + +- [ ] **Step 2: Read the log and check for failures** + +Use `manual-slop_read_file` or grep the log for `FAILED`/`ERROR`/`error`. If there are failures: +- Do NOT loop (per workflow.md §"Process Anti-Patterns" #1: max 2 runs, then predict + instrument). +- Read the relevant source, predict the failure, fix, re-run once. +- If still failing after 2 attempts, STOP and report to the user. + +- [ ] **Step 3: Grep verification (V1, V2, V5)** + +```bash +# V1: no grok vendor string in src/ +Select-String -Path "src\*.py" -Pattern "vendor='grok'" -CaseSensitive +# Expected: no matches + +Select-String -Path "src\*.py" -Pattern "_send_grok|_grok_client|_ensure_grok_client|_list_grok_models" -CaseSensitive +# Expected: no matches + +# V2: per-vendor model counts +uv run python -c "from src.ai_client import list_models_for_vendor; [print(f'{v}: {len(list_models_for_vendor(v))} models') for v in ['gemini','anthropic','deepseek','minimax','qwen','xai','llama']]" +# Expected: gemini: 24, anthropic: 22, deepseek: 5, minimax: 7, qwen: 32, xai: 4, llama: 3 + +# V5: no grok vendor string in target docs +Select-String -Path "docs\guide_ai_client.md","docs\guide_models.md","docs\Readme.md" -Pattern "\"grok\"" -CaseSensitive +# Expected: no matches (only model-name references like grok-4.3 may appear) +``` + +- [ ] **Step 4: Create phase checkpoint commit (if any uncommitted changes)** + +If the verification required any fixes, commit them. If everything passed clean, create an empty checkpoint commit: + +```bash +git commit --allow-empty -m "conductor(checkpoint): Phase 1-3 complete, vendor refresh verified + +All 7 vendors synced to models.dev. grok->xai rename complete across 5 +layers. Cost tracker regex updated. Full test suite batch green." +``` + +- [ ] **Step 5: Attach git note + update state.toml** + +Mark `t3_2` as completed. Set `phase_3_tests_green = true` in state.toml. + +--- + +## Phase 4: Docs sync + +Focus: Update the deep-dive docs to reflect the rename and the new model counts. Per the Documentation Refresh Protocol (workflow.md), docs that reference renamed symbols must be updated in the same track. + +### Task 4.1: Update docs with final model counts and xai references + +**Files:** +- Modify: `docs/guide_ai_client.md` (vendor list, model counts, _send_xai references — mostly done in Phase 1 Step 8; verify and add model-count updates) +- Modify: `docs/guide_models.md:59` (PROVIDERS description — done in Phase 1; verify) +- Modify: `docs/Readme.md:34,39` (provider list — done in Phase 1; verify) + +- [ ] **Step 1: Read the current docs for any remaining `grok` vendor references** + +```bash +Select-String -Path "docs\guide_ai_client.md","docs\guide_models.md","docs\Readme.md" -Pattern "grok" -CaseSensitive +``` + +Review each match. Any `grok` vendor reference (e.g., "the grok provider", "_send_grok", `"grok"` in a provider list) should already be `xai` from Phase 1. Any `grok-4.3` / `grok-4.20` model-name references are correct and stay. + +- [ ] **Step 2: Update model counts in docs** + +`docs/guide_ai_client.md` — find any mention of model counts per vendor (e.g., "5 gemini models" or similar). Update to the new counts: gemini 24, anthropic 22, deepseek 5, minimax 7, qwen 32, xai 4, llama 3. If the docs don't mention specific counts, skip this step. + +`docs/guide_models.md:59` — the PROVIDERS description should now say `"xai"` (verified in Step 1). + +- [ ] **Step 3: Commit** + +```bash +git add docs/guide_ai_client.md docs/guide_models.md docs/Readme.md +git commit -m "docs: sync guide_ai_client/guide_models/Readme to vendor refresh + +Verified no grok vendor references remain. Updated model counts where +docs mention them (gemini 24, anthropic 22, deepseek 5, minimax 7, +qwen 32, xai 4, llama 3)." +``` + +- [ ] **Step 4: Attach git note + update state.toml** + +Mark `t4_1` as completed. Set `phase_4_docs_synced = true`. Set track `status = "completed"` if all verification criteria pass. + +- [ ] **Step 5: Final grep verification (V1 full)** + +```bash +# Final check: no grok vendor string anywhere in src/ or tests/ +Select-String -Path "src\*.py","tests\test_*.py" -Pattern "vendor=.grok|\"grok\"|_send_grok|_grok_client|_ensure_grok|_list_grok" -CaseSensitive +# Expected: no matches in src/. In tests/, only model-name references like "grok-4.3" (not vendor references). +``` + +--- + +## Self-Review + +**1. Spec coverage:** +- FR1.1 (public identifiers) → Task 1.1 Steps 3-4 ✓ +- FR1.2 (internal function names) → Task 1.1 Step 5 ✓ +- FR1.3 (dispatch branches) → Task 1.1 Step 6 ✓ +- FR1.4 (cost tracker regex) → Task 1.1 Step 7 + Task 3.1 ✓ +- FR1.5 (tests) → Task 1.1 Step 1 + per-vendor test updates in Phase 2 ✓ +- FR1.6 (docs) → Task 1.1 Step 8 + Task 4.1 ✓ +- FR2.1 (gemini) → Task 2.1 ✓ +- FR2.2 (anthropic) → Task 2.2 ✓ +- FR2.3 (deepseek) → Task 2.3 ✓ +- FR2.4 (minimax) → Task 2.4 ✓ +- FR2.5 (qwen) → Task 2.5 ✓ +- FR2.6 (xai) → Task 2.6 ✓ +- FR2.7 (llama) → Task 2.7 ✓ +- FR2.8 (cost tracker) → Task 3.1 ✓ +- FR3 (gemini_cli untouched) → no task touches it ✓ +- V1 (grep) → Task 1.1 Step 10 + Task 3.2 Step 3 + Task 4.1 Step 5 ✓ +- V2 (registry counts) → Task 3.2 Step 3 ✓ +- V3 (test suite green) → Task 3.2 ✓ +- V4 (cost tracker) → Task 3.1 ✓ +- V5 (docs grep) → Task 4.1 Step 1 ✓ + +**2. Placeholder scan:** No TBD/TODO. All code blocks are complete. The one judgment call (qwen-audio/qwen-long drop) is explicitly handled in Task 2.5 Step 3 with a note to update `test_v2_per_model_population`. + +**3. Type consistency:** `VendorCapabilities` fields used in Phase 2 (context_window, cost_input_per_mtok, cost_output_per_mtok, reasoning, structured_output, vision, audio, web_search, x_search, caching, notes) all match the dataclass definition at `ai_client.py:228-253`. `list_models_for_vendor()` return type `list[str]` is consistent across all test assertions. `get_capabilities()` return type `VendorCapabilities` is consistent. + +All spec requirements covered. No placeholders. Types consistent. Plan ready. \ No newline at end of file