- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
241 lines
7.4 KiB
Go
241 lines
7.4 KiB
Go
package capabilities
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"switchboard-core/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
|
|
}
|