Changeset 0.22.0.1 (#94)

This commit is contained in:
2026-03-02 01:55:19 +00:00
parent 12e03cec8b
commit 06c4e2a5a1
33 changed files with 1929 additions and 418 deletions

View File

@@ -154,7 +154,7 @@ func TestResolveIntrinsic_CatalogWins(t *testing.T) {
ToolCalling: true,
MaxOutputTokens: 16384,
}
merged := ResolveIntrinsic("claude-sonnet-4-20250514", &providerCaps)
merged := ResolveIntrinsic("claude-sonnet-4-20250514", &providerCaps, nil)
if !merged.ToolCalling {
t.Error("tool_calling should be preserved from provider")
@@ -179,7 +179,7 @@ func TestResolveIntrinsic_ProviderDataPreserved(t *testing.T) {
MaxOutputTokens: 8192,
MaxContext: 65536,
}
merged := ResolveIntrinsic("some-unknown-model", &providerCaps)
merged := ResolveIntrinsic("some-unknown-model", &providerCaps, nil)
if !merged.ToolCalling {
t.Error("tool_calling should be preserved from provider")
@@ -191,7 +191,7 @@ func TestResolveIntrinsic_ProviderDataPreserved(t *testing.T) {
func TestResolveIntrinsic_NilProvider(t *testing.T) {
// Nil provider caps — falls through entirely to heuristic
merged := ResolveIntrinsic("gpt-4o", nil)
merged := ResolveIntrinsic("gpt-4o", nil, nil)
if !merged.ToolCalling {
t.Error("should get tool_calling from heuristic (gpt-4 pattern)")
@@ -203,7 +203,7 @@ func TestResolveIntrinsic_NilProvider(t *testing.T) {
func TestResolveIntrinsic_HeuristicOnly(t *testing.T) {
// No catalog, no known table — pure heuristic
merged := ResolveIntrinsic("deepseek-r1-distill-llama-70b", nil)
merged := ResolveIntrinsic("deepseek-r1-distill-llama-70b", nil, nil)
if !merged.Reasoning {
t.Error("should infer reasoning from deepseek-r1 pattern")
@@ -229,3 +229,71 @@ func TestHasProviderData(t *testing.T) {
t.Error("caps with max_context should have provider data")
}
}
func TestResolveIntrinsic_AdminOverrides(t *testing.T) {
// Catalog says no vision, heuristic says no vision,
// but admin override forces vision=true.
catalogCaps := &models.ModelCapabilities{
ToolCalling: true,
Vision: false,
}
overrides := []models.CapabilityOverride{
{Field: "vision", Value: "true"},
}
merged := ResolveIntrinsic("some-custom-model", catalogCaps, overrides)
if !merged.Vision {
t.Error("admin override should force vision=true")
}
if !merged.ToolCalling {
t.Error("catalog tool_calling should be preserved")
}
}
func TestResolveIntrinsic_AdminOverrides_DisableCapability(t *testing.T) {
// Heuristic infers tool_calling for gpt-4o, but admin says no.
overrides := []models.CapabilityOverride{
{Field: "tool_calling", Value: "false"},
}
merged := ResolveIntrinsic("gpt-4o", nil, overrides)
if merged.ToolCalling {
t.Error("admin override should disable tool_calling")
}
}
func TestResolveIntrinsic_AdminOverrides_IntValues(t *testing.T) {
overrides := []models.CapabilityOverride{
{Field: "max_context", Value: "200000"},
{Field: "max_output_tokens", Value: "16384"},
}
merged := ResolveIntrinsic("some-model", nil, overrides)
if merged.MaxContext != 200000 {
t.Errorf("expected max_context=200000, got %d", merged.MaxContext)
}
if merged.MaxOutputTokens != 16384 {
t.Errorf("expected max_output_tokens=16384, got %d", merged.MaxOutputTokens)
}
}
func TestResolveIntrinsic_OverridePriority(t *testing.T) {
// Catalog says max_context=128000, admin overrides to 256000.
// Admin should win.
catalogCaps := &models.ModelCapabilities{
MaxContext: 128000,
}
overrides := []models.CapabilityOverride{
{Field: "max_context", Value: "256000"},
}
merged := ResolveIntrinsic("some-model", catalogCaps, overrides)
if merged.MaxContext != 256000 {
t.Errorf("admin override should win over catalog: expected 256000, got %d", merged.MaxContext)
}
}

View File

@@ -111,14 +111,16 @@ func InferCapabilities(modelID string) models.ModelCapabilities {
}
// ResolveIntrinsic determines the intrinsic capabilities of a model.
// Priority:
// 1. catalogCaps (from model_catalog DB — synced from provider API)
// 2. Heuristic inference (regex patterns on model ID)
// 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. No hardcoded model table —
// 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) models.ModelCapabilities {
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() {
@@ -128,9 +130,56 @@ func ResolveIntrinsic(modelID string, catalogCaps *models.ModelCapabilities) mod
// 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 {

View File

@@ -63,7 +63,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
countGlobal := len(globalModels)
for _, entry := range globalModels {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities, nil)
prov := providerMap[entry.ProviderConfigID]
result = append(result, models.UserModel{
@@ -100,7 +100,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
continue
}
for _, entry := range entries {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities, nil)
result = append(result, models.UserModel{
ID: entry.ModelID,
DisplayName: displayName(entry.DisplayName, entry.ModelID),
@@ -138,7 +138,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
continue
}
for _, entry := range entries {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities, nil)
result = append(result, models.UserModel{
ID: entry.ModelID,
DisplayName: displayName(entry.DisplayName, entry.ModelID),
@@ -181,7 +181,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
catalogCaps = &entry.Capabilities
}
}
caps := ResolveIntrinsic(p.BaseModelID, catalogCaps)
caps := ResolveIntrinsic(p.BaseModelID, catalogCaps, nil)
// Load tool grants
toolGrants, _ := stores.Personas.GetToolGrants(ctx, p.ID)
@@ -251,7 +251,7 @@ func ResolveForPersona(ctx context.Context, stores store.Stores, persona *models
catalogCaps = &entry.Capabilities
}
}
caps := ResolveIntrinsic(persona.BaseModelID, catalogCaps)
caps := ResolveIntrinsic(persona.BaseModelID, catalogCaps, nil)
toolGrants, err := stores.Personas.GetToolGrants(ctx, persona.ID)
if err != nil {