155 lines
5.6 KiB
Python
155 lines
5.6 KiB
Python
from typing import List, Dict, Tuple, Optional
|
|
from app.models.schemas import ModelMetadata, RequestAnalysis, CandidateScore, PriorityLevel
|
|
|
|
|
|
def filter_candidate_models(
|
|
models: List[ModelMetadata],
|
|
analysis: RequestAnalysis,
|
|
) -> Tuple[List[ModelMetadata], Dict[str, str]]:
|
|
"""
|
|
Hard-filtering step: eliminates models that cannot satisfy non-negotiable requirements.
|
|
Returns: (eligible_models, rejected_candidates_with_reasons)
|
|
"""
|
|
eligible: List[ModelMetadata] = []
|
|
rejected: Dict[str, str] = {}
|
|
|
|
for m in models:
|
|
if not m.is_active:
|
|
rejected[m.id] = "Model is currently disabled/inactive."
|
|
continue
|
|
|
|
# 1. Context Window check
|
|
if analysis.context_size > m.context_window:
|
|
rejected[m.id] = (
|
|
f"Context window ({m.context_window} tokens) is smaller than required input ({analysis.context_size} tokens)."
|
|
)
|
|
continue
|
|
|
|
# 2. Vision requirement check
|
|
if analysis.vision_required and not m.supports_vision:
|
|
rejected[m.id] = "Task requires vision / multimodal capabilities which this model does not support."
|
|
continue
|
|
|
|
# 3. Coding capability check (for high complexity code tasks)
|
|
if analysis.coding_required and analysis.complexity >= 0.70 and not m.supports_coding:
|
|
rejected[m.id] = "Task requires specialized coding capabilities for complex logic."
|
|
continue
|
|
|
|
# 4. Deep reasoning capability check
|
|
if analysis.reasoning_required and analysis.complexity >= 0.85 and not m.supports_reasoning:
|
|
rejected[m.id] = "Task requires advanced reasoning capabilities for multi-step problem solving."
|
|
continue
|
|
|
|
eligible.append(m)
|
|
|
|
# If all filtered out (edge case), return active models to avoid total failure
|
|
if not eligible and models:
|
|
for m in models:
|
|
if m.is_active:
|
|
eligible.append(m)
|
|
rejected.clear()
|
|
|
|
return eligible, rejected
|
|
|
|
|
|
def compute_candidate_score(
|
|
model: ModelMetadata,
|
|
analysis: RequestAnalysis,
|
|
weights: Dict[str, float],
|
|
) -> CandidateScore:
|
|
"""
|
|
Multi-criteria configurable scoring formula.
|
|
Quality, Speed, CostEfficiency, CapabilityMatch, Reliability.
|
|
Normalized score: 0 to 100.
|
|
"""
|
|
# 1. Quality Component (0.0 to 1.0)
|
|
quality_comp = model.quality_score
|
|
if analysis.quality_requirement == PriorityLevel.HIGH:
|
|
# Boost premium models for high quality requirements
|
|
if model.tier.value == "POWER":
|
|
quality_comp = min(1.0, quality_comp * 1.15)
|
|
elif analysis.quality_requirement == PriorityLevel.LOW:
|
|
quality_comp = model.quality_score * 0.9
|
|
|
|
# 2. Speed Component (0.0 to 1.0)
|
|
speed_comp = model.speed_score
|
|
if analysis.latency_priority == PriorityLevel.HIGH:
|
|
if model.tier.value == "FAST":
|
|
speed_comp = min(1.0, speed_comp * 1.20)
|
|
elif analysis.latency_priority == PriorityLevel.LOW:
|
|
speed_comp = model.speed_score * 0.85
|
|
|
|
# 3. Cost Efficiency Component (0.0 to 1.0)
|
|
# Zero-cost local models get 1.0, cheaper cloud models get high score
|
|
combined_token_cost = (model.cost_per_input_token * 1000) + (model.cost_per_output_token * 1000)
|
|
if combined_token_cost == 0.0:
|
|
cost_efficiency = 1.0
|
|
else:
|
|
# Scale: $0.0001 -> 0.95, $0.01 -> 0.50, $0.05 -> 0.10
|
|
cost_efficiency = max(0.05, min(0.99, 1.0 / (1.0 + (combined_token_cost * 100.0))))
|
|
|
|
if analysis.cost_sensitivity == PriorityLevel.HIGH:
|
|
cost_efficiency = min(1.0, cost_efficiency * 1.25)
|
|
|
|
# 4. Capability Match Component (0.0 to 1.0)
|
|
cap_points = 0.0
|
|
total_caps = 0.0
|
|
|
|
if analysis.coding_required:
|
|
total_caps += 1.0
|
|
if model.supports_coding:
|
|
cap_points += 1.0
|
|
|
|
if analysis.reasoning_required:
|
|
total_caps += 1.0
|
|
if model.supports_reasoning:
|
|
cap_points += 1.0
|
|
|
|
if analysis.vision_required:
|
|
total_caps += 1.0
|
|
if model.supports_vision:
|
|
cap_points += 1.0
|
|
|
|
capability_comp = (cap_points / total_caps) if total_caps > 0 else 0.90
|
|
|
|
# 5. Reliability Component (0.0 to 1.0)
|
|
reliability_comp = model.reliability_score
|
|
|
|
# Weighted Sum
|
|
w_q = weights.get("quality_weight", 0.35)
|
|
w_c = weights.get("cost_weight", 0.25)
|
|
w_s = weights.get("speed_weight", 0.20)
|
|
w_cap = weights.get("capability_weight", 0.15)
|
|
w_r = weights.get("reliability_weight", 0.05)
|
|
total_w = w_q + w_c + w_s + w_cap + w_r
|
|
|
|
raw_score = (
|
|
(quality_comp * w_q)
|
|
+ (cost_efficiency * w_c)
|
|
+ (speed_comp * w_s)
|
|
+ (capability_comp * w_cap)
|
|
+ (reliability_comp * w_r)
|
|
) / (total_w if total_w > 0 else 1.0)
|
|
|
|
overall_score = round(raw_score * 100.0, 2)
|
|
|
|
# Estimated metrics
|
|
est_latency = 120.0 if model.tier.value == "FAST" else (350.0 if model.tier.value == "BALANCED" else 750.0)
|
|
est_cost = (analysis.context_size * model.cost_per_input_token) + (250 * model.cost_per_output_token)
|
|
|
|
return CandidateScore(
|
|
model_id=model.id,
|
|
model_name=model.name,
|
|
provider=model.provider,
|
|
tier=model.tier.value if hasattr(model.tier, "value") else str(model.tier),
|
|
overall_score=overall_score,
|
|
quality_component=round(quality_comp * 100.0, 1),
|
|
cost_component=round(cost_efficiency * 100.0, 1),
|
|
speed_component=round(speed_comp * 100.0, 1),
|
|
capability_component=round(capability_comp * 100.0, 1),
|
|
reliability_component=round(reliability_comp * 100.0, 1),
|
|
estimated_latency_ms=est_latency,
|
|
estimated_cost_usd=round(est_cost, 6),
|
|
eligible=True,
|
|
)
|