model-router-cli 1.0.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.
- app/analytics/service.py +119 -0
- app/analyzer/analyzer.py +67 -0
- app/analyzer/heuristics.py +192 -0
- app/api/routes.py +589 -0
- app/budgets/manager.py +39 -0
- app/cli/main.py +287 -0
- app/config/settings.py +43 -0
- app/experiments/service.py +85 -0
- app/fallback/handler.py +105 -0
- app/models/schemas.py +127 -0
- app/observability/events.py +43 -0
- app/providers/base.py +46 -0
- app/providers/external_providers.py +321 -0
- app/providers/mock_provider.py +108 -0
- app/providers/ollama_provider.py +141 -0
- app/providers/registry.py +35 -0
- app/router/engine.py +150 -0
- app/router/rules_engine.py +73 -0
- app/router/scoring.py +154 -0
- app/static/assets/index-CQFztymk.js +63 -0
- app/static/assets/index-DWa3sE4Y.css +2 -0
- app/static/favicon.png +0 -0
- app/static/favicon.svg +1 -0
- app/static/icons.svg +24 -0
- app/static/index.html +17 -0
- app/static/logo.png +0 -0
- app/storage/database.py +366 -0
- app/storage/models.py +202 -0
- model_router_cli-1.0.0.dist-info/METADATA +343 -0
- model_router_cli-1.0.0.dist-info/RECORD +38 -0
- model_router_cli-1.0.0.dist-info/WHEEL +5 -0
- model_router_cli-1.0.0.dist-info/entry_points.txt +2 -0
- model_router_cli-1.0.0.dist-info/licenses/LICENSE +22 -0
- model_router_cli-1.0.0.dist-info/top_level.txt +2 -0
- tests/test_analyzer.py +41 -0
- tests/test_e2e.py +127 -0
- tests/test_providers.py +21 -0
- tests/test_router.py +78 -0
app/analytics/service.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
|
2
|
+
from sqlalchemy import select, func
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
|
+
from app.storage.models import RequestRecord, FeedbackRecord, ModelRecord
|
|
5
|
+
from app.config.settings import get_settings
|
|
6
|
+
|
|
7
|
+
settings = get_settings()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def calculate_cost_savings(
|
|
11
|
+
session: AsyncSession,
|
|
12
|
+
baseline_model_id: Optional[str] = None,
|
|
13
|
+
) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Computes accurate, non-invented cost savings:
|
|
16
|
+
Baseline Cost (if all requests were sent to baseline model) vs Actual Routed Cost.
|
|
17
|
+
"""
|
|
18
|
+
base_id = baseline_model_id or settings.BASELINE_MODEL_ID
|
|
19
|
+
|
|
20
|
+
# Fetch baseline model pricing
|
|
21
|
+
res_base = await session.execute(select(ModelRecord).filter_by(id=base_id))
|
|
22
|
+
baseline_model = res_base.scalar_one_or_none()
|
|
23
|
+
|
|
24
|
+
base_in_rate = baseline_model.cost_per_input_token if baseline_model else 0.000005
|
|
25
|
+
base_out_rate = baseline_model.cost_per_output_token if baseline_model else 0.000015
|
|
26
|
+
|
|
27
|
+
# Fetch aggregate requests
|
|
28
|
+
res = await session.execute(
|
|
29
|
+
select(
|
|
30
|
+
func.count(RequestRecord.request_id),
|
|
31
|
+
func.sum(RequestRecord.input_tokens),
|
|
32
|
+
func.sum(RequestRecord.output_tokens),
|
|
33
|
+
func.sum(RequestRecord.estimated_cost),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
total_reqs, total_in_tok, total_out_tok, total_routed_cost = res.one()
|
|
37
|
+
|
|
38
|
+
total_reqs = total_reqs or 0
|
|
39
|
+
total_in_tok = total_in_tok or 0
|
|
40
|
+
total_out_tok = total_out_tok or 0
|
|
41
|
+
total_routed_cost = round(total_routed_cost or 0.0, 6)
|
|
42
|
+
|
|
43
|
+
# Theoretical cost if all traffic ran on baseline
|
|
44
|
+
baseline_total_cost = round((total_in_tok * base_in_rate) + (total_out_tok * base_out_rate), 6)
|
|
45
|
+
saved_amount = max(0.0, round(baseline_total_cost - total_routed_cost, 6))
|
|
46
|
+
savings_percent = round((saved_amount / baseline_total_cost * 100.0), 2) if baseline_total_cost > 0 else 0.0
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"baseline_model": base_id,
|
|
50
|
+
"total_requests": total_reqs,
|
|
51
|
+
"total_input_tokens": total_in_tok,
|
|
52
|
+
"total_output_tokens": total_out_tok,
|
|
53
|
+
"baseline_total_cost": baseline_total_cost,
|
|
54
|
+
"routed_total_cost": total_routed_cost,
|
|
55
|
+
"cost_saved_usd": saved_amount,
|
|
56
|
+
"savings_percentage": savings_percent,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def get_system_analytics(session: AsyncSession) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Collects full aggregate metrics for the dashboard.
|
|
63
|
+
"""
|
|
64
|
+
# 1. Totals & averages
|
|
65
|
+
res = await session.execute(
|
|
66
|
+
select(
|
|
67
|
+
func.count(RequestRecord.request_id),
|
|
68
|
+
func.avg(RequestRecord.total_latency_ms),
|
|
69
|
+
func.avg(RequestRecord.routing_latency_ms),
|
|
70
|
+
func.avg(RequestRecord.estimated_cost),
|
|
71
|
+
func.sum(RequestRecord.estimated_cost),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
count, avg_lat, avg_route_lat, avg_cost, sum_cost = res.one()
|
|
75
|
+
|
|
76
|
+
# 2. Distribution by model
|
|
77
|
+
model_dist_res = await session.execute(
|
|
78
|
+
select(RequestRecord.selected_model, func.count(RequestRecord.request_id))
|
|
79
|
+
.group_by(RequestRecord.selected_model)
|
|
80
|
+
)
|
|
81
|
+
model_distribution = {m: c for m, c in model_dist_res.all()}
|
|
82
|
+
|
|
83
|
+
# 3. Distribution by task type
|
|
84
|
+
task_dist_res = await session.execute(
|
|
85
|
+
select(RequestRecord.task_type, func.count(RequestRecord.request_id))
|
|
86
|
+
.group_by(RequestRecord.task_type)
|
|
87
|
+
)
|
|
88
|
+
task_distribution = {t: c for t, c in task_dist_res.all()}
|
|
89
|
+
|
|
90
|
+
# 4. Fallback rate
|
|
91
|
+
fb_res = await session.execute(
|
|
92
|
+
select(func.count(RequestRecord.request_id)).filter(RequestRecord.fallback_used == True)
|
|
93
|
+
)
|
|
94
|
+
fallback_count = fb_res.scalar() or 0
|
|
95
|
+
fallback_rate = round((fallback_count / count * 100.0), 2) if count and count > 0 else 0.0
|
|
96
|
+
|
|
97
|
+
# 5. User Feedback aggregate
|
|
98
|
+
fb_pos_res = await session.execute(select(func.count(FeedbackRecord.id)).filter(FeedbackRecord.rating == 1))
|
|
99
|
+
fb_neg_res = await session.execute(select(func.count(FeedbackRecord.id)).filter(FeedbackRecord.rating == -1))
|
|
100
|
+
pos_feedback = fb_pos_res.scalar() or 0
|
|
101
|
+
neg_feedback = fb_neg_res.scalar() or 0
|
|
102
|
+
total_feedback = pos_feedback + neg_feedback
|
|
103
|
+
quality_score = round((pos_feedback / total_feedback * 100.0), 1) if total_feedback > 0 else None
|
|
104
|
+
|
|
105
|
+
savings = await calculate_cost_savings(session)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"total_requests": count or 0,
|
|
109
|
+
"avg_latency_ms": round(avg_lat or 0.0, 1),
|
|
110
|
+
"avg_routing_latency_ms": round(avg_route_lat or 0.0, 1),
|
|
111
|
+
"avg_cost_usd": round(avg_cost or 0.0, 6),
|
|
112
|
+
"total_cost_usd": round(sum_cost or 0.0, 6),
|
|
113
|
+
"quality_score_percent": quality_score,
|
|
114
|
+
"fallback_rate_percent": fallback_rate,
|
|
115
|
+
"fallback_count": fallback_count,
|
|
116
|
+
"model_distribution": model_distribution,
|
|
117
|
+
"task_distribution": task_distribution,
|
|
118
|
+
"savings": savings,
|
|
119
|
+
}
|
app/analyzer/analyzer.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from app.models.schemas import RequestAnalysis, TaskType, PriorityLevel
|
|
5
|
+
from app.analyzer.heuristics import analyze_request_heuristics, estimate_tokens
|
|
6
|
+
from app.providers.registry import provider_registry
|
|
7
|
+
from app.config.settings import get_settings
|
|
8
|
+
|
|
9
|
+
settings = get_settings()
|
|
10
|
+
|
|
11
|
+
CLASSIFIER_PROMPT = """You are a real-time request classifier for an intelligent LLM model router.
|
|
12
|
+
Analyze the user prompt and return ONLY a JSON object matching this schema:
|
|
13
|
+
{
|
|
14
|
+
"task_type": "GENERAL_QA" | "CODING" | "DEBUGGING" | "REASONING" | "SUMMARIZATION" | "EXTRACTION" | "WRITING" | "TRANSLATION" | "ANALYSIS" | "MATH" | "LONG_CONTEXT" | "CREATIVE",
|
|
15
|
+
"complexity": 0.0 to 1.0,
|
|
16
|
+
"reasoning_required": true/false,
|
|
17
|
+
"coding_required": true/false,
|
|
18
|
+
"vision_required": true/false,
|
|
19
|
+
"latency_priority": "LOW" | "MEDIUM" | "HIGH",
|
|
20
|
+
"cost_sensitivity": "LOW" | "MEDIUM" | "HIGH",
|
|
21
|
+
"quality_requirement": "LOW" | "MEDIUM" | "HIGH"
|
|
22
|
+
}
|
|
23
|
+
Output valid JSON only with no markdown wrapping or additional text.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def analyze_request(prompt: str, analyzer_mode: Optional[str] = None) -> RequestAnalysis:
|
|
28
|
+
mode = (analyzer_mode or settings.ROUTER_ANALYZER).lower()
|
|
29
|
+
|
|
30
|
+
if mode == "llm":
|
|
31
|
+
try:
|
|
32
|
+
# Attempt to use local or configured provider for classification
|
|
33
|
+
provider = provider_registry.get_provider("ollama")
|
|
34
|
+
if provider:
|
|
35
|
+
health = await provider.check_health()
|
|
36
|
+
if health.get("status") == "CONNECTED":
|
|
37
|
+
resp = await provider.generate(
|
|
38
|
+
prompt=f"Classify this request:\n\n{prompt[:1000]}",
|
|
39
|
+
model_id="qwen2.5-coder",
|
|
40
|
+
system_prompt=CLASSIFIER_PROMPT,
|
|
41
|
+
temperature=0.1,
|
|
42
|
+
)
|
|
43
|
+
clean_json = re.search(r"\{.*\}", resp.content, re.DOTALL)
|
|
44
|
+
if clean_json:
|
|
45
|
+
data = json.loads(clean_json.group(0))
|
|
46
|
+
comp = float(data.get("complexity", 0.5))
|
|
47
|
+
comp_label = PriorityLevel.HIGH if comp >= 0.75 else (PriorityLevel.MEDIUM if comp >= 0.45 else PriorityLevel.LOW)
|
|
48
|
+
return RequestAnalysis(
|
|
49
|
+
task_type=TaskType(data.get("task_type", "GENERAL_QA")),
|
|
50
|
+
complexity=comp,
|
|
51
|
+
complexity_label=comp_label,
|
|
52
|
+
reasoning_required=bool(data.get("reasoning_required", False)),
|
|
53
|
+
coding_required=bool(data.get("coding_required", False)),
|
|
54
|
+
vision_required=bool(data.get("vision_required", False)),
|
|
55
|
+
context_size=estimate_tokens(prompt),
|
|
56
|
+
latency_priority=PriorityLevel(data.get("latency_priority", "MEDIUM")),
|
|
57
|
+
cost_sensitivity=PriorityLevel(data.get("cost_sensitivity", "MEDIUM")),
|
|
58
|
+
quality_requirement=PriorityLevel(data.get("quality_requirement", "MEDIUM")),
|
|
59
|
+
keywords_detected=["llm_classified"],
|
|
60
|
+
analyzer_used="llm",
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
# Graceful fallback to heuristics if LLM classifier is unavailable or times out
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Default to fast, deterministic heuristics
|
|
67
|
+
return analyze_request_heuristics(prompt)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
from app.models.schemas import TaskType, PriorityLevel, RequestAnalysis
|
|
4
|
+
|
|
5
|
+
# Regex rules and indicator vocabularies
|
|
6
|
+
DEBUG_PATTERNS = [
|
|
7
|
+
r"\b(debug|bug|stacktrace|exception|error|fix this error|traceback|segfault|syntaxerror|nullpointer)\b",
|
|
8
|
+
r"\b(crash|panicked|failed test|reproduce|root cause|undefined is not a function)\b",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
CODING_PATTERNS = [
|
|
12
|
+
r"\b(write a python|write code|implement|function|class |def |const |async def|typescript|javascript|golang|rust|react|sql query|endpoint|api client)\b",
|
|
13
|
+
r"\b(refactor|algorithm|regex|unit test|dockerfile|kubernetes yaml|prisma|fastapi)\b",
|
|
14
|
+
r"```[a-zA-Z0-9_-]*\n",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
REASONING_PATTERNS = [
|
|
18
|
+
r"\b(explain why|reasoning|step by step|proof|derive|why did|compare and contrast|architectural decision|tradeoffs|trade-offs|consequences of)\b",
|
|
19
|
+
r"\b(game theory|logical fallacy|syllogism|first principles|hypothesize)\b",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
MATH_PATTERNS = [
|
|
23
|
+
r"\b(calculate|integral|derivative|matrix|eigenvalue|probability|equation|differential equation|solve for x|combinatorics|standard deviation)\b",
|
|
24
|
+
r"[\$\\\^\{\}\[\]\=]{3,}",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
SUMMARIZATION_PATTERNS = [
|
|
28
|
+
r"\b(summarize|summary|tldr|tl;dr|key points|bullet points of this|condense|digest|brief overview)\b",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
EXTRACTION_PATTERNS = [
|
|
32
|
+
r"\b(extract|extract json|parse this table|pull all emails|convert to json|output as csv|structured data from)\b",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
TRANSLATION_PATTERNS = [
|
|
36
|
+
r"\b(translate to|translate into|in french|in spanish|in german|in japanese|in chinese|in hindi|translate from)\b",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
CREATIVE_PATTERNS = [
|
|
40
|
+
r"\b(write a poem|write a story|screenplay|brainstorm catchy|creative writing|dialogue between|fiction|metaphor)\b",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
ANALYSIS_PATTERNS = [
|
|
44
|
+
r"\b(analyze|breakdown|evaluate|critique|audit|financial report|trend analysis|pros and cons)\b",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def estimate_tokens(text: str) -> int:
|
|
49
|
+
"""Fast, reliable token estimation (roughly 4 characters per token)."""
|
|
50
|
+
return max(1, len(text) // 4)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def analyze_request_heuristics(prompt: str) -> RequestAnalysis:
|
|
54
|
+
text_lower = prompt.lower()
|
|
55
|
+
tokens = estimate_tokens(prompt)
|
|
56
|
+
|
|
57
|
+
detected_keywords: List[str] = []
|
|
58
|
+
task_scores: dict[TaskType, float] = {t: 0.0 for t in TaskType}
|
|
59
|
+
|
|
60
|
+
# 1. Evaluate patterns
|
|
61
|
+
for p in DEBUG_PATTERNS:
|
|
62
|
+
matches = re.findall(p, text_lower)
|
|
63
|
+
if matches:
|
|
64
|
+
task_scores[TaskType.DEBUGGING] += len(matches) * 2.5
|
|
65
|
+
detected_keywords.extend(matches)
|
|
66
|
+
|
|
67
|
+
for p in CODING_PATTERNS:
|
|
68
|
+
matches = re.findall(p, text_lower)
|
|
69
|
+
if matches:
|
|
70
|
+
task_scores[TaskType.CODING] += len(matches) * 2.0
|
|
71
|
+
detected_keywords.extend(matches)
|
|
72
|
+
|
|
73
|
+
for p in REASONING_PATTERNS:
|
|
74
|
+
matches = re.findall(p, text_lower)
|
|
75
|
+
if matches:
|
|
76
|
+
task_scores[TaskType.REASONING] += len(matches) * 2.0
|
|
77
|
+
detected_keywords.extend(matches)
|
|
78
|
+
|
|
79
|
+
for p in MATH_PATTERNS:
|
|
80
|
+
matches = re.findall(p, text_lower)
|
|
81
|
+
if matches:
|
|
82
|
+
task_scores[TaskType.MATH] += len(matches) * 2.2
|
|
83
|
+
detected_keywords.extend(matches)
|
|
84
|
+
|
|
85
|
+
for p in SUMMARIZATION_PATTERNS:
|
|
86
|
+
matches = re.findall(p, text_lower)
|
|
87
|
+
if matches:
|
|
88
|
+
task_scores[TaskType.SUMMARIZATION] += len(matches) * 2.5
|
|
89
|
+
detected_keywords.extend(matches)
|
|
90
|
+
|
|
91
|
+
for p in EXTRACTION_PATTERNS:
|
|
92
|
+
matches = re.findall(p, text_lower)
|
|
93
|
+
if matches:
|
|
94
|
+
task_scores[TaskType.EXTRACTION] += len(matches) * 2.0
|
|
95
|
+
detected_keywords.extend(matches)
|
|
96
|
+
|
|
97
|
+
for p in TRANSLATION_PATTERNS:
|
|
98
|
+
matches = re.findall(p, text_lower)
|
|
99
|
+
if matches:
|
|
100
|
+
task_scores[TaskType.TRANSLATION] += len(matches) * 3.0
|
|
101
|
+
detected_keywords.extend(matches)
|
|
102
|
+
|
|
103
|
+
for p in CREATIVE_PATTERNS:
|
|
104
|
+
matches = re.findall(p, text_lower)
|
|
105
|
+
if matches:
|
|
106
|
+
task_scores[TaskType.CREATIVE] += len(matches) * 2.0
|
|
107
|
+
detected_keywords.extend(matches)
|
|
108
|
+
|
|
109
|
+
for p in ANALYSIS_PATTERNS:
|
|
110
|
+
matches = re.findall(p, text_lower)
|
|
111
|
+
if matches:
|
|
112
|
+
task_scores[TaskType.ANALYSIS] += len(matches) * 1.8
|
|
113
|
+
detected_keywords.extend(matches)
|
|
114
|
+
|
|
115
|
+
# Long context check
|
|
116
|
+
if tokens > 4000:
|
|
117
|
+
task_scores[TaskType.LONG_CONTEXT] += 3.0
|
|
118
|
+
|
|
119
|
+
# 2. Determine Primary Task
|
|
120
|
+
best_task = TaskType.GENERAL_QA
|
|
121
|
+
max_score = 0.0
|
|
122
|
+
for task, score in task_scores.items():
|
|
123
|
+
if score > max_score:
|
|
124
|
+
max_score = score
|
|
125
|
+
best_task = task
|
|
126
|
+
|
|
127
|
+
# 3. Derive Complexity (0.0 to 1.0)
|
|
128
|
+
complexity = 0.35 # Base complexity
|
|
129
|
+
|
|
130
|
+
# Token length factor
|
|
131
|
+
if tokens > 10000:
|
|
132
|
+
complexity += 0.30
|
|
133
|
+
elif tokens > 3000:
|
|
134
|
+
complexity += 0.20
|
|
135
|
+
elif tokens > 800:
|
|
136
|
+
complexity += 0.10
|
|
137
|
+
elif tokens < 30:
|
|
138
|
+
complexity -= 0.10
|
|
139
|
+
|
|
140
|
+
# Task inherent complexity
|
|
141
|
+
if best_task in (TaskType.DEBUGGING, TaskType.MATH, TaskType.REASONING):
|
|
142
|
+
complexity += 0.30
|
|
143
|
+
elif best_task in (TaskType.CODING, TaskType.ANALYSIS, TaskType.LONG_CONTEXT):
|
|
144
|
+
complexity += 0.20
|
|
145
|
+
elif best_task in (TaskType.TRANSLATION, TaskType.EXTRACTION, TaskType.SUMMARIZATION):
|
|
146
|
+
complexity += 0.05
|
|
147
|
+
elif best_task == TaskType.GENERAL_QA and tokens < 50:
|
|
148
|
+
complexity -= 0.15
|
|
149
|
+
|
|
150
|
+
# Multi-step keyword boosts
|
|
151
|
+
if any(k in text_lower for k in ["distributed", "async", "concurrency", "deadlock", "microservices", "architecture"]):
|
|
152
|
+
complexity += 0.25
|
|
153
|
+
if any(k in text_lower for k in ["simple", "quick", "one liner", "easy", "hello world"]):
|
|
154
|
+
complexity -= 0.20
|
|
155
|
+
|
|
156
|
+
complexity = max(0.05, min(0.99, round(complexity, 2)))
|
|
157
|
+
|
|
158
|
+
# 4. Priority and Capabilities
|
|
159
|
+
coding_required = best_task in (TaskType.CODING, TaskType.DEBUGGING) or task_scores[TaskType.CODING] > 0
|
|
160
|
+
reasoning_required = best_task in (TaskType.REASONING, TaskType.DEBUGGING, TaskType.MATH) or complexity >= 0.70
|
|
161
|
+
|
|
162
|
+
if complexity >= 0.75:
|
|
163
|
+
complexity_label = PriorityLevel.HIGH
|
|
164
|
+
quality_req = PriorityLevel.HIGH
|
|
165
|
+
latency_pri = PriorityLevel.LOW
|
|
166
|
+
cost_sens = PriorityLevel.LOW
|
|
167
|
+
elif complexity >= 0.45:
|
|
168
|
+
complexity_label = PriorityLevel.MEDIUM
|
|
169
|
+
quality_req = PriorityLevel.MEDIUM
|
|
170
|
+
latency_pri = PriorityLevel.MEDIUM
|
|
171
|
+
cost_sens = PriorityLevel.MEDIUM
|
|
172
|
+
else:
|
|
173
|
+
complexity_label = PriorityLevel.LOW
|
|
174
|
+
quality_req = PriorityLevel.LOW
|
|
175
|
+
latency_pri = PriorityLevel.HIGH
|
|
176
|
+
cost_sens = PriorityLevel.HIGH
|
|
177
|
+
|
|
178
|
+
return RequestAnalysis(
|
|
179
|
+
task_type=best_task,
|
|
180
|
+
complexity=complexity,
|
|
181
|
+
complexity_label=complexity_label,
|
|
182
|
+
reasoning_required=reasoning_required,
|
|
183
|
+
coding_required=coding_required,
|
|
184
|
+
vision_required=False,
|
|
185
|
+
tools_required=False,
|
|
186
|
+
context_size=tokens,
|
|
187
|
+
latency_priority=latency_pri,
|
|
188
|
+
cost_sensitivity=cost_sens,
|
|
189
|
+
quality_requirement=quality_req,
|
|
190
|
+
keywords_detected=list(set(detected_keywords))[:10],
|
|
191
|
+
analyzer_used="rules",
|
|
192
|
+
)
|