Files

169 lines
5.7 KiB
Go

package providers
import "sync"
// ModelSpec describes a model's capabilities and cost.
type ModelSpec struct {
ID string
Provider string
ContextWindow int
MaxTokens int
Reasoning bool
Vision bool
TokenizerID string
Cost ModelCost
}
// ModelCost tracks per-1M-token pricing.
type ModelCost struct {
InputPer1M float64
OutputPer1M float64
CacheReadPer1M float64
}
// ModelRegistry resolves model IDs to specs with forward-compatibility support.
type ModelRegistry interface {
Resolve(provider, modelID string) *ModelSpec
Register(spec ModelSpec)
Catalog(provider string) []ModelSpec
}
// ForwardCompatResolver is implemented by providers to handle unknown models.
type ForwardCompatResolver interface {
ResolveForwardCompat(modelID string, registry ModelRegistry) *ModelSpec
}
// InMemoryRegistry is a thread-safe in-memory ModelRegistry.
type InMemoryRegistry struct {
models sync.Map // key: "provider:modelID" → *ModelSpec
resolvers sync.Map // key: provider → ForwardCompatResolver
}
// NewInMemoryRegistry creates a registry and seeds it with known models.
func NewInMemoryRegistry() *InMemoryRegistry {
r := &InMemoryRegistry{}
SeedDefaultModels(r)
return r
}
func registryKey(provider, modelID string) string {
return provider + ":" + modelID
}
// Register adds or updates a model spec.
func (r *InMemoryRegistry) Register(spec ModelSpec) {
r.models.Store(registryKey(spec.Provider, spec.ID), &spec)
}
// RegisterResolver sets the forward-compat resolver for a provider.
func (r *InMemoryRegistry) RegisterResolver(provider string, resolver ForwardCompatResolver) {
r.resolvers.Store(provider, resolver)
}
// Resolve looks up a model: direct hit → forward-compat → nil.
func (r *InMemoryRegistry) Resolve(provider, modelID string) *ModelSpec {
// Direct cache hit
if v, ok := r.models.Load(registryKey(provider, modelID)); ok {
return v.(*ModelSpec)
}
// Forward-compat resolver
if v, ok := r.resolvers.Load(provider); ok {
if resolver, ok := v.(ForwardCompatResolver); ok {
if spec := resolver.ResolveForwardCompat(modelID, r); spec != nil {
// Cache for next lookup
r.Register(*spec)
return spec
}
}
}
return nil
}
// Catalog returns all known specs for a provider.
func (r *InMemoryRegistry) Catalog(provider string) []ModelSpec {
var specs []ModelSpec
prefix := provider + ":"
r.models.Range(func(key, value any) bool {
k := key.(string)
if len(k) > len(prefix) && k[:len(prefix)] == prefix {
specs = append(specs, *value.(*ModelSpec))
}
return true
})
return specs
}
// CloneFromTemplate finds the first matching template and clones it with overrides.
// Returns nil if no template found.
func CloneFromTemplate(registry ModelRegistry, provider, modelID string, templateIDs []string, patch *ModelSpec) *ModelSpec {
for _, tmplID := range templateIDs {
tmpl := registry.Resolve(provider, tmplID)
if tmpl == nil {
continue
}
// Clone template
spec := *tmpl
spec.ID = modelID
spec.Provider = provider
// Apply non-zero patch fields
if patch != nil {
if patch.ContextWindow > 0 {
spec.ContextWindow = patch.ContextWindow
}
if patch.MaxTokens > 0 {
spec.MaxTokens = patch.MaxTokens
}
if patch.TokenizerID != "" {
spec.TokenizerID = patch.TokenizerID
}
if patch.Cost.InputPer1M > 0 {
spec.Cost.InputPer1M = patch.Cost.InputPer1M
}
if patch.Cost.OutputPer1M > 0 {
spec.Cost.OutputPer1M = patch.Cost.OutputPer1M
}
if patch.Cost.CacheReadPer1M > 0 {
spec.Cost.CacheReadPer1M = patch.Cost.CacheReadPer1M
}
// Boolean fields: patch overrides if true
if patch.Reasoning {
spec.Reasoning = true
}
if patch.Vision {
spec.Vision = true
}
}
return &spec
}
return nil
}
// SeedDefaultModels registers well-known models into the registry.
func SeedDefaultModels(r *InMemoryRegistry) {
// Anthropic models
for _, s := range []ModelSpec{
// TokenizerID "cl100k_base" is an approximation — Claude uses a proprietary tokenizer.
// Used for rough token estimation; actual counting should use provider-specific logic.
{ID: "claude-opus-4-6", Provider: "anthropic", ContextWindow: 200_000, MaxTokens: 32_000, Reasoning: true, Vision: true, TokenizerID: "cl100k_base"},
{ID: "claude-sonnet-4-6", Provider: "anthropic", ContextWindow: 200_000, MaxTokens: 16_000, Reasoning: true, Vision: true, TokenizerID: "cl100k_base"},
{ID: "claude-haiku-4-5-20251001", Provider: "anthropic", ContextWindow: 200_000, MaxTokens: 8_192, Reasoning: false, Vision: true, TokenizerID: "cl100k_base"},
} {
r.Register(s)
}
// OpenAI models
for _, s := range []ModelSpec{
{ID: "gpt-5.6-sol", Provider: "openai", ContextWindow: 1_050_000, MaxTokens: 128_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "gpt-5.6-terra", Provider: "openai", ContextWindow: 1_050_000, MaxTokens: 128_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "gpt-5.5", Provider: "openai", ContextWindow: 1_050_000, MaxTokens: 128_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "gpt-5.4", Provider: "openai", ContextWindow: 1_000_000, MaxTokens: 100_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "gpt-5.2", Provider: "openai", ContextWindow: 256_000, MaxTokens: 64_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "gpt-4o", Provider: "openai", ContextWindow: 128_000, MaxTokens: 16_384, Reasoning: false, Vision: true, TokenizerID: "o200k_base"},
{ID: "o3", Provider: "openai", ContextWindow: 200_000, MaxTokens: 100_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
{ID: "o4-mini", Provider: "openai", ContextWindow: 200_000, MaxTokens: 100_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"},
} {
r.Register(s)
}
}