Yash030 commited on
Commit
4974012
·
1 Parent(s): 8238c16

Add smart task-aware routing (Phase 1)

Browse files

- core/model_capabilities.py: Model capability registry
- core/task_detector.py: Detect task requirements from requests
- core/chain_engine.py: Multi-model pipeline engine (placeholder)
- api/model_router.py: Add resolve_with_task_awareness method

Now 'auto' model can detect coding/reasoning and route to best model.

api/model_router.py CHANGED
@@ -9,6 +9,12 @@ from loguru import logger
9
  from config.provider_ids import SUPPORTED_PROVIDER_IDS
10
  from config.settings import Settings
11
  from core.session_tracker import SessionTracker
 
 
 
 
 
 
12
 
13
  from .gateway_model_ids import decode_gateway_model_id
14
  from .models.anthropic import MessagesRequest, TokenCountRequest
@@ -278,3 +284,73 @@ class ModelRouter:
278
  update={"model": resolved.provider_model}, deep=True
279
  )
280
  return RoutedTokenCountRequest(request=routed, resolved=resolved)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from config.provider_ids import SUPPORTED_PROVIDER_IDS
10
  from config.settings import Settings
11
  from core.session_tracker import SessionTracker
12
+ from core.model_capabilities import (
13
+ get_model_capabilities,
14
+ find_best_model_for_task,
15
+ find_models_with_capability,
16
+ )
17
+ from core.task_detector import TaskDetector
18
 
19
  from .gateway_model_ids import decode_gateway_model_id
20
  from .models.anthropic import MessagesRequest, TokenCountRequest
 
284
  update={"model": resolved.provider_model}, deep=True
285
  )
286
  return RoutedTokenCountRequest(request=routed, resolved=resolved)
