cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.7.9.0 — model_router.py: the Smart Model Router + Request Classifier.
|
|
3
|
+
|
|
4
|
+
CAT should not blindly send every request to the same model through the
|
|
5
|
+
same heavyweight path. This module is the brain that sits between the
|
|
6
|
+
user's message and the pipeline:
|
|
7
|
+
|
|
8
|
+
USER MESSAGE
|
|
9
|
+
↓
|
|
10
|
+
classify() — task types (pure regex/heuristics, no LLM call:
|
|
11
|
+
↓ classification itself must cost microseconds,
|
|
12
|
+
│ not seconds)
|
|
13
|
+
route()
|
|
14
|
+
├── fast_path — simple chat: skip memory/tools/multi-agent/vision
|
|
15
|
+
├── agent_path — real tool loop (coding / agentic execution)
|
|
16
|
+
├── vision_path — image(s) attached → vision-capable model chosen
|
|
17
|
+
└── multi_ai_path — genuinely complex tasks only
|
|
18
|
+
|
|
19
|
+
The router also owns the MODEL CAPABILITY REGISTRY: every configured
|
|
20
|
+
provider/model (primary + backups) is described by ModelCapabilities
|
|
21
|
+
(text/vision/tools/streaming/reasoning/context_window/latency/cost/
|
|
22
|
+
reliability) so routing decisions never have to be discovered "after a
|
|
23
|
+
request fails".
|
|
24
|
+
|
|
25
|
+
Everything here is heuristic and local: no network calls on the routing
|
|
26
|
+
path (availability probing would add latency — the opposite of the
|
|
27
|
+
goal). Availability/failover stays with aicore's existing backup chain;
|
|
28
|
+
the router just picks the BEST CAPABLE candidate order.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
print("This is a library file and is not meant to be run directly.")
|
|
33
|
+
import sys
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
import time
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
40
|
+
|
|
41
|
+
# ----------------------------------------------------------------- types --
|
|
42
|
+
|
|
43
|
+
TASK_TYPES = (
|
|
44
|
+
"simple_chat", "coding", "debugging", "research", "mathematics",
|
|
45
|
+
"long_context", "vision", "document_analysis", "multimodal",
|
|
46
|
+
"planning", "agentic_execution", "multi_agent",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Pipeline paths the router can choose between.
|
|
50
|
+
PATH_FAST = "fast"
|
|
51
|
+
PATH_STREAM = "stream" # plain streaming chat (default)
|
|
52
|
+
PATH_AGENT = "agent" # tool-calling loop
|
|
53
|
+
PATH_VISION = "vision" # image analysis
|
|
54
|
+
PATH_MULTI_AI = "multi_ai" # orchestrated team
|
|
55
|
+
|
|
56
|
+
# Privacy & routing policies (CAT Master Architecture)
|
|
57
|
+
POLICY_LOCAL_FIRST = "local_first"
|
|
58
|
+
POLICY_CLOUD_FIRST = "cloud_first"
|
|
59
|
+
POLICY_BEST_AVAILABLE = "best_available"
|
|
60
|
+
POLICY_NEVER_CLOUD = "never_cloud"
|
|
61
|
+
POLICIES = (POLICY_LOCAL_FIRST, POLICY_CLOUD_FIRST, POLICY_BEST_AVAILABLE, POLICY_NEVER_CLOUD)
|
|
62
|
+
|
|
63
|
+
# v0.7.9.5 complexity classes (spec #4): one coarse label per request so
|
|
64
|
+
# the execution path AND the timeout budget both key off the same
|
|
65
|
+
# classification — simple prompt → simple path, complex prompt → complex
|
|
66
|
+
# path. Pure-local, microseconds.
|
|
67
|
+
CLASS_TRIVIAL = "TRIVIAL" # "hi", "2+2", "thanks" → direct answer
|
|
68
|
+
CLASS_SIMPLE = "SIMPLE" # "what is recursion?" → one efficient call
|
|
69
|
+
CLASS_NORMAL = "NORMAL" # coding/debugging questions → model + tools
|
|
70
|
+
CLASS_COMPLEX = "COMPLEX" # long context / docs / vision analysis
|
|
71
|
+
CLASS_AGENT = "AGENT" # workspace/build tasks → agent workflow
|
|
72
|
+
CLASS_RESEARCH = "RESEARCH" # web research → retrieval-heavy path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class RouteDecision:
|
|
77
|
+
"""What the router decided for one user message."""
|
|
78
|
+
path: str = PATH_STREAM
|
|
79
|
+
task_types: Set[str] = field(default_factory=set)
|
|
80
|
+
reason: str = ""
|
|
81
|
+
# When images are attached and the PRIMARY provider/model cannot see,
|
|
82
|
+
# this carries a vision-capable backup config to use instead.
|
|
83
|
+
vision_config: Optional[dict] = None
|
|
84
|
+
use_tools: bool = False
|
|
85
|
+
fast: bool = False
|
|
86
|
+
use_multi_agent: bool = False
|
|
87
|
+
# TRIVIAL / SIMPLE / NORMAL / COMPLEX / AGENT / RESEARCH
|
|
88
|
+
size_class: str = CLASS_NORMAL
|
|
89
|
+
privacy_policy: str = POLICY_LOCAL_FIRST
|
|
90
|
+
candidate_chain: List[dict] = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class ModelCapabilities:
|
|
95
|
+
"""Requirement #8: explicit per-model capability record. The router
|
|
96
|
+
consults this BEFORE sending a request; capabilities are never
|
|
97
|
+
discovered only after a failure."""
|
|
98
|
+
provider: str = ""
|
|
99
|
+
model: str = ""
|
|
100
|
+
api_style: str = "openai"
|
|
101
|
+
text: bool = True
|
|
102
|
+
vision: bool = False
|
|
103
|
+
tools: bool = True
|
|
104
|
+
streaming: bool = True
|
|
105
|
+
reasoning: bool = False
|
|
106
|
+
context_window: int = 32000
|
|
107
|
+
latency_score: float = 0.5 # 0 slow .. 1 instant (heuristic prior)
|
|
108
|
+
cost_score: float = 0.5 # 0 cheap .. 1 expensive (heuristic prior)
|
|
109
|
+
reliability: float = 0.7 # heuristic prior; nudged by observed fails
|
|
110
|
+
is_backup: bool = False
|
|
111
|
+
|
|
112
|
+
def score_for(self, needed_vision: bool = False,
|
|
113
|
+
needed_reasoning: bool = False,
|
|
114
|
+
needed_long_ctx: bool = False,
|
|
115
|
+
wants_fast: bool = False) -> float:
|
|
116
|
+
"""Higher is better fit for the stated requirements."""
|
|
117
|
+
if needed_vision and not self.vision:
|
|
118
|
+
return -1.0
|
|
119
|
+
if needed_long_ctx and self.context_window < 60000:
|
|
120
|
+
return -1.0
|
|
121
|
+
s = 1.0
|
|
122
|
+
if needed_vision:
|
|
123
|
+
s += 0.5
|
|
124
|
+
if needed_reasoning and self.reasoning:
|
|
125
|
+
s += 0.4
|
|
126
|
+
if wants_fast:
|
|
127
|
+
s += self.latency_score * 0.6
|
|
128
|
+
s -= self.cost_score * 0.15
|
|
129
|
+
else:
|
|
130
|
+
s += self.reliability * 0.2
|
|
131
|
+
if needed_reasoning:
|
|
132
|
+
s += min(self.context_window, 200000) / 500000.0
|
|
133
|
+
return s
|
|
134
|
+
|
|
135
|
+
def to_dict(self) -> dict:
|
|
136
|
+
return {
|
|
137
|
+
"provider": self.provider,
|
|
138
|
+
"model": self.model,
|
|
139
|
+
"api_style": self.api_style,
|
|
140
|
+
"text": self.text,
|
|
141
|
+
"vision": self.vision,
|
|
142
|
+
"tools": self.tools,
|
|
143
|
+
"streaming": self.streaming,
|
|
144
|
+
"reasoning": self.reasoning,
|
|
145
|
+
"context_window": self.context_window,
|
|
146
|
+
"latency_score": self.latency_score,
|
|
147
|
+
"cost_score": self.cost_score,
|
|
148
|
+
"reliability": self.reliability,
|
|
149
|
+
"is_backup": self.is_backup,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------- classification --
|
|
154
|
+
|
|
155
|
+
_RE_GREETING = re.compile(
|
|
156
|
+
r"^\s*(hi|hello|hey|yo|sup|good (morning|evening|afternoon)|thanks|thank you|"
|
|
157
|
+
r"ok(ay)?|cool|nice|great|bye)\b[\s!.?]*$", re.I)
|
|
158
|
+
|
|
159
|
+
_RE_SIMPLE_Q = re.compile(
|
|
160
|
+
r"\b(what is|what's|who is|when is|where is|define|explain|meaning of|"
|
|
161
|
+
r"how do you spell|translate)\b.{0,120}\?", re.I)
|
|
162
|
+
|
|
163
|
+
_RE_ARITH = re.compile(r"^\s*[\d\s().+\-*/^%]+$")
|
|
164
|
+
# "what is 2+2", "12*7" etc. — tiny arithmetic, no tools needed.
|
|
165
|
+
_RE_TINY_MATH = re.compile(
|
|
166
|
+
r"\b(?:what(?:'s| is)\s+)?\d+[\s]*[+\-*/x×÷^%][\s]*\d+", re.I)
|
|
167
|
+
|
|
168
|
+
_RE_CODING = re.compile(
|
|
169
|
+
r"\b(code|coding|program|script|function|class |refactor|implement|"
|
|
170
|
+
r"build (me |us )?(a|an|the)? ?(website|web ?app|app|site|page|dashboard|game|api|server|cli|bot)|"
|
|
171
|
+
r"create (a|an|the)? ?(website|web ?app|app|site|page|component|file|module|package)|"
|
|
172
|
+
r"react|vue|angular|django|flask|fastapi|node|express|html|css|javascript|typescript|python|"
|
|
173
|
+
r"java\b|c\+\+|c#|rust|golang|sql query|database schema|unit test|pytest|npm|pip install)\b",
|
|
174
|
+
re.I)
|
|
175
|
+
|
|
176
|
+
_RE_DEBUGGING = re.compile(
|
|
177
|
+
r"\b(debug|bug|error|exception|traceback|stack trace|crash|not working|"
|
|
178
|
+
r"doesn'?t work|fails?|fix this|why is this (broken|failing)|"
|
|
179
|
+
r"undefined|null pointer|segfault|lint)\b", re.I)
|
|
180
|
+
|
|
181
|
+
_RE_RESEARCH = re.compile(
|
|
182
|
+
r"\b(research|latest|recent|news|current|202\d|compare|vs\.?|versus|"
|
|
183
|
+
r"search (the web|online)|look up|sources?|cite|state of the art|"
|
|
184
|
+
r"best (library|framework|tool)|documentation)\b", re.I)
|
|
185
|
+
|
|
186
|
+
_RE_MATH = re.compile(
|
|
187
|
+
r"\b(solve|equation|integral|derivative|limit|matrix|probability|"
|
|
188
|
+
r"calculus|algebra|geometry|trigonometry|prove|theorem|"
|
|
189
|
+
r"first[- ]order|second[- ]order|kinetics|mole|stoichiometr|nernst|"
|
|
190
|
+
r"half[- ]life|arrhenius|ph\b|equilibrium)\b", re.I)
|
|
191
|
+
|
|
192
|
+
_RE_PLANNING = re.compile(
|
|
193
|
+
r"\b(plan|roadmap|strategy|milestones?|architecture design|design (a|an) "
|
|
194
|
+
r"(system|architecture|approach)|step[- ]by[- ]step plan|brainstorm|proposal)\b",
|
|
195
|
+
re.I)
|
|
196
|
+
|
|
197
|
+
_RE_MULTI_AGENT = re.compile(
|
|
198
|
+
r"\b(review (this|the) (whole |entire )?(project|codebase|architecture)|"
|
|
199
|
+
r"(refactor|improve|overhaul).{0,40}(project|codebase|architecture)|"
|
|
200
|
+
r"across (the )?(whole |entire )?(project|codebase)|multi[- ]agent|"
|
|
201
|
+
r"use (multiple|several|3) (agents|ais|models)|team (up|of agents))\b",
|
|
202
|
+
re.I)
|
|
203
|
+
|
|
204
|
+
_RE_AGENTIC = re.compile(
|
|
205
|
+
r"\b(create|write|make|generate|build|install|delete|move|rename|organize|run|execute)"
|
|
206
|
+
r"\b[^?.!]{0,60}\b(file|folder|directory|script|package|command|test)s?\b", re.I)
|
|
207
|
+
_RE_FILE_OP = re.compile(
|
|
208
|
+
r"\b(read|open|list|summarize) (this |the |that )?(file|folder|directory|workspace)\b", re.I)
|
|
209
|
+
|
|
210
|
+
_RE_LONG_CTX = re.compile(
|
|
211
|
+
r"\b(entire (document|codebase|book|pdf)|whole (document|file|report)|"
|
|
212
|
+
r"large document|long document|all \d+ (pages|files)|summarize .{0,30}(document|chapter|paper))\b",
|
|
213
|
+
re.I)
|
|
214
|
+
|
|
215
|
+
_RE_DOC_ANALYSIS = re.compile(
|
|
216
|
+
r"\b(analyz|extract|parse|ocr|read) .{0,40}(image|screenshot|scan|document|receipt|invoice|table|chart|diagram)\b",
|
|
217
|
+
re.I)
|
|
218
|
+
|
|
219
|
+
_RE_EXTRACT_ALL = re.compile(
|
|
220
|
+
r"\b(extract (and explain |all )?(everything|all|important|information|text|data)|"
|
|
221
|
+
r"detailed (analysis|extraction)|full(y)? (analyz|extract)|transcribe)\b", re.I)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def classify(prompt: str, attachments: Optional[List] = None,
|
|
225
|
+
mode: Optional[str] = None) -> Set[str]:
|
|
226
|
+
"""Pure-local task classification. Returns a set of TASK_TYPES.
|
|
227
|
+
Costs microseconds — regex only, deliberately NO LLM call here."""
|
|
228
|
+
text = (prompt or "").strip()
|
|
229
|
+
types: Set[str] = set()
|
|
230
|
+
low = text.lower()
|
|
231
|
+
|
|
232
|
+
has_image = any(getattr(a, "kind", "") == "image" or
|
|
233
|
+
str(getattr(a, "extension", "")).lower() in
|
|
234
|
+
(".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp",
|
|
235
|
+
".tif", ".tiff")
|
|
236
|
+
for a in (attachments or []))
|
|
237
|
+
has_any_attachment = bool(attachments)
|
|
238
|
+
|
|
239
|
+
if len(text) <= 140 and (_RE_GREETING.match(text) or _RE_SIMPLE_Q.match(text)
|
|
240
|
+
or _RE_ARITH.match(text)):
|
|
241
|
+
types.add("simple_chat")
|
|
242
|
+
|
|
243
|
+
if _RE_TINY_MATH.search(low) and len(text) < 80:
|
|
244
|
+
types.add("simple_chat")
|
|
245
|
+
types.add("mathematics")
|
|
246
|
+
|
|
247
|
+
if _RE_CODING.search(low):
|
|
248
|
+
types.update(("coding",))
|
|
249
|
+
if _RE_DEBUGGING.search(low):
|
|
250
|
+
types.add("debugging")
|
|
251
|
+
if _RE_RESEARCH.search(low):
|
|
252
|
+
types.add("research")
|
|
253
|
+
if _RE_MATH.search(low):
|
|
254
|
+
types.add("mathematics")
|
|
255
|
+
if _RE_PLANNING.search(low):
|
|
256
|
+
types.add("planning")
|
|
257
|
+
if _RE_LONG_CTX.search(low) or (has_any_attachment and len(text) > 400):
|
|
258
|
+
types.add("long_context")
|
|
259
|
+
if has_image:
|
|
260
|
+
types.update(("vision", "multimodal"))
|
|
261
|
+
if _RE_DOC_ANALYSIS.search(low) or _RE_EXTRACT_ALL.search(low):
|
|
262
|
+
types.add("document_analysis")
|
|
263
|
+
elif has_any_attachment:
|
|
264
|
+
types.add("document_analysis")
|
|
265
|
+
if _RE_AGENTIC.search(low) or _RE_FILE_OP.search(low):
|
|
266
|
+
types.add("agentic_execution")
|
|
267
|
+
if _RE_MULTI_AGENT.search(low):
|
|
268
|
+
types.add("multi_agent")
|
|
269
|
+
if mode in ("agent", "build"):
|
|
270
|
+
types.add("agentic_execution")
|
|
271
|
+
if mode == "debugger":
|
|
272
|
+
types.add("debugging")
|
|
273
|
+
if mode == "research":
|
|
274
|
+
types.add("research")
|
|
275
|
+
if mode == "plan":
|
|
276
|
+
types.add("planning")
|
|
277
|
+
if not types:
|
|
278
|
+
types.add("simple_chat" if len(text) < 60 else "research")
|
|
279
|
+
return types
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def complexity_score(types: Set[str], prompt: str) -> float:
|
|
283
|
+
"""0..1 rough complexity estimate used for dynamic multi-AI team sizing
|
|
284
|
+
and the fast/heavy path decision."""
|
|
285
|
+
score = 0.15
|
|
286
|
+
heavy = {"coding": 0.25, "agentic_execution": 0.25, "multi_agent": 0.35,
|
|
287
|
+
"long_context": 0.2, "vision": 0.15, "document_analysis": 0.1,
|
|
288
|
+
"planning": 0.1, "debugging": 0.15}
|
|
289
|
+
for t in types:
|
|
290
|
+
score += heavy.get(t, 0.05)
|
|
291
|
+
words = len((prompt or "").split())
|
|
292
|
+
if words > 60:
|
|
293
|
+
score += 0.1
|
|
294
|
+
if words > 150:
|
|
295
|
+
score += 0.1
|
|
296
|
+
return min(1.0, score)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def complexity_class(types: Set[str], prompt: str,
|
|
300
|
+
mode: Optional[str] = None) -> str:
|
|
301
|
+
"""Map the classification to one coarse execution class (spec #4).
|
|
302
|
+
Drives both the pipeline path AND the timeout size class."""
|
|
303
|
+
p = (prompt or "").strip()
|
|
304
|
+
# Explicit agent/build intent or agentic work always wins.
|
|
305
|
+
if "multi_agent" in types or "agentic_execution" in types \
|
|
306
|
+
or "planning" in types or mode in ("agent", "build"):
|
|
307
|
+
return CLASS_AGENT
|
|
308
|
+
if "research" in types:
|
|
309
|
+
return CLASS_RESEARCH
|
|
310
|
+
if types & {"vision", "document_analysis", "long_context", "multimodal"}:
|
|
311
|
+
return CLASS_COMPLEX
|
|
312
|
+
if len(p) <= 25 and (types <= {"simple_chat"} or not types):
|
|
313
|
+
return CLASS_TRIVIAL
|
|
314
|
+
if types <= {"simple_chat", "mathematics"} and len(p) < 120:
|
|
315
|
+
return CLASS_SIMPLE
|
|
316
|
+
if types & {"coding", "debugging", "mathematics"}:
|
|
317
|
+
return CLASS_NORMAL
|
|
318
|
+
return CLASS_NORMAL
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# Timeout budget per class (aicore.request_timeouts consumes the same
|
|
322
|
+
# labels): trivial prompts fail fast instead of hanging on '...'.
|
|
323
|
+
_CLASS_TO_SIZE = {
|
|
324
|
+
CLASS_TRIVIAL: "simple",
|
|
325
|
+
CLASS_SIMPLE: "simple",
|
|
326
|
+
CLASS_NORMAL: "normal",
|
|
327
|
+
CLASS_COMPLEX: "normal",
|
|
328
|
+
CLASS_AGENT: "large",
|
|
329
|
+
CLASS_RESEARCH: "normal",
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def timeout_size_class(complexity_cls: str) -> str:
|
|
334
|
+
return _CLASS_TO_SIZE.get(complexity_cls, "normal")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------- capability registry --
|
|
338
|
+
|
|
339
|
+
_VISION_STYLES = ("openai", "anthropic", "gemini")
|
|
340
|
+
|
|
341
|
+
_NON_VISION_HINTS = ("gpt-3.5", "llama", "mixtral", "mistral", "deepseek",
|
|
342
|
+
"phi-3", "phi3", "qwen", "command", "gemma", "granite",
|
|
343
|
+
"codex", "o1-mini", "o3-mini")
|
|
344
|
+
_VISION_HINTS = ("gpt-4o", "gpt-4.1", "gpt-5", "o3", "o4", "claude-3",
|
|
345
|
+
"claude-4", "gemini-1.5", "gemini-2.0", "gemini-2.5",
|
|
346
|
+
"gemini-3", "qwen2.5-vl", "llava", "llama-3.2-vision",
|
|
347
|
+
"pixtral", "vision")
|
|
348
|
+
_REASONING_HINTS = ("o1", "o3", "o4", "r1", "thinking", "reason", "qwq",
|
|
349
|
+
"claude-3.7", "claude-4", "gpt-5", "deepseek-r")
|
|
350
|
+
_FAST_HINTS = ("mini", "flash", "lite", "small", "turbo", "instant", "haiku",
|
|
351
|
+
"nano", "8b", "7b")
|
|
352
|
+
_EXPENSIVE_HINTS = ("opus", "gpt-5", "o3", "ultra", "claude-4")
|
|
353
|
+
_BIGCTX_HINTS = ("gemini", "claude-3", "claude-4", "gpt-4.1", "gpt-5")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
_registry_cache: Optional[Tuple[float, List[ModelCapabilities]]] = None
|
|
357
|
+
_REGISTRY_TTL = 30.0 # seconds — config rarely changes mid-session
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def invalidate_cache() -> None:
|
|
361
|
+
"""Invalidate cached available configs so router immediately picks up
|
|
362
|
+
new primary or backup model choices without waiting for TTL."""
|
|
363
|
+
global _registry_cache
|
|
364
|
+
_registry_cache = None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def capabilities_for(provider: str, model: str = "") -> ModelCapabilities:
|
|
368
|
+
"""Public helper to derive capabilities for a given provider and model."""
|
|
369
|
+
return _capabilities_for({"provider": provider, "model": model})
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _capabilities_for(config: dict, is_backup: bool = False) -> ModelCapabilities:
|
|
373
|
+
"""Derive ModelCapabilities from a provider config dict. Pure name/
|
|
374
|
+
style heuristics plus curated context-window metadata — no network."""
|
|
375
|
+
provider = str(config.get("provider", "")).lower()
|
|
376
|
+
model = str(config.get("model", "")).lower()
|
|
377
|
+
api_style = str(config.get("api_style", "") or "").lower()
|
|
378
|
+
try:
|
|
379
|
+
from . import aicore as _a
|
|
380
|
+
info = _a.PROVIDERS.get(provider)
|
|
381
|
+
api_style = api_style or ((info["api_style"] if info else "openai") or "openai")
|
|
382
|
+
except Exception:
|
|
383
|
+
api_style = api_style or "openai"
|
|
384
|
+
|
|
385
|
+
caps = ModelCapabilities(provider=provider, model=model,
|
|
386
|
+
api_style=api_style, is_backup=is_backup)
|
|
387
|
+
caps.vision = (api_style in _VISION_STYLES
|
|
388
|
+
and not any(h in model for h in _NON_VISION_HINTS)
|
|
389
|
+
and any(h in model for h in _VISION_HINTS))
|
|
390
|
+
caps.tools = api_style != "unsupported"
|
|
391
|
+
caps.streaming = True
|
|
392
|
+
caps.reasoning = any(h in model for h in _REASONING_HINTS)
|
|
393
|
+
caps.latency_score = 0.85 if any(h in model for h in _FAST_HINTS) else 0.45
|
|
394
|
+
if provider == "ollama":
|
|
395
|
+
caps.latency_score = max(0.1, caps.latency_score - 0.25)
|
|
396
|
+
caps.cost_score = 0.0
|
|
397
|
+
if provider == "groq":
|
|
398
|
+
caps.latency_score = min(1.0, caps.latency_score + 0.2)
|
|
399
|
+
caps.cost_score = 0.8 if any(h in model for h in _EXPENSIVE_HINTS) else \
|
|
400
|
+
(0.3 if any(h in model for h in _FAST_HINTS) else 0.5)
|
|
401
|
+
try:
|
|
402
|
+
from . import aicore as _a
|
|
403
|
+
caps.context_window = int(_a.context_window_for(model) or 32000)
|
|
404
|
+
except Exception:
|
|
405
|
+
caps.context_window = 200000 if any(h in model for h in _BIGCTX_HINTS) else 32000
|
|
406
|
+
return caps
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def available_configs(force_refresh: bool = False) -> List[ModelCapabilities]:
|
|
410
|
+
"""Every configured model (primary first, then enabled backups), with
|
|
411
|
+
capabilities. Cached briefly — building it must stay off the hot path."""
|
|
412
|
+
global _registry_cache
|
|
413
|
+
now = time.time()
|
|
414
|
+
if not force_refresh and _registry_cache is not None:
|
|
415
|
+
ts, items = _registry_cache
|
|
416
|
+
if now - ts < _REGISTRY_TTL:
|
|
417
|
+
return items
|
|
418
|
+
out: List[ModelCapabilities] = []
|
|
419
|
+
try:
|
|
420
|
+
from . import aicore as _a
|
|
421
|
+
cfg = _a.load_config() or {}
|
|
422
|
+
if cfg.get("provider"):
|
|
423
|
+
out.append(_capabilities_for(cfg, is_backup=False))
|
|
424
|
+
try:
|
|
425
|
+
from .providers.provider_manager import backup_configs
|
|
426
|
+
for bcfg, _entry in backup_configs():
|
|
427
|
+
out.append(_capabilities_for(bcfg, is_backup=True))
|
|
428
|
+
except Exception:
|
|
429
|
+
pass
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
_registry_cache = (now, out)
|
|
433
|
+
return out
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def find_vision_capable(preferred_config: Optional[dict] = None
|
|
437
|
+
) -> Tuple[Optional[dict], Optional[ModelCapabilities]]:
|
|
438
|
+
"""Requirement #19: when the active model can't see an image, find a
|
|
439
|
+
configured model that CAN. Returns (config_dict_or_None, caps_or_None).
|
|
440
|
+
Never raises; (None, None) means honestly 'no vision-capable model is
|
|
441
|
+
configured'."""
|
|
442
|
+
configs = available_configs()
|
|
443
|
+
if preferred_config:
|
|
444
|
+
pref_caps = _capabilities_for(preferred_config)
|
|
445
|
+
if pref_caps.vision:
|
|
446
|
+
return preferred_config, pref_caps
|
|
447
|
+
for caps in configs:
|
|
448
|
+
if caps.vision:
|
|
449
|
+
try:
|
|
450
|
+
if caps.is_backup:
|
|
451
|
+
from .providers.provider_manager import backup_configs
|
|
452
|
+
for bcfg, _e in backup_configs():
|
|
453
|
+
if str(bcfg.get("provider", "")).lower() == caps.provider \
|
|
454
|
+
and str(bcfg.get("model", "")).lower() == caps.model:
|
|
455
|
+
return bcfg, caps
|
|
456
|
+
else:
|
|
457
|
+
from . import aicore as _a
|
|
458
|
+
return _a.load_config(), caps
|
|
459
|
+
except Exception:
|
|
460
|
+
continue
|
|
461
|
+
return None, None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_privacy_policy() -> str:
|
|
465
|
+
"""Read the active privacy and routing policy from central config."""
|
|
466
|
+
try:
|
|
467
|
+
from . import config as _cfg
|
|
468
|
+
c = _cfg.get_config()
|
|
469
|
+
p = getattr(c, "privacy_policy", POLICY_LOCAL_FIRST)
|
|
470
|
+
if p in POLICIES:
|
|
471
|
+
return p
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
return POLICY_LOCAL_FIRST
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def set_privacy_policy(policy: str) -> None:
|
|
478
|
+
"""Update and persist the active privacy and routing policy."""
|
|
479
|
+
if policy not in POLICIES:
|
|
480
|
+
return
|
|
481
|
+
try:
|
|
482
|
+
from . import config as _cfg
|
|
483
|
+
c = _cfg.get_config()
|
|
484
|
+
c.privacy_policy = policy
|
|
485
|
+
_cfg.save_config(c)
|
|
486
|
+
invalidate_cache()
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def build_candidate_chain(task_types: Set[str], prompt: str = "", policy: Optional[str] = None) -> List[dict]:
|
|
492
|
+
"""Return ordered candidate model configs adhering to the selected privacy policy.
|
|
493
|
+
|
|
494
|
+
- never_cloud: Strict privacy — filters out all non-local providers.
|
|
495
|
+
- local_first: Prioritizes local models; cloud models act as secondary fallback.
|
|
496
|
+
- cloud_first: Prioritizes high-end cloud providers; local models act as offline fallback.
|
|
497
|
+
- best_available: Scores candidates purely on capability and benchmark fit.
|
|
498
|
+
"""
|
|
499
|
+
pol = policy or get_privacy_policy()
|
|
500
|
+
caps_list = available_configs()
|
|
501
|
+
|
|
502
|
+
needed_vision = "vision" in task_types
|
|
503
|
+
needed_reasoning = bool({"reasoning", "mathematics", "debugging"} & task_types)
|
|
504
|
+
needed_long_ctx = "long_context" in task_types
|
|
505
|
+
wants_fast = "simple_chat" in task_types
|
|
506
|
+
|
|
507
|
+
local_providers = {"ollama", "local", "vllm"}
|
|
508
|
+
|
|
509
|
+
scored_candidates = []
|
|
510
|
+
for c in caps_list:
|
|
511
|
+
is_local = c.provider.lower() in local_providers
|
|
512
|
+
if pol == POLICY_NEVER_CLOUD and not is_local:
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
base_score = c.score_for(
|
|
516
|
+
needed_vision=needed_vision,
|
|
517
|
+
needed_reasoning=needed_reasoning,
|
|
518
|
+
needed_long_ctx=needed_long_ctx,
|
|
519
|
+
wants_fast=wants_fast,
|
|
520
|
+
)
|
|
521
|
+
if base_score < 0:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
priority = 0.0
|
|
525
|
+
if pol == POLICY_LOCAL_FIRST:
|
|
526
|
+
priority = 10.0 if is_local else 0.0
|
|
527
|
+
elif pol == POLICY_CLOUD_FIRST:
|
|
528
|
+
priority = 10.0 if not is_local else 0.0
|
|
529
|
+
elif pol == POLICY_BEST_AVAILABLE:
|
|
530
|
+
priority = 5.0 * (1.0 if not is_local else 0.8)
|
|
531
|
+
|
|
532
|
+
final_score = priority + base_score
|
|
533
|
+
scored_candidates.append((final_score, c))
|
|
534
|
+
|
|
535
|
+
scored_candidates.sort(key=lambda x: x[0], reverse=True)
|
|
536
|
+
return [c.to_dict() for _, c in scored_candidates]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# ----------------------------------------------------------------- route --
|
|
540
|
+
|
|
541
|
+
def route(prompt: str, attachments: Optional[List] = None,
|
|
542
|
+
mode: Optional[str] = None,
|
|
543
|
+
workflow_hint: Optional[str] = None) -> RouteDecision:
|
|
544
|
+
"""The one function the UI/pipeline calls per user message. Returns a
|
|
545
|
+
RouteDecision choosing the pipeline path + best capable model config
|
|
546
|
+
(for vision rerouting). Deliberately cheap: pure local heuristics."""
|
|
547
|
+
t0 = time.perf_counter()
|
|
548
|
+
types = classify(prompt, attachments, mode)
|
|
549
|
+
pol = get_privacy_policy()
|
|
550
|
+
dec = RouteDecision(task_types=types, privacy_policy=pol)
|
|
551
|
+
dec.candidate_chain = build_candidate_chain(types, prompt, pol)
|
|
552
|
+
has_image = "vision" in types
|
|
553
|
+
complexity = complexity_score(types, prompt)
|
|
554
|
+
dec.size_class = timeout_size_class(complexity_class(types, prompt, mode))
|
|
555
|
+
|
|
556
|
+
# ---- vision path -----------------------------------------------------
|
|
557
|
+
if has_image:
|
|
558
|
+
dec.path = PATH_VISION
|
|
559
|
+
dec.use_tools = bool({"agentic_execution"} & types) and mode in ("agent", "build")
|
|
560
|
+
dec.reason = "image attached → vision-capable model required"
|
|
561
|
+
dec.size_class = "large"
|
|
562
|
+
vision_cfg, vcaps = find_vision_capable()
|
|
563
|
+
dec.vision_config = vision_cfg
|
|
564
|
+
if vision_cfg is None:
|
|
565
|
+
dec.reason = ("image attached but no vision-capable model is "
|
|
566
|
+
"configured — will report clearly")
|
|
567
|
+
_stamp(dec, t0)
|
|
568
|
+
return dec
|
|
569
|
+
|
|
570
|
+
# ---- fast path (requirement #9) --------------------------------------
|
|
571
|
+
tiny = len(prompt) < 220 and types <= {"simple_chat", "mathematics", "research"}
|
|
572
|
+
greetingish = bool({"simple_chat"} & types) and complexity <= 0.2
|
|
573
|
+
# v0.7.9.5: a genuinely trivial prompt takes the fast path regardless
|
|
574
|
+
# of complexity scoring — "hi" must never trigger planning.
|
|
575
|
+
trivial = len((prompt or "").strip()) <= 25 and \
|
|
576
|
+
types <= {"simple_chat", "mathematics"}
|
|
577
|
+
if (tiny and (greetingish or mode in (None, "notebook"))) or \
|
|
578
|
+
(trivial and mode in (None, "notebook")):
|
|
579
|
+
dec.path = PATH_FAST
|
|
580
|
+
dec.fast = True
|
|
581
|
+
dec.size_class = "simple"
|
|
582
|
+
dec.reason = ("trivial prompt → fast path (no memory scan, no agents,"
|
|
583
|
+
" no tools)" if trivial else
|
|
584
|
+
"simple question → fast path (no memory scan, no agents,"
|
|
585
|
+
" no tools)")
|
|
586
|
+
_stamp(dec, t0)
|
|
587
|
+
return dec
|
|
588
|
+
|
|
589
|
+
# ---- multi-AI only when it actually helps (requirement #33) ----------
|
|
590
|
+
wants_multi = "multi_agent" in types or (
|
|
591
|
+
mode == "agent" and complexity >= 0.75 and
|
|
592
|
+
({"coding", "agentic_execution", "long_context"} & types))
|
|
593
|
+
if wants_multi and complexity >= 0.6:
|
|
594
|
+
dec.path = PATH_MULTI_AI
|
|
595
|
+
dec.use_multi_agent = True
|
|
596
|
+
dec.size_class = "large"
|
|
597
|
+
dec.reason = f"complex multi-part task (complexity {complexity:.2f}) → orchestrated team"
|
|
598
|
+
_stamp(dec, t0)
|
|
599
|
+
return dec
|
|
600
|
+
|
|
601
|
+
# ---- agent/tool path --------------------------------------------------
|
|
602
|
+
if mode in ("agent", "build"):
|
|
603
|
+
dec.path = PATH_AGENT
|
|
604
|
+
dec.use_tools = True
|
|
605
|
+
dec.size_class = "large"
|
|
606
|
+
dec.reason = f"{mode} mode → tool-executing agent loop"
|
|
607
|
+
_stamp(dec, t0)
|
|
608
|
+
return dec
|
|
609
|
+
if "agentic_execution" in types and mode not in ("notebook",):
|
|
610
|
+
dec.path = PATH_AGENT
|
|
611
|
+
dec.use_tools = True
|
|
612
|
+
dec.size_class = "large"
|
|
613
|
+
dec.reason = "task requires real file/tool operations → agent loop"
|
|
614
|
+
_stamp(dec, t0)
|
|
615
|
+
return dec
|
|
616
|
+
if workflow_hint in ("install", "pipeline"):
|
|
617
|
+
dec.path = PATH_AGENT
|
|
618
|
+
dec.use_tools = True
|
|
619
|
+
dec.size_class = "large"
|
|
620
|
+
dec.reason = f"workflow '{workflow_hint}' → agent/pipeline execution"
|
|
621
|
+
_stamp(dec, t0)
|
|
622
|
+
return dec
|
|
623
|
+
|
|
624
|
+
# ---- default: plain streaming -----------------------------------------
|
|
625
|
+
bits = sorted(types)
|
|
626
|
+
dec.path = PATH_STREAM
|
|
627
|
+
dec.fast = complexity <= 0.2
|
|
628
|
+
dec.reason = "streaming chat (" + ", ".join(bits) + ")" if bits else "streaming chat"
|
|
629
|
+
_stamp(dec, t0)
|
|
630
|
+
return dec
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _stamp(dec: RouteDecision, t0: float) -> None:
|
|
634
|
+
try:
|
|
635
|
+
from . import metrics
|
|
636
|
+
m = metrics.current()
|
|
637
|
+
if m is not None:
|
|
638
|
+
m.mark_stage("routing", t0)
|
|
639
|
+
m.task_types = sorted(dec.task_types)
|
|
640
|
+
m.route_path = dec.path
|
|
641
|
+
except Exception:
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def describe_decision(dec: RouteDecision) -> str:
|
|
646
|
+
"""One human-readable line for the UI activity feed — real decision,
|
|
647
|
+
really made."""
|
|
648
|
+
tt = ", ".join(sorted(dec.task_types)) or "general"
|
|
649
|
+
pol_tag = f" [{dec.privacy_policy.upper()}]" if hasattr(dec, "privacy_policy") and dec.privacy_policy else ""
|
|
650
|
+
icon = "[Router]"
|
|
651
|
+
try:
|
|
652
|
+
# Check if terminal/stream supports unicode compass
|
|
653
|
+
import sys
|
|
654
|
+
enc = getattr(sys.stdout, "encoding", "") or "utf-8"
|
|
655
|
+
if "utf" in enc.lower():
|
|
656
|
+
icon = "\U0001f9ed Router:"
|
|
657
|
+
else:
|
|
658
|
+
icon = "[Router]"
|
|
659
|
+
except Exception:
|
|
660
|
+
icon = "[Router]"
|
|
661
|
+
line = f"{icon} {dec.path.upper()}{pol_tag} ({tt})"
|
|
662
|
+
if dec.vision_config is not None:
|
|
663
|
+
line += (f" → {dec.vision_config.get('provider', '?')}/"
|
|
664
|
+
f"{dec.vision_config.get('model', '?')}")
|
|
665
|
+
return line
|
|
File without changes
|