package capabilities import ( "regexp" "strings" "git.gobha.me/xcaliber/chat-switchboard/models" ) // ── Known Model Defaults ──────────────────── // // REMOVED in 0.9.1: The static known model table was removed because the same // model ID can have different capabilities depending on the provider hosting it // (e.g. DeepSeek on Venice has no tool_calling, same model on OpenRouter does). // // Capability resolution chain: // 1. Catalog (provider API sync) — authoritative per-provider // 2. Heuristic inference (regex on model ID) — best effort // 3. Admin override — manual correction (shipped v0.22.0) // // The catalog is populated when providers are added (auto-fetch) or refreshed. // Heuristics cover broad model families (llama→tools, gemini→vision, etc.) // until catalog data is available. // LookupKnownModel always returns false — kept for interface compatibility. // Callers fall through to heuristic inference. func LookupKnownModel(modelID string) (models.ModelCapabilities, bool) { return models.ModelCapabilities{}, false } // ── Heuristic Capability Detection ────────── var ( toolPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)llama[_-]?[34]`), regexp.MustCompile(`(?i)granite[_-]?[34]`), regexp.MustCompile(`(?i)hermes[_-]?[23]?`), regexp.MustCompile(`(?i)qwen[_-]?[23]`), regexp.MustCompile(`(?i)mistral|mixtral`), regexp.MustCompile(`(?i)command[_-]?r`), regexp.MustCompile(`(?i)phi[_-]?[34]`), regexp.MustCompile(`(?i)deepseek[_-]?v[23]`), regexp.MustCompile(`(?i)functionary`), regexp.MustCompile(`(?i)^gpt[_-]?[345]`), regexp.MustCompile(`(?i)^openai[_-]gpt`), regexp.MustCompile(`(?i)^claude`), regexp.MustCompile(`(?i)gemma[_-]?[23]`), regexp.MustCompile(`(?i)glm[_-]?[45]`), regexp.MustCompile(`(?i)gemini`), regexp.MustCompile(`(?i)grok`), regexp.MustCompile(`(?i)kimi`), regexp.MustCompile(`(?i)minimax`), } visionPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)llava|bakllava`), regexp.MustCompile(`(?i)moondream`), regexp.MustCompile(`(?i)llama.*vision|vision.*llama`), regexp.MustCompile(`(?i)minicpm[_-]?v`), regexp.MustCompile(`(?i)^gpt[_-]?4[_-]?o|^gpt[_-]?4[_-]?turbo`), regexp.MustCompile(`(?i)^claude[_-]?(3|opus|sonnet)`), regexp.MustCompile(`(?i)gemini`), regexp.MustCompile(`(?i)qwen.*vl|vl.*qwen`), regexp.MustCompile(`(?i)phi[_-]?[34].*vision`), regexp.MustCompile(`(?i)internvl`), regexp.MustCompile(`(?i)glm[_-]?4v`), regexp.MustCompile(`(?i)gemma[_-]?3`), regexp.MustCompile(`(?i)grok[_-]?4`), regexp.MustCompile(`(?i)mistral.*24b`), } reasoningPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)deepseek[_-]?r1`), regexp.MustCompile(`(?i)qwq`), regexp.MustCompile(`(?i)^o[1234][_-]|^o[1234]$`), regexp.MustCompile(`(?i)thinking`), regexp.MustCompile(`(?i)grok`), regexp.MustCompile(`(?i)glm[_-]?[45]`), } codePatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)codellama|code[_-]?llama`), regexp.MustCompile(`(?i)deepseek[_-]?coder`), regexp.MustCompile(`(?i)starcoder`), regexp.MustCompile(`(?i)coder|codestral`), regexp.MustCompile(`(?i)wizardcoder`), regexp.MustCompile(`(?i)codegemma`), } ) func matchesAny(id string, patterns []*regexp.Regexp) bool { for _, p := range patterns { if p.MatchString(id) { return true } } return false } // InferCapabilities guesses model capabilities from the model ID string. // Best-effort fallback when the provider API hasn't reported capabilities. func InferCapabilities(modelID string) models.ModelCapabilities { id := normalizeModelID(modelID) return models.ModelCapabilities{ Streaming: true, // virtually everything streams ToolCalling: matchesAny(id, toolPatterns), Vision: matchesAny(id, visionPatterns), Reasoning: matchesAny(id, reasoningPatterns), CodeOptimized: matchesAny(id, codePatterns), } } // ResolveIntrinsic determines the intrinsic capabilities of a model. // Priority (highest wins): // 1. Admin overrides (manual corrections from capability_overrides table) // 2. Catalog (from model_catalog DB — synced from provider API) // 3. Heuristic inference (regex patterns on model ID) // // The catalog is the source of truth per provider. Heuristics fill gaps // for models that haven't been synced yet. Admin overrides correct // provider-reported or heuristic errors. No hardcoded model table — // the same model can have different capabilities on different providers. func ResolveIntrinsic(modelID string, catalogCaps *models.ModelCapabilities, overrides []models.CapabilityOverride) models.ModelCapabilities { // Start with catalog data if available var base models.ModelCapabilities if catalogCaps != nil && catalogCaps.HasProviderData() { base = *catalogCaps } // Fill gaps from heuristic inference inferred := InferCapabilities(modelID) mergeGaps(&base, &inferred) // Apply admin overrides (highest priority — can flip any field) applyOverrides(&base, overrides) return base } // applyOverrides applies admin capability corrections to the resolved capabilities. func applyOverrides(caps *models.ModelCapabilities, overrides []models.CapabilityOverride) { for _, o := range overrides { boolVal := o.Value == "true" || o.Value == "1" switch o.Field { case "streaming": caps.Streaming = boolVal case "tool_calling": caps.ToolCalling = boolVal case "vision": caps.Vision = boolVal case "thinking": caps.Thinking = boolVal case "reasoning": caps.Reasoning = boolVal case "code_optimized": caps.CodeOptimized = boolVal case "web_search": caps.WebSearch = boolVal case "max_context": if n := parseInt(o.Value); n > 0 { caps.MaxContext = n } case "max_output_tokens": if n := parseInt(o.Value); n > 0 { caps.MaxOutputTokens = n } } } } // parseInt parses a string to int, returning 0 on failure. func parseInt(s string) int { n := 0 for _, c := range s { if c < '0' || c > '9' { return 0 } n = n*10 + int(c-'0') } return n } // ResolveMaxOutput returns the max output tokens for a model. // Priority: explicit caps → derive from context → 4096 fallback. func ResolveMaxOutput(modelID string, caps models.ModelCapabilities) int { if caps.MaxOutputTokens > 0 { return caps.MaxOutputTokens } if caps.MaxContext > 0 { derived := caps.MaxContext / 8 if derived < 2048 { derived = 2048 } if derived > 16384 { derived = 16384 } return derived } return 4096 } // mergeGaps fills zero/false fields in dst from src. Never overrides existing data. func mergeGaps(dst, src *models.ModelCapabilities) { if !dst.Streaming && src.Streaming { dst.Streaming = true } if !dst.ToolCalling && src.ToolCalling { dst.ToolCalling = true } if !dst.Vision && src.Vision { dst.Vision = true } if !dst.Thinking && src.Thinking { dst.Thinking = true } if !dst.Reasoning && src.Reasoning { dst.Reasoning = true } if !dst.CodeOptimized && src.CodeOptimized { dst.CodeOptimized = true } if !dst.WebSearch && src.WebSearch { dst.WebSearch = true } if dst.MaxContext == 0 && src.MaxContext > 0 { dst.MaxContext = src.MaxContext } if dst.MaxOutputTokens == 0 && src.MaxOutputTokens > 0 { dst.MaxOutputTokens = src.MaxOutputTokens } } // normalizeModelID strips provider prefix and lowercases. func normalizeModelID(modelID string) string { id := strings.ToLower(modelID) if idx := strings.Index(id, "/"); idx >= 0 { id = id[idx+1:] } return id }