Private
Public Access
Merge origin/tier2/module_taxonomy_refactor_20260627: bring in v2 SHIPPED work
Per post_module_taxonomy_de_cruft_20260627 Phase 0 prerequisite. Master is at6344b49f(pre-merge of v2 SHIPPED). This merge brings in the 18 v2 SHIPPED commits that define the destination modules (src.mma, src/project.py, src/project_files.py, src.tool_presets, src.tool_bias, src.external_editor, src.personas, src.workspace_manager, src.mcp_client) needed by the Phase 2 consumer migration in commit8f11340b. Conflicts resolved (all were import-block re-orderings between my migration's update and v2 SHIPPED's update of the same files): - src/external_editor.py: took v2 SHIPPED version (class definitions + the no-alias import pattern) - src/personas.py: took v2 SHIPPED version - src/tool_bias.py: took v2 SHIPPED version - src/tool_presets.py: took v2 SHIPPED version - src/workspace_manager.py: took v2 SHIPPED version - src/ai_client.py: took v2 SHIPPED version (removes the 'as _FIC' alias; uses 'from src.project_files import FileItem' directly per the v2 SHIPPED style) - conductor/tracks/module_taxonomy_refactor_20260627/spec.md: took HEAD version (my Phase 1 VC2 + VC10 corrections; the v2 SHIPPED version was the pre-correction spec)
This commit is contained in:
+120
-20
@@ -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,18 +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.project_files import FileItem
|
||||
from src.tool_bias import BiasProfile
|
||||
from src.tool_presets import ToolPreset, Tool
|
||||
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"]
|
||||
|
||||
@@ -188,6 +188,110 @@ _project_context_marker: str = ""
|
||||
|
||||
#endregion: Provider Configuration
|
||||
|
||||
#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
|
||||
|
||||
#region: Vendor State (moved from src/vendor_state.py)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VendorMetric:
|
||||
key: str
|
||||
label: str
|
||||
value: str
|
||||
state: str
|
||||
tooltip: str
|
||||
|
||||
#endregion: Vendor State
|
||||
|
||||
#region: System Prompt Management
|
||||
|
||||
def set_custom_system_prompt(prompt: str) -> None:
|
||||
@@ -412,7 +516,7 @@ def set_provider(provider: str, model: str, validate: bool = True) -> None:
|
||||
When validate is True (default), the model is checked against the provider's
|
||||
LIVE model list, which for gemini_cli/minimax means a blocking subprocess /
|
||||
network call (and importing the provider SDK). Pass validate=False during
|
||||
startup so the GUI's first frame is not blocked — AppController._fetch_models
|
||||
startup so the GUI's first frame is not blocked ΓÇö AppController._fetch_models
|
||||
corrects the model against the live list shortly after, off the main thread.
|
||||
"""
|
||||
global _provider, _model
|
||||
@@ -726,7 +830,7 @@ def _parse_tool_args_result(tool_args_str: str) -> Result[Metadata]:
|
||||
On JSON parse failure, returns Result(data={}, errors=[ErrorInfo(...)]).
|
||||
The legacy caller accumulates errors into file_errors and falls back to
|
||||
empty args (preserving original behavior). Per TIER1_REVIEW 2026-06-20:
|
||||
empty-default is NOT a drain — the caller must observe the errors.
|
||||
empty-default is NOT a drain ΓÇö the caller must observe the errors.
|
||||
"""
|
||||
try:
|
||||
return Result(data=json.loads(tool_args_str))
|
||||
@@ -1305,7 +1409,7 @@ def _list_anthropic_models_result() -> Result[list[str]]:
|
||||
The previous version had:
|
||||
except Exception as exc:
|
||||
raise _classify_anthropic_error(exc) from exc
|
||||
which raised an ErrorInfo as an Exception — a runtime bug. This
|
||||
which raised an ErrorInfo as an Exception ΓÇö a runtime bug. This
|
||||
migration follows the Phase 9 redo precedent: convert to Result[T].
|
||||
"""
|
||||
try:
|
||||
@@ -2563,8 +2667,8 @@ def _send_grok(md_content: str, user_message: str, base_dir: str,
|
||||
if file_items:
|
||||
for fi in file_items:
|
||||
if fi.get("is_image") and fi.get("base64_data"):
|
||||
from src.project_files import FileItem as _FIC
|
||||
fi_item = fi if isinstance(fi, _FIC) else _FIC.from_dict(fi)
|
||||
from src.project_files import FileItem
|
||||
fi_item = fi if isinstance(fi, FileItem) else FileItem.from_dict(fi)
|
||||
user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}"
|
||||
if discussion_history and not history:
|
||||
history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"})
|
||||
@@ -2597,7 +2701,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,
|
||||
@@ -2755,7 +2858,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,
|
||||
@@ -2807,8 +2909,8 @@ def _send_qwen(md_content: str, user_message: str, base_dir: str,
|
||||
if file_items:
|
||||
for fi in file_items:
|
||||
if fi.get("is_image") and fi.get("base64_data"):
|
||||
from src.project_files import FileItem as _FIC
|
||||
fi_item = fi if isinstance(fi, _FIC) else _FIC.from_dict(fi)
|
||||
from src.project_files import FileItem
|
||||
fi_item = fi if isinstance(fi, FileItem) else FileItem.from_dict(fi)
|
||||
user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}"
|
||||
if discussion_history and not history:
|
||||
history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"})
|
||||
@@ -2900,8 +3002,8 @@ def _send_llama(md_content: str, user_message: str, base_dir: str,
|
||||
if file_items:
|
||||
for fi in file_items:
|
||||
if fi.get("is_image") and fi.get("base64_data"):
|
||||
from src.project_files import FileItem as _FIC
|
||||
fi_item = fi if isinstance(fi, _FIC) else _FIC.from_dict(fi)
|
||||
from src.project_files import FileItem
|
||||
fi_item = fi if isinstance(fi, FileItem) else FileItem.from_dict(fi)
|
||||
user_content = f"[IMAGE: {fi_item.path or 'attachment'}]\n{user_content}"
|
||||
if discussion_history and not history:
|
||||
history.append({"role": "user", "content": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"})
|
||||
@@ -3017,13 +3119,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
|
||||
|
||||
Reference in New Issue
Block a user