397 lines
12 KiB
Go
397 lines
12 KiB
Go
package providers
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// ModelCapabilities describes what a model can do and its limits.
|
|
// Zero values mean "unknown / use heuristic".
|
|
type ModelCapabilities struct {
|
|
Streaming bool `json:"streaming"`
|
|
ToolCalling bool `json:"tool_calling"`
|
|
Vision bool `json:"vision"`
|
|
Thinking bool `json:"thinking"`
|
|
Reasoning bool `json:"reasoning"`
|
|
CodeOptimized bool `json:"code_optimized"`
|
|
WebSearch bool `json:"web_search"`
|
|
MaxContext int `json:"max_context"`
|
|
MaxOutputTokens int `json:"max_output_tokens"`
|
|
}
|
|
|
|
// ── Known Model Defaults ────────────────────
|
|
// Authoritative output limits for models where the provider API
|
|
// doesn't report them. Keyed by exact model ID or prefix.
|
|
//
|
|
// Sources:
|
|
// Anthropic: https://docs.anthropic.com/en/docs/about-claude/models
|
|
// OpenAI: https://platform.openai.com/docs/models
|
|
// Meta: Model cards on Hugging Face
|
|
// Google: https://ai.google.dev/gemini-api/docs/models
|
|
|
|
var knownModels = map[string]ModelCapabilities{
|
|
// ── Anthropic ────────────────────────────
|
|
"claude-opus-4": {
|
|
Streaming: true, ToolCalling: true, Vision: true, Thinking: true,
|
|
MaxContext: 200000, MaxOutputTokens: 32000,
|
|
},
|
|
"claude-sonnet-4": {
|
|
Streaming: true, ToolCalling: true, Vision: true, Thinking: true,
|
|
MaxContext: 200000, MaxOutputTokens: 16000,
|
|
},
|
|
"claude-3-5-sonnet": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 200000, MaxOutputTokens: 8192,
|
|
},
|
|
"claude-3-5-haiku": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 200000, MaxOutputTokens: 8192,
|
|
},
|
|
"claude-3-opus": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 200000, MaxOutputTokens: 4096,
|
|
},
|
|
"claude-3-haiku": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 200000, MaxOutputTokens: 4096,
|
|
},
|
|
|
|
// ── OpenAI ──────────────────────────────
|
|
"gpt-4o": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 128000, MaxOutputTokens: 16384,
|
|
},
|
|
"gpt-4o-mini": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 128000, MaxOutputTokens: 16384,
|
|
},
|
|
"gpt-4-turbo": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 128000, MaxOutputTokens: 4096,
|
|
},
|
|
"gpt-4": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 8192, MaxOutputTokens: 4096,
|
|
},
|
|
"o1": {
|
|
Streaming: true, Reasoning: true,
|
|
MaxContext: 200000, MaxOutputTokens: 100000,
|
|
},
|
|
"o1-mini": {
|
|
Streaming: true, Reasoning: true,
|
|
MaxContext: 128000, MaxOutputTokens: 65536,
|
|
},
|
|
"o3": {
|
|
Streaming: true, ToolCalling: true, Reasoning: true,
|
|
MaxContext: 200000, MaxOutputTokens: 100000,
|
|
},
|
|
"o3-mini": {
|
|
Streaming: true, ToolCalling: true, Reasoning: true,
|
|
MaxContext: 200000, MaxOutputTokens: 65536,
|
|
},
|
|
"o4-mini": {
|
|
Streaming: true, ToolCalling: true, Reasoning: true,
|
|
MaxContext: 200000, MaxOutputTokens: 100000,
|
|
},
|
|
|
|
// ── Meta Llama ──────────────────────────
|
|
"llama-3.1-405b": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 4096,
|
|
},
|
|
"llama-3.1-70b": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 4096,
|
|
},
|
|
"llama-3.1-8b": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 4096,
|
|
},
|
|
"llama-3.3-70b": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 4096,
|
|
},
|
|
"llama-4-maverick": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 1048576, MaxOutputTokens: 16384,
|
|
},
|
|
"llama-4-scout": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 524288, MaxOutputTokens: 16384,
|
|
},
|
|
|
|
// ── Google Gemini ───────────────────────
|
|
"gemini-2.5-pro": {
|
|
Streaming: true, ToolCalling: true, Vision: true, Thinking: true,
|
|
MaxContext: 1048576, MaxOutputTokens: 65536,
|
|
},
|
|
"gemini-2.5-flash": {
|
|
Streaming: true, ToolCalling: true, Vision: true, Thinking: true,
|
|
MaxContext: 1048576, MaxOutputTokens: 65536,
|
|
},
|
|
"gemini-2.0-flash": {
|
|
Streaming: true, ToolCalling: true, Vision: true,
|
|
MaxContext: 1048576, MaxOutputTokens: 8192,
|
|
},
|
|
|
|
// ── DeepSeek ────────────────────────────
|
|
"deepseek-r1": {
|
|
Streaming: true, Reasoning: true,
|
|
MaxContext: 65536, MaxOutputTokens: 8192,
|
|
},
|
|
"deepseek-v3": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 65536, MaxOutputTokens: 8192,
|
|
},
|
|
"deepseek-chat": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 65536, MaxOutputTokens: 8192,
|
|
},
|
|
|
|
// ── Mistral ─────────────────────────────
|
|
"mistral-large": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 8192,
|
|
},
|
|
"mistral-small": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 8192,
|
|
},
|
|
"codestral": {
|
|
Streaming: true, ToolCalling: true, CodeOptimized: true,
|
|
MaxContext: 262144, MaxOutputTokens: 8192,
|
|
},
|
|
|
|
// ── Qwen ────────────────────────────────
|
|
"qwen-2.5-72b": {
|
|
Streaming: true, ToolCalling: true,
|
|
MaxContext: 131072, MaxOutputTokens: 8192,
|
|
},
|
|
"qwen-2.5-coder-32b": {
|
|
Streaming: true, ToolCalling: true, CodeOptimized: true,
|
|
MaxContext: 131072, MaxOutputTokens: 8192,
|
|
},
|
|
"qwq-32b": {
|
|
Streaming: true, Reasoning: true,
|
|
MaxContext: 131072, MaxOutputTokens: 8192,
|
|
},
|
|
}
|
|
|
|
// LookupKnownModel finds capabilities for a model by exact ID or prefix match.
|
|
// Returns the caps and true if found, zero value and false if not.
|
|
func LookupKnownModel(modelID string) (ModelCapabilities, bool) {
|
|
id := strings.ToLower(modelID)
|
|
|
|
// Strip provider prefix (e.g. "anthropic/claude-sonnet-4-20250514" → "claude-sonnet-4-20250514")
|
|
if idx := strings.Index(id, "/"); idx >= 0 {
|
|
id = id[idx+1:]
|
|
}
|
|
|
|
// Exact match first
|
|
if caps, ok := knownModels[id]; ok {
|
|
return caps, true
|
|
}
|
|
|
|
// Prefix match: "claude-sonnet-4-20250514" matches "claude-sonnet-4"
|
|
var bestKey string
|
|
var bestCaps ModelCapabilities
|
|
for key, caps := range knownModels {
|
|
if strings.HasPrefix(id, key) && len(key) > len(bestKey) {
|
|
bestKey = key
|
|
bestCaps = caps
|
|
}
|
|
}
|
|
if bestKey != "" {
|
|
return bestCaps, true
|
|
}
|
|
|
|
return ModelCapabilities{}, false
|
|
}
|
|
|
|
// ── Heuristic Capability Detection ──────────
|
|
// Ported from ai-editor's ProviderRegistry.
|
|
// Used for Ollama, LM Studio, and other generic endpoints
|
|
// that don't report capabilities in their /models response.
|
|
|
|
var (
|
|
toolPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)llama[_-]?[34]`),
|
|
regexp.MustCompile(`(?i)granite[_-]?[34]`),
|
|
regexp.MustCompile(`(?i)hermes[_-]?[23]?`),
|
|
regexp.MustCompile(`(?i)qwen[_-]?2`),
|
|
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[_-]?[34]`),
|
|
regexp.MustCompile(`(?i)^claude`),
|
|
regexp.MustCompile(`(?i)gemma[_-]?2`),
|
|
regexp.MustCompile(`(?i)glm[_-]?4`),
|
|
regexp.MustCompile(`(?i)gemini`),
|
|
}
|
|
|
|
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`),
|
|
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`),
|
|
}
|
|
|
|
reasoningPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)deepseek[_-]?r1`),
|
|
regexp.MustCompile(`(?i)qwq`),
|
|
regexp.MustCompile(`(?i)^o[1234][_-]|^o[1234]$`),
|
|
}
|
|
|
|
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.
|
|
// This is the fallback when neither the known model table nor the provider
|
|
// API provides capability data.
|
|
func InferCapabilities(modelID string) ModelCapabilities {
|
|
id := strings.ToLower(modelID)
|
|
// Strip provider prefix
|
|
if idx := strings.Index(id, "/"); idx >= 0 {
|
|
id = id[idx+1:]
|
|
}
|
|
|
|
return ModelCapabilities{
|
|
Streaming: true, // virtually everything streams
|
|
ToolCalling: matchesAny(id, toolPatterns),
|
|
Vision: matchesAny(id, visionPatterns),
|
|
Reasoning: matchesAny(id, reasoningPatterns),
|
|
CodeOptimized: matchesAny(id, codePatterns),
|
|
}
|
|
}
|
|
|
|
// ResolveMaxOutput returns the max output tokens for a model.
|
|
// Priority:
|
|
// 1. Explicit value in caps (from DB / provider API)
|
|
// 2. Known model table
|
|
// 3. Derive from context window (context/8, clamped 2048..16384)
|
|
// 4. 4096 as absolute last resort
|
|
//
|
|
// This is the ONE place the default lives. Nothing else in the
|
|
// codebase should hardcode a max_tokens value.
|
|
func ResolveMaxOutput(modelID string, caps ModelCapabilities) int {
|
|
// 1. Already set (from model_configs DB or provider API)
|
|
if caps.MaxOutputTokens > 0 {
|
|
return caps.MaxOutputTokens
|
|
}
|
|
|
|
// 2. Known model table
|
|
if known, ok := LookupKnownModel(modelID); ok && known.MaxOutputTokens > 0 {
|
|
return known.MaxOutputTokens
|
|
}
|
|
|
|
// 3. Derive from context window
|
|
if caps.MaxContext > 0 {
|
|
derived := caps.MaxContext / 8
|
|
if derived < 2048 {
|
|
derived = 2048
|
|
}
|
|
if derived > 16384 {
|
|
derived = 16384
|
|
}
|
|
return derived
|
|
}
|
|
|
|
// 4. Last resort — the ONLY place 4096 appears as a default
|
|
return 4096
|
|
}
|
|
|
|
// MergeCapabilities takes authoritative caps (from provider API or DB) and fills
|
|
// gaps from the known model table and heuristic detection. Provider-reported data
|
|
// always wins; known table fills missing fields; heuristics are last resort.
|
|
func MergeCapabilities(authoritative ModelCapabilities, modelID string) ModelCapabilities {
|
|
merged := authoritative
|
|
|
|
// Fill gaps from known model table
|
|
known, found := LookupKnownModel(modelID)
|
|
if found {
|
|
if !merged.ToolCalling && known.ToolCalling {
|
|
merged.ToolCalling = true
|
|
}
|
|
if !merged.Vision && known.Vision {
|
|
merged.Vision = true
|
|
}
|
|
if !merged.Thinking && known.Thinking {
|
|
merged.Thinking = true
|
|
}
|
|
if !merged.Reasoning && known.Reasoning {
|
|
merged.Reasoning = true
|
|
}
|
|
if !merged.CodeOptimized && known.CodeOptimized {
|
|
merged.CodeOptimized = true
|
|
}
|
|
if !merged.WebSearch && known.WebSearch {
|
|
merged.WebSearch = true
|
|
}
|
|
if merged.MaxContext == 0 && known.MaxContext > 0 {
|
|
merged.MaxContext = known.MaxContext
|
|
}
|
|
if merged.MaxOutputTokens == 0 && known.MaxOutputTokens > 0 {
|
|
merged.MaxOutputTokens = known.MaxOutputTokens
|
|
}
|
|
return merged
|
|
}
|
|
|
|
// No known model — fill gaps from heuristics
|
|
inferred := InferCapabilities(modelID)
|
|
if !merged.ToolCalling && inferred.ToolCalling {
|
|
merged.ToolCalling = true
|
|
}
|
|
if !merged.Vision && inferred.Vision {
|
|
merged.Vision = true
|
|
}
|
|
if !merged.Thinking && inferred.Thinking {
|
|
merged.Thinking = true
|
|
}
|
|
if !merged.Reasoning && inferred.Reasoning {
|
|
merged.Reasoning = true
|
|
}
|
|
if !merged.CodeOptimized && inferred.CodeOptimized {
|
|
merged.CodeOptimized = true
|
|
}
|
|
if merged.MaxContext == 0 && inferred.MaxContext > 0 {
|
|
merged.MaxContext = inferred.MaxContext
|
|
}
|
|
if merged.MaxOutputTokens == 0 && inferred.MaxOutputTokens > 0 {
|
|
merged.MaxOutputTokens = inferred.MaxOutputTokens
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
// HasProviderData returns true if this capability set contains any data that
|
|
// was likely reported by a provider (not just zero values).
|
|
func (c ModelCapabilities) HasProviderData() bool {
|
|
return c.ToolCalling || c.Vision || c.Thinking || c.Reasoning ||
|
|
c.CodeOptimized || c.WebSearch || c.MaxContext > 0 || c.MaxOutputTokens > 0
|
|
}
|