From 81d8bce41923220bf9aed37a7f104d00d8ecf3cc Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 26 Jun 2026 07:07:12 -0400 Subject: [PATCH] refactor(ai_client): merge vendor_capabilities into ai_client; git rm src/vendor_capabilities.py Per spec FR2 + Phase 2.1: VendorCapabilities + register + get_capabilities + list_models_for_vendor + the ~40 vendor registrations move into ai_client.py as a region block. Renamed internal _REGISTRY to _VENDOR_REGISTRY to avoid collision with mcp_tool_specs._REGISTRY. Importers (in src/) updated: - src/ai_client.py: removed top-level import; removed 4 local imports of list_models_for_vendor/get_capabilities (symbol now in module namespace) - src/app_controller.py: 2 sites updated to 'from src.ai_client import get_capabilities' - src/gui_2.py: 1 site updated to 'from src.ai_client import VendorCapabilities, get_capabilities' Tests updated: - 8 test_*.py files: changed 'from src.vendor_capabilities import' to 'from src.ai_client import' - tests/test_vendor_capabilities.py: _clean_registry fixture updated to reference src.ai_client._VENDOR_REGISTRY (was src.vendor_capabilities._REGISTRY) Verification: 157 tests pass across the affected files (vendor_capabilities, ai_client_tool_loop variants, openai_compatible, command_palette, diff_viewer, patch_modal, app_controller_result, app_controller_sigint, handle_reset_session, ai_loop_regressions, grok/llama/minimax provider tests). --- src/ai_client.py | 104 ++++++++++++++++++-- src/app_controller.py | 4 +- src/gui_2.py | 2 +- src/vendor_capabilities.py | 91 ----------------- tests/test_ai_client_tool_loop.py | 2 +- tests/test_ai_client_tool_loop_builder.py | 2 +- tests/test_ai_client_tool_loop_send_func.py | 2 +- tests/test_ai_loop_regressions_20260614.py | 2 +- tests/test_grok_provider.py | 2 +- tests/test_llama_provider.py | 2 +- tests/test_minimax_provider.py | 4 +- tests/test_openai_compatible.py | 2 +- tests/test_vendor_capabilities.py | 12 +-- 13 files changed, 114 insertions(+), 117 deletions(-) delete mode 100644 src/vendor_capabilities.py diff --git a/src/ai_client.py b/src/ai_client.py index 8699c6de..a1f21851 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -29,6 +29,7 @@ import sys import threading import time import tomllib +from dataclasses import dataclass # TODO(Ed): Eliminate These? from collections import deque @@ -44,16 +45,17 @@ from src import mma_prompts from src import performance_monitor from src import project_manager from src import provider_state -from src.vendor_capabilities import VendorCapabilities, get_capabilities - -# TODO(Ed): Eliminate these? from src.events import EventEmitter from src.gemini_cli_adapter import GeminiCliAdapter from src.models import FileItem, ToolPreset, BiasProfile, Tool from src.paths import get_credentials_path from src.tool_bias import ToolBiasEngine from src.tool_presets import ToolPresetManager -from src.tool_presets import ToolPresetManager + +# VendorCapabilities, get_capabilities, list_models_for_vendor, register +# are defined in this file (see '#region: Vendor Capabilities'). Previously +# imported from src/vendor_capabilities.py (deleted in +# module_taxonomy_refactor_20260627 Phase 2.1). PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] @@ -2595,7 +2597,6 @@ def _send_grok(md_content: str, user_message: str, base_dir: str, return Result(data="", errors=[_classify_openai_compatible_error(exc, source="ai_client.grok")]) def _list_grok_models() -> list[str]: - from src.vendor_capabilities import list_models_for_vendor return list_models_for_vendor("grok") def _send_minimax(md_content: str, user_message: str, base_dir: str, @@ -2753,7 +2754,6 @@ def _extract_dashscope_tool_calls(resp: Any) -> list[Metadata]: return out def _list_qwen_models() -> list[str]: - from src.vendor_capabilities import list_models_for_vendor return list_models_for_vendor("qwen") def _send_qwen(md_content: str, user_message: str, base_dir: str, @@ -3015,13 +3015,11 @@ def _send_llama_native(md_content: str, user_message: str, base_dir: str, return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(exc), source="ai_client.llama_native", original=exc)]) def _list_llama_models() -> list[str]: - from src.vendor_capabilities import list_models_for_vendor return list_models_for_vendor("llama") def _get_llama_cost_tracking() -> bool: if "localhost" in _llama_base_url or "127.0.0.1" in _llama_base_url: return False - from src.vendor_capabilities import get_capabilities try: caps = get_capabilities("llama", _model) return caps.cost_tracking @@ -3449,3 +3447,93 @@ def run_discussion_compression(discussion_text: str) -> str: return f"ERROR: Unsupported provider for discussion compression: '{p}'" #endregion: Subagent Summarization + +#region: Vendor Capabilities (moved from src/vendor_capabilities.py) +@dataclass(frozen=True) +class VendorCapabilities: + vendor: str + model: str + vision: bool = False + tool_calling: bool = True + caching: bool = False + streaming: bool = True + model_discovery: bool = True + context_window: int = 8192 + cost_tracking: bool = True + cost_input_per_mtok: float = 0.0 + cost_output_per_mtok: float = 0.0 + notes: str = '' + local: bool = False + reasoning: bool = False + structured_output: bool = False + code_execution: bool = False + web_search: bool = False + x_search: bool = False + file_search: bool = False + mcp_support: bool = False + audio: bool = False + video: bool = False + grounding: bool = False + computer_use: bool = False + +_VENDOR_REGISTRY: dict[tuple[str, str], "VendorCapabilities"] = {} + +def register(cap: "VendorCapabilities") -> None: + _VENDOR_REGISTRY[(cap.vendor, cap.model)] = cap + +def get_capabilities(vendor: str, model: str) -> "VendorCapabilities": + if (vendor, model) in _VENDOR_REGISTRY: return _VENDOR_REGISTRY[(vendor, model)] + if (vendor, '*') in _VENDOR_REGISTRY: return _VENDOR_REGISTRY[(vendor, '*')] + raise KeyError(f'No capabilities registered for vendor={vendor!r} model={model!r}') + +def list_models_for_vendor(vendor: str) -> list[str]: + return sorted({m for v, m in _VENDOR_REGISTRY if v == vendor and m != '*'}) + +register(VendorCapabilities(vendor='minimax', model='*', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.7', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.5', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20, reasoning=True)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.1', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) +register(VendorCapabilities(vendor='minimax', model='MiniMax-M2', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) +register(VendorCapabilities(vendor='grok', model='*', context_window=131072, cost_input_per_mtok=2.00, cost_output_per_mtok=10.00, web_search=True, x_search=True)) +register(VendorCapabilities(vendor='grok', model='grok-2', context_window=131072, web_search=True, x_search=True)) +register(VendorCapabilities(vendor='grok', model='grok-2-vision', vision=True, context_window=32768, web_search=True, x_search=True)) +register(VendorCapabilities(vendor='grok', model='grok-beta', context_window=131072, cost_input_per_mtok=5.00, cost_output_per_mtok=15.00, web_search=True, x_search=True)) +register(VendorCapabilities(vendor='llama', model='*', context_window=131072)) +register(VendorCapabilities(vendor='llama', model='llama-3.1-8b-instant', context_window=131072, cost_input_per_mtok=0.05, cost_output_per_mtok=0.08)) +register(VendorCapabilities(vendor='llama', model='llama-3.1-70b-versatile', context_window=131072, cost_input_per_mtok=0.59, cost_output_per_mtok=0.79)) +register(VendorCapabilities(vendor='llama', model='llama-3.1-405b-reasoning', context_window=131072, cost_input_per_mtok=3.00, cost_output_per_mtok=3.00, reasoning=True)) +register(VendorCapabilities(vendor='llama', model='llama-3.2-1b-preview', context_window=131072, cost_input_per_mtok=0.04, cost_output_per_mtok=0.04)) +register(VendorCapabilities(vendor='llama', model='llama-3.2-3b-preview', context_window=131072, cost_input_per_mtok=0.06, cost_output_per_mtok=0.06)) +register(VendorCapabilities(vendor='llama', model='llama-3.2-11b-vision-preview', vision=True, context_window=131072, cost_input_per_mtok=0.18, cost_output_per_mtok=0.18)) +register(VendorCapabilities(vendor='llama', model='llama-3.2-90b-vision-preview', vision=True, context_window=131072, cost_input_per_mtok=0.90, cost_output_per_mtok=0.90)) +register(VendorCapabilities(vendor='llama', model='llama-3.3-70b-specdec', context_window=131072, cost_input_per_mtok=0.59, cost_output_per_mtok=0.79)) +register(VendorCapabilities(vendor='qwen', model='*', context_window=32768)) +register(VendorCapabilities(vendor='qwen', model='qwen-turbo', context_window=1000000, cost_input_per_mtok=0.05, cost_output_per_mtok=0.10)) +register(VendorCapabilities(vendor='qwen', model='qwen-plus', context_window=131072, cost_input_per_mtok=0.40, cost_output_per_mtok=1.20)) +register(VendorCapabilities(vendor='qwen', model='qwen-max', context_window=32768, cost_input_per_mtok=2.00, cost_output_per_mtok=6.00)) +register(VendorCapabilities(vendor='qwen', model='qwen-long', context_window=1000000, cost_input_per_mtok=0.07, cost_output_per_mtok=0.28, caching=True, notes='qwen-long supports custom chunked long-context caching')) +register(VendorCapabilities(vendor='qwen', model='qwen-vl-plus', vision=True, context_window=131072, cost_input_per_mtok=0.21, cost_output_per_mtok=0.63)) +register(VendorCapabilities(vendor='qwen', model='qwen-vl-max', vision=True, context_window=32768, cost_input_per_mtok=0.50, cost_output_per_mtok=1.50)) +register(VendorCapabilities(vendor='qwen', model='qwen-audio', context_window=32768, cost_input_per_mtok=0.10, cost_output_per_mtok=0.30, audio=True, notes='Audio input support added 2026-06-11 (v2 matrix)')) +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-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-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-sonnet-4-6', 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-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-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-opus-4-5-20251101', 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-6', 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-7', 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-8', 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-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-fable-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='gemini', model='*', context_window=1000000, cost_input_per_mtok=1.25, cost_output_per_mtok=5.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, notes='Gemini wildcard: 1M+ context window. Per-model variations below.')) +register(VendorCapabilities(vendor='gemini', model='gemini-3.1-pro-preview', context_window=1000000, cost_input_per_mtok=3.50, cost_output_per_mtok=10.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-3-flash-preview', context_window=1000000, cost_input_per_mtok=0.15, cost_output_per_mtok=0.60, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash', context_window=1000000, cost_input_per_mtok=0.15, cost_output_per_mtok=0.60, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash-lite', context_window=1000000, cost_input_per_mtok=0.075, cost_output_per_mtok=0.30, caching=True, vision=True, grounding=True, structured_output=True)) +register(VendorCapabilities(vendor='deepseek', model='*', context_window=32768, cost_input_per_mtok=0.27, cost_output_per_mtok=1.10, reasoning=True, structured_output=True, notes='DeepSeek wildcard: V3 defaults. R1/reasoner variants below.')) +register(VendorCapabilities(vendor='deepseek', model='deepseek-v3', context_window=32768, cost_input_per_mtok=0.27, cost_output_per_mtok=1.10, structured_output=True)) +register(VendorCapabilities(vendor='deepseek', model='deepseek-reasoner', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) +register(VendorCapabilities(vendor='deepseek', model='deepseek-r1', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) +#endregion: Vendor Capabilities diff --git a/src/app_controller.py b/src/app_controller.py index c4e6bc97..22333484 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -2054,7 +2054,7 @@ class AppController: from src.personas import PersonaManager self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None) - from src.vendor_capabilities import get_capabilities + from src.ai_client import get_capabilities try: caps = get_capabilities(self.current_provider, self.current_model) except KeyError: @@ -4281,7 +4281,7 @@ class AppController: def _on_ai_stream(self, text: str) -> None: """Handles streaming text from the AI.""" self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"}) - from src.vendor_capabilities import get_capabilities + from src.ai_client import get_capabilities try: caps = get_capabilities(self.current_provider, self.current_model) except KeyError: diff --git a/src/gui_2.py b/src/gui_2.py index a7c5b26f..6bf14e4b 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -785,7 +785,7 @@ class App: #TODO(Ed): Remove Exception based errors. def _get_active_capabilities(self) -> "VendorCapabilities": - from src.vendor_capabilities import VendorCapabilities, get_capabilities + from src.ai_client import VendorCapabilities, get_capabilities #TODO(Ed): Remove Exception based errors. try: caps = get_capabilities(self.current_provider, self.current_model) diff --git a/src/vendor_capabilities.py b/src/vendor_capabilities.py deleted file mode 100644 index 04c8f31e..00000000 --- a/src/vendor_capabilities.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations -from dataclasses import dataclass - -@dataclass(frozen=True) -class VendorCapabilities: - vendor: str - model: str - vision: bool = False - tool_calling: bool = True - caching: bool = False - streaming: bool = True - model_discovery: bool = True - context_window: int = 8192 - cost_tracking: bool = True - cost_input_per_mtok: float = 0.0 - cost_output_per_mtok: float = 0.0 - notes: str = '' - # v2 fields (added 2026-06-11) - local: bool = False - reasoning: bool = False - structured_output: bool = False - code_execution: bool = False - web_search: bool = False - x_search: bool = False - file_search: bool = False - mcp_support: bool = False - audio: bool = False - video: bool = False - grounding: bool = False - computer_use: bool = False - -_REGISTRY: dict[tuple[str, str], VendorCapabilities] = {} - -def register(cap: VendorCapabilities) -> None: - _REGISTRY[(cap.vendor, cap.model)] = cap - -def get_capabilities(vendor: str, model: str) -> VendorCapabilities: - if (vendor, model) in _REGISTRY: return _REGISTRY[(vendor, model)] - if (vendor, '*') in _REGISTRY: return _REGISTRY[(vendor, '*')] - raise KeyError(f'No capabilities registered for vendor={vendor!r} model={model!r}') - -def list_models_for_vendor(vendor: str) -> list[str]: - return sorted({m for v, m in _REGISTRY if v == vendor and m != '*'}) - -register(VendorCapabilities(vendor='minimax', model='*', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) -register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.7', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20, reasoning=True)) -register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.5', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20, reasoning=True)) -register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.1', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) -register(VendorCapabilities(vendor='minimax', model='MiniMax-M2', context_window=131072, cost_input_per_mtok=0.20, cost_output_per_mtok=0.20)) -register(VendorCapabilities(vendor='grok', model='*', context_window=131072, cost_input_per_mtok=2.00, cost_output_per_mtok=10.00, web_search=True, x_search=True)) -register(VendorCapabilities(vendor='grok', model='grok-2', context_window=131072, web_search=True, x_search=True)) -register(VendorCapabilities(vendor='grok', model='grok-2-vision', vision=True, context_window=32768, web_search=True, x_search=True)) -register(VendorCapabilities(vendor='grok', model='grok-beta', context_window=131072, cost_input_per_mtok=5.00, cost_output_per_mtok=15.00, web_search=True, x_search=True)) -register(VendorCapabilities(vendor='llama', model='*', context_window=131072)) -register(VendorCapabilities(vendor='llama', model='llama-3.1-8b-instant', context_window=131072, cost_input_per_mtok=0.05, cost_output_per_mtok=0.08)) -register(VendorCapabilities(vendor='llama', model='llama-3.1-70b-versatile', context_window=131072, cost_input_per_mtok=0.59, cost_output_per_mtok=0.79)) -register(VendorCapabilities(vendor='llama', model='llama-3.1-405b-reasoning', context_window=131072, cost_input_per_mtok=3.00, cost_output_per_mtok=3.00, reasoning=True)) -register(VendorCapabilities(vendor='llama', model='llama-3.2-1b-preview', context_window=131072, cost_input_per_mtok=0.04, cost_output_per_mtok=0.04)) -register(VendorCapabilities(vendor='llama', model='llama-3.2-3b-preview', context_window=131072, cost_input_per_mtok=0.06, cost_output_per_mtok=0.06)) -register(VendorCapabilities(vendor='llama', model='llama-3.2-11b-vision-preview', vision=True, context_window=131072, cost_input_per_mtok=0.18, cost_output_per_mtok=0.18)) -register(VendorCapabilities(vendor='llama', model='llama-3.2-90b-vision-preview', vision=True, context_window=131072, cost_input_per_mtok=0.90, cost_output_per_mtok=0.90)) -register(VendorCapabilities(vendor='llama', model='llama-3.3-70b-specdec', context_window=131072, cost_input_per_mtok=0.59, cost_output_per_mtok=0.79)) -register(VendorCapabilities(vendor='qwen', model='*', context_window=32768)) -register(VendorCapabilities(vendor='qwen', model='qwen-turbo', context_window=1000000, cost_input_per_mtok=0.05, cost_output_per_mtok=0.10)) -register(VendorCapabilities(vendor='qwen', model='qwen-plus', context_window=131072, cost_input_per_mtok=0.40, cost_output_per_mtok=1.20)) -register(VendorCapabilities(vendor='qwen', model='qwen-max', context_window=32768, cost_input_per_mtok=2.00, cost_output_per_mtok=6.00)) -register(VendorCapabilities(vendor='qwen', model='qwen-long', context_window=1000000, cost_input_per_mtok=0.07, cost_output_per_mtok=0.28, caching=True, notes='qwen-long supports custom chunked long-context caching')) -register(VendorCapabilities(vendor='qwen', model='qwen-vl-plus', vision=True, context_window=131072, cost_input_per_mtok=0.21, cost_output_per_mtok=0.63)) -register(VendorCapabilities(vendor='qwen', model='qwen-vl-max', vision=True, context_window=32768, cost_input_per_mtok=0.50, cost_output_per_mtok=1.50)) -register(VendorCapabilities(vendor='qwen', model='qwen-audio', context_window=32768, cost_input_per_mtok=0.10, cost_output_per_mtok=0.30, audio=True, notes='Audio input support added 2026-06-11 (v2 matrix)')) -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-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-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-sonnet-4-6', 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-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-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-opus-4-5-20251101', 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-6', 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-7', 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-8', 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-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-fable-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='gemini', model='*', context_window=1000000, cost_input_per_mtok=1.25, cost_output_per_mtok=5.00, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True, notes='Gemini wildcard: 1M+ context window. Per-model variations below.')) -register(VendorCapabilities(vendor='gemini', model='gemini-3.1-pro-preview', context_window=1000000, cost_input_per_mtok=3.50, cost_output_per_mtok=10.50, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) -register(VendorCapabilities(vendor='gemini', model='gemini-3-flash-preview', context_window=1000000, cost_input_per_mtok=0.15, cost_output_per_mtok=0.60, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) -register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash', context_window=1000000, cost_input_per_mtok=0.15, cost_output_per_mtok=0.60, caching=True, vision=True, video=True, audio=True, grounding=True, structured_output=True)) -register(VendorCapabilities(vendor='gemini', model='gemini-2.5-flash-lite', context_window=1000000, cost_input_per_mtok=0.075, cost_output_per_mtok=0.30, caching=True, vision=True, grounding=True, structured_output=True)) -register(VendorCapabilities(vendor='deepseek', model='*', context_window=32768, cost_input_per_mtok=0.27, cost_output_per_mtok=1.10, reasoning=True, structured_output=True, notes='DeepSeek wildcard: V3 defaults. R1/reasoner variants below.')) -register(VendorCapabilities(vendor='deepseek', model='deepseek-v3', context_window=32768, cost_input_per_mtok=0.27, cost_output_per_mtok=1.10, structured_output=True)) -register(VendorCapabilities(vendor='deepseek', model='deepseek-reasoner', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) -register(VendorCapabilities(vendor='deepseek', model='deepseek-r1', context_window=32768, cost_input_per_mtok=0.55, cost_output_per_mtok=2.19, reasoning=True, structured_output=True)) diff --git a/tests/test_ai_client_tool_loop.py b/tests/test_ai_client_tool_loop.py index e6f1b4c8..7da2f6ab 100644 --- a/tests/test_ai_client_tool_loop.py +++ b/tests/test_ai_client_tool_loop.py @@ -20,7 +20,7 @@ from src.result_types import Result from src.openai_compatible import NormalizedResponse, OpenAICompatibleRequest from src.openai_schemas import UsageStats from src.ai_client import run_with_tool_loop -from src.vendor_capabilities import VendorCapabilities +from src.ai_client import VendorCapabilities @pytest.fixture def caps() -> VendorCapabilities: diff --git a/tests/test_ai_client_tool_loop_builder.py b/tests/test_ai_client_tool_loop_builder.py index 76a3437e..6d96fb02 100644 --- a/tests/test_ai_client_tool_loop_builder.py +++ b/tests/test_ai_client_tool_loop_builder.py @@ -11,7 +11,7 @@ from src.openai_compatible import NormalizedResponse, OpenAICompatibleRequest from src.openai_schemas import UsageStats from src.ai_client import run_with_tool_loop from src.result_types import Result -from src.vendor_capabilities import VendorCapabilities +from src.ai_client import VendorCapabilities def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse: return NormalizedResponse( diff --git a/tests/test_ai_client_tool_loop_send_func.py b/tests/test_ai_client_tool_loop_send_func.py index c4df65bd..6f270139 100644 --- a/tests/test_ai_client_tool_loop_send_func.py +++ b/tests/test_ai_client_tool_loop_send_func.py @@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch from src.openai_compatible import NormalizedResponse from src.openai_schemas import UsageStats from src.ai_client import run_with_tool_loop -from src.vendor_capabilities import VendorCapabilities +from src.ai_client import VendorCapabilities def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse: return NormalizedResponse( diff --git a/tests/test_ai_loop_regressions_20260614.py b/tests/test_ai_loop_regressions_20260614.py index 804bc1df..6a58f8e5 100644 --- a/tests/test_ai_loop_regressions_20260614.py +++ b/tests/test_ai_loop_regressions_20260614.py @@ -214,7 +214,7 @@ def test_fr3_minimax_thinking_in_returned_text() -> None: from src import openai_compatible as oc from src import provider_state from src.provider_state import ProviderHistory - from src.vendor_capabilities import register, VendorCapabilities + from src.ai_client import register, VendorCapabilities register(VendorCapabilities(vendor="minimax", model="MiniMax-M2.7", reasoning=True)) ai_client._model = "MiniMax-M2.7" diff --git a/tests/test_grok_provider.py b/tests/test_grok_provider.py index 0d54c203..f48fccd1 100644 --- a/tests/test_grok_provider.py +++ b/tests/test_grok_provider.py @@ -23,7 +23,7 @@ def test_send_grok_uses_xai_endpoint(monkeypatch: pytest.MonkeyPatch) -> None: assert mock_client.chat.completions.create.called def test_grok_2_vision_supports_image() -> None: - from src.vendor_capabilities import get_capabilities + from src.ai_client import get_capabilities caps = get_capabilities("grok", "grok-2-vision") assert caps.vision is True diff --git a/tests/test_llama_provider.py b/tests/test_llama_provider.py index 5b2f19ab..efb573f1 100644 --- a/tests/test_llama_provider.py +++ b/tests/test_llama_provider.py @@ -62,7 +62,7 @@ def test_llama_model_discovery_unions_ollama_and_openrouter() -> None: assert "llama-3.3-70b-specdec" in models def test_llama_3_2_vision_vision_capability() -> None: - from src.vendor_capabilities import get_capabilities + from src.ai_client import get_capabilities caps = get_capabilities("llama", "llama-3.2-11b-vision-preview") assert caps.vision is True diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 685b2a4f..aacccb13 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -42,7 +42,7 @@ def test_minimax_reasoning_extractor_used_when_caps_reasoning_true() -> None: def _fake_send(client, request, *, capabilities): captured_kwargs.append({"model": request.model}) return MagicMock(text="ok", tool_calls=[], usage=UsageStats(input_tokens=0, output_tokens=0, cache_read_tokens=0, cache_creation_tokens=0), raw_response=None) - from src.vendor_capabilities import register, VendorCapabilities + from src.ai_client import register, VendorCapabilities register(VendorCapabilities(vendor='minimax', model='MiniMax-M2.5', reasoning=True)) with patch.object(oc, "send_openai_compatible", side_effect=_fake_send), \ patch("src.ai_client._ensure_minimax_client", return_value=MagicMock()), \ @@ -54,7 +54,7 @@ def test_minimax_reasoning_extractor_omitted_when_caps_reasoning_false() -> None """caps.reasoning=False (M2/M2.1) should NOT pass the reasoning_extractor (avoid useless getattr).""" from src import openai_compatible as oc from src.openai_schemas import UsageStats - from src.vendor_capabilities import register, VendorCapabilities + from src.ai_client import register, VendorCapabilities register(VendorCapabilities(vendor='minimax', model='MiniMax-M2', reasoning=False)) captured_kwargs: list[dict] = [] def _fake_send(client, request, *, capabilities): diff --git a/tests/test_openai_compatible.py b/tests/test_openai_compatible.py index 327aa718..bca95edd 100644 --- a/tests/test_openai_compatible.py +++ b/tests/test_openai_compatible.py @@ -6,7 +6,7 @@ from src.openai_compatible import ( send_openai_compatible, ) from src.openai_schemas import ChatMessage -from src.vendor_capabilities import VendorCapabilities, register +from src.ai_client import VendorCapabilities, register @pytest.fixture def caps() -> VendorCapabilities: diff --git a/tests/test_vendor_capabilities.py b/tests/test_vendor_capabilities.py index 733ade4d..f452c481 100644 --- a/tests/test_vendor_capabilities.py +++ b/tests/test_vendor_capabilities.py @@ -1,13 +1,13 @@ import pytest -from src.vendor_capabilities import VendorCapabilities, get_capabilities, register +from src.ai_client import VendorCapabilities, get_capabilities, register @pytest.fixture(autouse=True) def _clean_registry(): - import src.vendor_capabilities - snapshot = src.vendor_capabilities._REGISTRY.copy() + import src.ai_client as _ai + snapshot = _ai._VENDOR_REGISTRY.copy() yield - src.vendor_capabilities._REGISTRY.clear() - src.vendor_capabilities._REGISTRY.update(snapshot) + _ai._VENDOR_REGISTRY.clear() + _ai._VENDOR_REGISTRY.update(snapshot) def test_registry_lookup_known_model(): caps = VendorCapabilities( @@ -217,6 +217,6 @@ def test_v2_capability_badge_helper_skips_disabled_fields() -> None: a live context, but we can verify the helper is a no-op on the no-cap case.)""" from src.gui_2 import _render_v2_capability_badges - from src.vendor_capabilities import VendorCapabilities + from src.ai_client import VendorCapabilities empty_caps = VendorCapabilities(vendor='test', model='empty') _render_v2_capability_badges(empty_caps)