287
+
288
+ def resolve_with_task_awareness(
289
+ self,
290
+ claude_model_name: str,
291
+ messages: list,
292
+ ) -> ResolvedModel:
293
+ """Resolve model with task-based capability matching.
294
+
295
+ For 'auto' model, detects task requirements and routes to best-capable model.
296
+ """
297
+ if not self._is_auto(claude_model_name):
298
+ return self.resolve(claude_model_name)
299
+
300
+ # Detect what capabilities are needed
301
+ detector = TaskDetector()
302
+ requirements = detector.detect_requirements(messages)
303
+
304
+ logger.info(
305
+ "Task-aware routing: detected requirements={} confidence={:.2f}",
306
+ requirements.required_capabilities,
307
+ requirements.confidence,
308
+ )
309
+
310
+ # Get available candidates
311
+ candidates = self.resolve_candidates(claude_model_name)
312
+
313
+ if not candidates:
314
+ # Fallback to default
315
+ return self.resolve(claude_model_name)
316
+
317
+ # If confidence is low or only general text needed, use load-based selection
318
+ if requirements.confidence < 0.7 or (
319
+ not requirements.requires_vision
320
+ and not requirements.requires_coding
321
+ and not requirements.requires_reasoning
322
+ ):
323
+ logger.debug("Task-aware routing: low confidence, using load-based selection")
324
+ return candidates[0]
325
+
326
+ # Find best model matching required capabilities
327
+ required_caps = set()
328
+ if requirements.requires_coding:
329
+ required_caps.add("coding")
330
+ if requirements.requires_reasoning:
331
+ required_caps.add("reasoning")
332
+ if requirements.requires_vision:
333
+ required_caps.add("vision")
334
+
335
+ if required_caps:
336
+ model_refs = [c.provider_model_ref for c in candidates]
337
+ best = find_best_model_for_task(required_caps, model_refs)
338
+ if best:
339
+ # Find the matching candidate
340
+ for cand in candidates:
341
+ if cand.provider_model_ref == best.model_ref:
342
+ logger.info(
343
+ "Task-aware routing: selected {} for capabilities={}",
344
+ best.model_ref,
345
+ required_caps,
346
+ )
347
+ return cand
348
+
349
+ # Default to first candidate (load-balanced)
350
+ return candidates[0]
351
+
352
+ def get_routing_hint(self, messages: list) -> str:
353
+ """Get a hint about what kind of model would be best."""
354
+ detector = TaskDetector()
355
+ requirements = detector.detect_requirements(messages)
356
+ return detector.get_priority_hint(requirements)
core/chain_engine.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model chaining engine for multi-stage AI pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator
7
+ from dataclasses import dataclass
8
+ from typing import Any, Callable
9
+
10
+ from loguru import logger
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ChainStage:
15
+ """A single stage in a model chain."""
16
+
17
+ model_ref: str # e.g., "zen/minimax-m2.5-free"
18
+ stage_name: str # e.g., "vision_analysis", "code_generation"
19
+ description: str
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ChainResult:
24
+ """Result from executing a chain stage."""
25
+
26
+ stage: ChainStage
27
+ output: str
28
+ success: bool
29
+ error: str | None = None
30
+
31
+
32
+ # Chain templates for common multi-capability tasks
33
+ CHAIN_TEMPLATES: dict[str, list[ChainStage]] = {
34
+ "vision_to_text": [
35
+ ChainStage(
36
+ model_ref="nvidia_nim/stepfun-ai/step-3.5-flash",
37
+ stage_name="image_analysis",
38
+ description="Analyze image content",
39
+ ),
40
+ ChainStage(
41
+ model_ref="zen/minimax-m2.5-free",
42
+ stage_name="response_generation",
43
+ description="Generate final response",
44
+ ),
45
+ ],
46
+ "reasoning_to_generation": [
47
+ ChainStage(
48
+ model_ref="nvidia_nim/qwen/qwen3-coder-480b-a35b-instruct",
49
+ stage_name="analysis",
50
+ description="Analyze and plan",
51
+ ),
52
+ ChainStage(
53
+ model_ref="zen/minimax-m2.5-free",
54
+ stage_name="generation",
55
+ description="Generate output",
56
+ ),
57
+ ],
58
+ }
59
+
60
+
61
+ class ChainEngine:
62
+ """Execute multi-model pipelines for complex requests."""
63
+
64
+ def __init__(self, provider_getter: Callable[[str], Any]):
65
+ self._provider_getter = provider_getter
66
+
67
+ async def execute_simple_chain(
68
+ self,
69
+ stages: list[ChainStage],
70
+ initial_messages: list[Any],
71
+ system_prompt: str | None = None,
72
+ ) -> AsyncIterator[str]:
73
+ """Execute a chain of models sequentially.
74
+
75
+ Args:
76
+ stages: List of chain stages to execute
77
+ initial_messages: Initial user messages
78
+ system_prompt: Optional system prompt
79
+
80
+ Yields:
81
+ SSE events from the final model in the chain
82
+ """
83
+ if not stages:
84
+ return
85
+
86
+ logger.info("ChainEngine: executing {} stages", len(stages))
87
+
88
+ # For now, execute single model - full chaining requires more integration
89
+ # This is a placeholder for the full implementation
90
+ first_stage = stages[0]
91
+ provider = self._provider_getter(first_stage.model_ref.split("/")[0])
92
+
93
+ logger.info(
94
+ "ChainEngine: using model {} for chain",
95
+ first_stage.model_ref,
96
+ )
97
+
98
+ # For Phase 1, just delegate to provider - full chaining comes later
99
+ # The infrastructure is now in place
100
+ async for event in provider.stream_response(
101
+ initial_messages, system_prompt, {}
102
+ ):
103
+ yield event
104
+
105
+ def get_chain_for_requirements(
106
+ self,
107
+ required_capabilities: set[str],
108
+ available_models: list[str],
109
+ ) -> list[ChainStage] | None:
110
+ """Determine the appropriate chain based on required capabilities.
111
+
112
+ Args:
113
+ required_capabilities: Set of capabilities needed
114
+ available_models: Available model references
115
+
116
+ Returns:
117
+ Chain stages or None if single model is sufficient
118
+ """
119
+ # If only one capability needed, no chain needed
120
+ if len(required_capabilities) <= 1:
121
+ return None
122
+
123
+ # If multiple capabilities, build a simple chain
124
+ if "vision" in required_capabilities and "coding" in required_capabilities:
125
+ return CHAIN_TEMPLATES.get("vision_to_text")
126
+
127
+ if "vision" in required_capabilities and "reasoning" in required_capabilities:
128
+ return CHAIN_TEMPLATES.get("vision_to_text")
129
+
130
+ if "reasoning" in required_capabilities and "coding" in required_capabilities:
131
+ return CHAIN_TEMPLATES.get("reasoning_to_generation")
132
+
133
+ # Default: no chain for now
134
+ return None
135
+
136
+
137
+ async def execute_model_for_stage(
138
+ provider: Any,
139
+ messages: list[Any],
140
+ system: str | None,
141
+ metadata: dict[str, Any],
142
+ ) -> str:
143
+ """Execute a single model stage and return its output."""
144
+ output_parts = []
145
+
146
+ try:
147
+ async for event in provider.stream_response(messages, system, metadata):
148
+ # Parse SSE and collect text output
149
+ if "content_block_delta" in event:
150
+ # Extract text from delta
151
+ pass
152
+
153
+ return "".join(output_parts)
154
+ except Exception as e:
155
+ logger.error("Chain stage failed: {}", e)
156
+ raise
core/model_capabilities.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model capability registry for intelligent routing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Sequence
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ModelCapabilities:
14
+ """Capabilities of a specific model for routing decisions."""
15
+
16
+ provider_id: str
17
+ model_id: str
18
+ model_ref: str # provider/model format
19
+ vision: bool = False # Can process images
20
+ coding: bool = False # Good at code generation/analysis
21
+ reasoning: bool = False # Strong reasoning/thinking
22
+ general_text: bool = True # General text generation
23
+ multimodal_input: bool = False # Can handle multiple input types
24
+ multimodal_output: bool = False # Can produce multiple output types
25
+ max_tokens: int = 4096
26
+ speed: str = "medium" # "fast", "medium", "slow"
27
+ priority: int = 100 # Higher = preferred for its capabilities
28
+
29
+
30
+ # Registry of all available models and their capabilities
31
+ # This can be extended with actual model discovery later
32
+ MODEL_CAPABILITIES: dict[str, ModelCapabilities] = {
33
+ # Zen/minimax models
34
+ "zen/minimax-m2.5-free": ModelCapabilities(
35
+ provider_id="zen",
36
+ model_id="minimax-m2.5-free",
37
+ model_ref="zen/minimax-m2.5-free",
38
+ coding=True,
39
+ reasoning=True,
40
+ general_text=True,
41
+ max_tokens=32000,
42
+ speed="fast",
43
+ priority=80,
44
+ ),
45
+ # NVIDIA NIM models
46
+ "nvidia_nim/stepfun-ai/step-3.5-flash": ModelCapabilities(
47
+ provider_id="nvidia_nim",
48
+ model_id="step-3.5-flash",
49
+ model_ref="nvidia_nim/stepfun-ai/step-3.5-flash",
50
+ coding=True,
51
+ reasoning=True,
52
+ general_text=True,
53
+ max_tokens=32000,
54
+ speed="fast",
55
+ priority=70,
56
+ ),
57
+ "nvidia_nim/qwen/qwen3-coder-480b-a35b-instruct": ModelCapabilities(
58
+ provider_id="nvidia_nim",
59
+ model_id="qwen3-coder-480b-a35b-instruct",
60
+ model_ref="nvidia_nim/qwen/qwen3-coder-480b-a35b-instruct",
61
+ coding=True,
62
+ reasoning=True,
63
+ general_text=True,
64
+ max_tokens=32000,
65
+ speed="slow",
66
+ priority=90,
67
+ ),
68
+ "nvidia_nim/mistralai/mistral-large-3-675b-instruct-2512": ModelCapabilities(
69
+ provider_id="nvidia_nim",
70
+ model_id="mistral-large-3-675b-instruct-2512",
71
+ model_ref="nvidia_nim/mistralai/mistral-large-3-675b-instruct-2512",
72
+ coding=True,
73
+ reasoning=True,
74
+ general_text=True,
75
+ max_tokens=32000,
76
+ speed="slow",
77
+ priority=85,
78
+ ),
79
+ "nvidia_nim/abacusai/dracarys-llama-3.1-70b-instruct": ModelCapabilities(
80
+ provider_id="nvidia_nim",
81
+ model_id="dracarys-llama-3.1-70b-instruct",
82
+ model_ref="nvidia_nim/abacusai/dracarys-llama-3.1-70b-instruct",
83
+ coding=True,
84
+ reasoning=True,
85
+ general_text=True,
86
+ max_tokens=32000,
87
+ speed="medium",
88
+ priority=75,
89
+ ),
90
+ "nvidia_nim/z-ai/glm4.7": ModelCapabilities(
91
+ provider_id="nvidia_nim",
92
+ model_id="glm4.7",
93
+ model_ref="nvidia_nim/z-ai/glm4.7",
94
+ coding=True,
95
+ reasoning=True,
96
+ general_text=True,
97
+ max_tokens=32000,
98
+ speed="medium",
99
+ priority=70,
100
+ ),
101
+ "nvidia_nim/bytedance/seed-oss-36b-instruct": ModelCapabilities(
102
+ provider_id="nvidia_nim",
103
+ model_id="seed-oss-36b-instruct",
104
+ model_ref="nvidia_nim/bytedance/seed-oss-36b-instruct",
105
+ coding=True,
106
+ reasoning=True,
107
+ general_text=True,
108
+ max_tokens=32000,
109
+ speed="medium",
110
+ priority=65,
111
+ ),
112
+ "nvidia_nim/mistralai/mistral-nemotron": ModelCapabilities(
113
+ provider_id="nvidia_nim",
114
+ model_id="mistral-nemotron",
115
+ model_ref="nvidia_nim/mistralai/mistral-nemotron",
116
+ coding=True,
117
+ reasoning=True,
118
+ general_text=True,
119
+ max_tokens=32000,
120
+ speed="medium",
121
+ priority=60,
122
+ ),
123
+ }
124
+
125
+
126
+ def get_model_capabilities(model_ref: str) -> ModelCapabilities | None:
127
+ """Get capabilities for a specific model reference."""
128
+ return MODEL_CAPABILITIES.get(model_ref)
129
+
130
+
131
+ def find_models_with_capability(capability: str) -> list[ModelCapabilities]:
132
+ """Find all models that have a specific capability."""
133
+ results = []
134
+ for caps in MODEL_CAPABILITIES.values():
135
+ if getattr(caps, capability, False):
136
+ results.append(caps)
137
+ # Sort by priority (higher = better)
138
+ results.sort(key=lambda x: x.priority, reverse=True)
139
+ return results
140
+
141
+
142
+ def find_best_model_for_task(
143
+ required_capabilities: set[str],
144
+ available_models: Sequence[str] | None = None,
145
+ ) -> ModelCapabilities | None:
146
+ """Find the best model matching required capabilities.
147
+
148
+ Args:
149
+ required_capabilities: Set of capability names needed (e.g., {"coding", "vision"})
150
+ available_models: Optional list of model refs to filter by
151
+
152
+ Returns:
153
+ Best matching ModelCapabilities or None
154
+ """
155
+ candidates = []
156
+
157
+ models_to_check = (
158
+ [MODEL_CAPABILITIES[m] for m in available_models if m in MODEL_CAPABILITIES]
159
+ if available_models
160
+ else list(MODEL_CAPABILITIES.values())
161
+ )
162
+
163
+ for caps in models_to_check:
164
+ # Check if model has all required capabilities
165
+ if all(getattr(caps, cap, False) for cap in required_capabilities):
166
+ candidates.append(caps)
167
+
168
+ if not candidates:
169
+ return None
170
+
171
+ # Sort by priority and return best
172
+ candidates.sort(key=lambda x: x.priority, reverse=True)
173
+ return candidates[0]
174
+
175
+
176
+ def get_capability_match_score(
177
+ model_caps: ModelCapabilities,
178
+ required: set[str],
179
+ ) -> tuple[int, int]:
180
+ """Calculate match score for routing.
181
+
182
+ Returns (matched_count, priority) for sorting.
183
+ """
184
+ matched = sum(1 for cap in required if getattr(model_caps, cap, False))
185
+ return (matched, model_caps.priority)
core/task_detector.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task detection - analyze requests to determine required capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+ from core.anthropic.content import get_block_attr
12
+
13
+
14
+ # Keywords that indicate specific task types
15
+ CODING_KEYWORDS = {
16
+ "python", "javascript", "typescript", "java", "c++", "cpp", "golang",
17
+ "rust", "ruby", "php", "swift", "kotlin", "sql", "html", "css", "react",
18
+ "vue", "angular", "node", "django", "flask", "fastapi", "spring",
19
+ "function", "class", "method", "api", "endpoint", "database", "query",
20
+ "algorithm", "debug", "error", "fix", "implement", "create", "write",
21
+ "code", "programming", "script", "module", "import", "export",
22
+ "def ", "const ", "let ", "var ", "function ", "async ", "await ",
23
+ }
24
+
25
+ REASONING_KEYWORDS = {
26
+ "analyze", "analysis", "reason", "why", "how", "explain", "compare",
27
+ "contrast", "evaluate", "assess", "conclude", "deduce", "infer",
28
+ "logic", "proof", "theorem", "hypothesis", "synthesize", "strategy",
29
+ "think", "solve", "derive", "calculate", "compute", "math", "equation",
30
+ "formula", "solution", "optimal", "best", "improve", "optimize",
31
+ "design", "architecture", "system", "plan", "decision", "recommend",
32
+ }
33
+
34
+ VISION_KEYWORDS = {
35
+ "image", "picture", "photo", "screenshot", "diagram", "chart", "graph",
36
+ "visual", "see", "look at", "describe what", "what's in", "identify",
37
+ "recognize", "detect", "object", "scene", "face", "text in image",
38
+ }
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class TaskRequirements:
43
+ """Detected requirements for a request."""
44
+
45
+ requires_vision: bool = False
46
+ requires_coding: bool = False
47
+ requires_reasoning: bool = False
48
+ requires_general_text: bool = True
49
+ confidence: float = 0.0 # 0-1 confidence in detection
50
+
51
+ @property
52
+ def required_capabilities(self) -> set[str]:
53
+ caps = set()
54
+ if self.requires_vision:
55
+ caps.add("vision")
56
+ if self.requires_coding:
57
+ caps.add("coding")
58
+ if self.requires_reasoning:
59
+ caps.add("reasoning")
60
+ if self.requires_general_text:
61
+ caps.add("general_text")
62
+ return caps
63
+
64
+
65
+ class TaskDetector:
66
+ """Analyze request messages to detect required capabilities."""
67
+
68
+ def detect_requirements(self, messages: list[Any]) -> TaskRequirements:
69
+ """Analyze messages and return required capabilities."""
70
+ has_vision = False
71
+ has_coding = False
72
+ has_reasoning = False
73
+ total_text = ""
74
+
75
+ for msg in messages:
76
+ # Handle both dict and object message formats
77
+ if isinstance(msg, dict):
78
+ content = msg.get("content")
79
+ elif hasattr(msg, "content"):
80
+ content = msg.content
81
+ else:
82
+ continue
83
+
84
+ if isinstance(content, str):
85
+ total_text += content.lower() + " "
86
+ elif isinstance(content, list):
87
+ for block in content:
88
+ b_type = get_block_attr(block, "type") or ""
89
+
90
+ # Check for image content
91
+ if b_type == "image":
92
+ has_vision = True
93
+ logger.debug("TaskDetector: Found image in message")
94
+
95
+ # Get text content
96
+ if b_type == "text":
97
+ text = get_block_attr(block, "text", "") or ""
98
+ total_text += text.lower() + " "
99
+
100
+ # Analyze text for keywords
101
+ if total_text:
102
+ has_coding = self._detect_coding(total_text)
103
+ has_reasoning = self._detect_reasoning(total_text)
104
+
105
+ # Calculate confidence
106
+ confidence = self._calculate_confidence(
107
+ has_vision, has_coding, has_reasoning, total_text
108
+ )
109
+
110
+ # Default to general text if nothing detected
111
+ if not has_vision and not has_coding and not has_reasoning:
112
+ has_general = True
113
+
114
+ result = TaskRequirements(
115
+ requires_vision=has_vision,
116
+ requires_coding=has_coding,
117
+ requires_reasoning=has_reasoning,
118
+ requires_general_text=True,
119
+ confidence=confidence,
120
+ )
121
+
122
+ logger.info(
123
+ "TaskDetector: detected caps={} confidence={:.2f}",
124
+ result.required_capabilities,
125
+ confidence,
126
+ )
127
+
128
+ return result
129
+
130
+ def _detect_coding(self, text: str) -> bool:
131
+ """Detect if request requires coding capabilities."""
132
+ # Check exact word matches first
133
+ words = set(re.findall(r'\b\w+\b', text))
134
+ coding_matches = words & CODING_KEYWORDS
135
+ if len(coding_matches) >= 2:
136
+ return True
137
+
138
+ # Also check for substring matches (e.g., "python" in "write python code")
139
+ for keyword in CODING_KEYWORDS:
140
+ if keyword in text:
141
+ # Found one keyword as substring, check for another
142
+ remaining = text.replace(keyword, "")
143
+ for kw2 in CODING_KEYWORDS:
144
+ if kw2 in remaining and kw2 != keyword:
145
+ return True
146
+ # Also check for programming patterns
147
+ if any(pat in text for pat in ["def ", "function ", "class ", "import ", "const ", "let ", "var ", "()", "=>"]):
148
+ return True
149
+
150
+ return False
151
+
152
+ def _detect_reasoning(self, text: str) -> bool:
153
+ """Detect if request requires reasoning capabilities."""
154
+ words = set(re.findall(r'\b\w+\b', text))
155
+ reasoning_matches = words & REASONING_KEYWORDS
156
+ if len(reasoning_matches) >= 1:
157
+ return True
158
+ # Also check substring
159
+ for keyword in REASONING_KEYWORDS:
160
+ if keyword in text:
161
+ return True
162
+ return False
163
+
164
+ def _calculate_confidence(
165
+ self,
166
+ has_vision: bool,
167
+ has_coding: bool,
168
+ has_reasoning: bool,
169
+ text: str,
170
+ ) -> float:
171
+ """Calculate confidence in the detection."""
172
+ if has_vision:
173
+ return 0.95 # Image detection is reliable
174
+ if has_coding or has_reasoning:
175
+ # More text = more confident
176
+ word_count = len(text.split())
177
+ base = 0.7
178
+ if word_count > 50:
179
+ base = 0.8
180
+ if word_count > 100:
181
+ base = 0.85
182
+ return base
183
+ return 0.5 # Default confidence for general text
184
+
185
+ def get_priority_hint(self, requirements: TaskRequirements) -> str:
186
+ """Get a hint for model priority based on requirements."""
187
+ if requirements.requires_vision:
188
+ return "vision"
189
+ if requirements.requires_coding:
190
+ return "coding"
191
+ if requirements.requires_reasoning:
192
+ return "reasoning"
193
+ return "balanced"