diff --git a/cmd/fetch_devin_models/main.go b/cmd/fetch_devin_models/main.go index 89844ce2d..454691010 100644 --- a/cmd/fetch_devin_models/main.go +++ b/cmd/fetch_devin_models/main.go @@ -32,6 +32,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" sdkauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" @@ -407,6 +408,90 @@ func formatRawModels(raw []rawDevinModel) []devinModelJSON { return res } +var devinCompoundSuffixes = []struct { + suffix string + effort string + readd string +}{ + {suffix: "-low-fast", effort: "low"}, + {suffix: "-medium-fast", effort: "medium"}, + {suffix: "-high-fast", effort: "high"}, + {suffix: "-xhigh-fast", effort: "xhigh"}, + {suffix: "-max-fast", effort: "max"}, + {suffix: "-none-fast", effort: "none"}, + {suffix: "-low-priority", effort: "low"}, + {suffix: "-medium-priority", effort: "medium"}, + {suffix: "-high-priority", effort: "high"}, + {suffix: "-xhigh-priority", effort: "xhigh"}, + {suffix: "-max-priority", effort: "max"}, + {suffix: "-none-priority", effort: "none"}, + {suffix: "-thinking-1m", effort: "", readd: "-1m"}, + {suffix: "-thinking", effort: ""}, + {suffix: "-max-1m", effort: "max", readd: "-1m"}, + {suffix: "-none-1m", effort: "none", readd: "-1m"}, +} + +var devinSimpleEffortSuffixes = []struct { + suffix string + effort string +}{ + {suffix: "-none", effort: "none"}, + {suffix: "-minimal", effort: "minimal"}, + {suffix: "-low", effort: "low"}, + {suffix: "-medium", effort: "medium"}, + {suffix: "-high", effort: "high"}, + {suffix: "-xhigh", effort: "xhigh"}, + {suffix: "-max", effort: "max"}, +} + +func splitDevinUID(uid string) (string, string) { + if uid == "swe-1-6-slow" { + return uid, "" + } + if uid == "swe-1-6-fast" { + return "swe-1-6", "" + } + + upper := strings.ToUpper(uid) + for _, s := range []struct { + suffix string + effort string + }{ + {"_NONE", "none"}, + {"_MINIMAL", "minimal"}, + {"_LOW", "low"}, + {"_MEDIUM", "medium"}, + {"_HIGH", "high"}, + {"_XHIGH", "xhigh"}, + {"_MAX", "max"}, + {"_THINKING", "high"}, + } { + if strings.HasSuffix(upper, s.suffix) { + base := uid[:len(uid)-len(s.suffix)] + return base, s.effort + } + } + + for _, s := range devinCompoundSuffixes { + if strings.HasSuffix(uid, s.suffix) { + base := uid[:len(uid)-len(s.suffix)] + if s.readd != "" { + base += s.readd + } + return base, s.effort + } + } + + for _, s := range devinSimpleEffortSuffixes { + if strings.HasSuffix(uid, s.suffix) { + base := uid[:len(uid)-len(s.suffix)] + return base, s.effort + } + } + + return uid, "" +} + func aggregateModels(raw []rawDevinModel) []devinModelJSON { type aggEntry struct { baseID string @@ -417,43 +502,51 @@ func aggregateModels(raw []rawDevinModel) []devinModelJSON { levels map[string]struct{} } - knownSuffixes := []string{"-minimal", "-low", "-medium", "-high", "-xhigh", "-max", "-none", "-priority"} grouped := make(map[string]*aggEntry) var order []string for _, r := range raw { - base := r.UID - level := "" - - for _, s := range knownSuffixes { - if strings.HasSuffix(base, s) { - level = strings.TrimPrefix(s, "-") - base = strings.TrimSuffix(base, s) - break - } + base, level := splitDevinUID(r.UID) + if base == "" { + base = r.UID } + isBase := (base == r.UID) entry, exists := grouped[base] if !exists { + initialLevels := make(map[string]struct{}) + if mInfo := registry.LookupDevinModel(base); mInfo != nil && mInfo.Thinking != nil { + for _, l := range mInfo.Thinking.Levels { + if l != "" && l != "priority" { + initialLevels[l] = struct{}{} + } + } + } entry = &aggEntry{ baseID: base, displayName: cleanDisplayName(r.Label), vendorID: r.VendorID, contextLength: r.ContextLength, multimodal: r.Multimodal, - levels: make(map[string]struct{}), + levels: initialLevels, } grouped[base] = entry order = append(order, base) } + if isBase { + entry.displayName = cleanDisplayName(r.Label) + if r.VendorID != 0 { + entry.vendorID = r.VendorID + } + } if r.Multimodal { entry.multimodal = true } if r.ContextLength > entry.contextLength { entry.contextLength = r.ContextLength } - if level != "" { + if level != "" && level != "priority" { entry.levels[level] = struct{}{} } } @@ -495,23 +588,45 @@ func aggregateModels(raw []rawDevinModel) []devinModelJSON { return res } +var devinDisplayNameSuffixes = []string{ + " Low Fast", " Medium Fast", " High Fast", " XHigh Fast", " Max Fast", + " Low Thinking Fast", " Medium Thinking Fast", " High Thinking Fast", + " XHigh Thinking Fast", " Max Thinking Fast", " No Thinking Fast", + " Low Thinking", " Medium Thinking", " High Thinking", " XHigh Thinking", + " Max Thinking", " No Thinking", + " Low", " Medium", " High", " XHigh", " Max", " None", " Minimal", + " Thinking", " Fast", +} + func cleanDisplayName(label string) string { - clean := label - for _, s := range []string{" Low", " Medium", " High", " XHigh", " Max", " Minimal", " None", " Priority"} { - clean = strings.TrimSuffix(clean, s) + trimmed := strings.TrimSpace(label) + for { + changed := false + for _, s := range devinDisplayNameSuffixes { + if strings.HasSuffix(strings.ToLower(trimmed), strings.ToLower(s)) { + trimmed = strings.TrimSpace(trimmed[:len(trimmed)-len(s)]) + changed = true + break + } + } + if !changed { + break + } } - return clean + return trimmed } func sortLevels(levels []string) { rank := map[string]int{ - "none": 0, - "minimal": 1, - "low": 2, - "medium": 3, - "high": 4, - "xhigh": 5, - "max": 6, + "none": 0, + "minimal": 1, + "low": 2, + "medium": 3, + "high": 4, + "xhigh": 5, + "max": 6, + "fast": 7, + "priority": 8, } sort.Slice(levels, func(i, j int) bool { rI, okI := rank[levels[i]] @@ -522,6 +637,9 @@ func sortLevels(levels []string) { if !okJ { rJ = 99 } - return rI < rJ + if rI != rJ { + return rI < rJ + } + return levels[i] < levels[j] }) } diff --git a/cmd/fetch_devin_models/main_test.go b/cmd/fetch_devin_models/main_test.go new file mode 100644 index 000000000..31c0058e8 --- /dev/null +++ b/cmd/fetch_devin_models/main_test.go @@ -0,0 +1,136 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestSplitDevinUID(t *testing.T) { + tests := []struct { + uid string + wantBase string + wantEffort string + }{ + {"claude-opus-5-low-fast", "claude-opus-5", "low"}, + {"claude-opus-5-max-fast", "claude-opus-5", "max"}, + {"gpt-6-astra-low-priority", "gpt-6-astra", "low"}, + {"gpt-6-astra-high", "gpt-6-astra", "high"}, + {"MODEL_GPT_5_2_LOW", "MODEL_GPT_5_2", "low"}, + {"MODEL_GPT_5_2_HIGH", "MODEL_GPT_5_2", "high"}, + {"MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL", "MODEL_GOOGLE_GEMINI_3_0_FLASH", "minimal"}, + {"claude-opus-4-6-thinking", "claude-opus-4-6", ""}, + {"claude-opus-4-6-thinking-1m", "claude-opus-4-6-1m", ""}, + {"glm-5-2-max-1m", "glm-5-2-1m", "max"}, + {"swe-1-6-slow", "swe-1-6-slow", ""}, + {"swe-1-6-fast", "swe-1-6", ""}, + {"swe-2", "swe-2", ""}, + } + + for _, tt := range tests { + t.Run(tt.uid, func(t *testing.T) { + base, effort := splitDevinUID(tt.uid) + if base != tt.wantBase { + t.Errorf("splitDevinUID(%q) base = %q, want %q", tt.uid, base, tt.wantBase) + } + if effort != tt.wantEffort { + t.Errorf("splitDevinUID(%q) effort = %q, want %q", tt.uid, effort, tt.wantEffort) + } + }) + } +} + +func TestAggregateModels_MergesThinkingVariants_Issue6141(t *testing.T) { + raw := []rawDevinModel{ + { + UID: "test-opus-model", + Label: "Test Opus Model", + ContextLength: 1000000, + Multimodal: true, + VendorID: 3, + }, + { + UID: "test-opus-model-low-fast", + Label: "Test Opus Model Low Fast", + ContextLength: 1000000, + Multimodal: true, + VendorID: 3, + }, + { + UID: "test-opus-model-high-fast", + Label: "Test Opus Model High Fast", + ContextLength: 1000000, + Multimodal: true, + VendorID: 3, + }, + { + UID: "test-astra-model-low-priority", + Label: "Test Astra Model Low Thinking Fast", + ContextLength: 1000000, + Multimodal: true, + VendorID: 2, + }, + { + UID: "test-astra-model-high-priority", + Label: "Test Astra Model High Thinking Fast", + ContextLength: 1000000, + Multimodal: true, + VendorID: 2, + }, + } + + aggregated := aggregateModels(raw) + if len(aggregated) != 2 { + t.Fatalf("expected 2 aggregated models, got %d: %+v", len(aggregated), aggregated) + } + + // Verify test-opus-model + opus := aggregated[0] + if opus.ID != "test-opus-model" { + t.Errorf("expected ID test-opus-model, got %q", opus.ID) + } + if opus.DisplayName != "Test Opus Model" { + t.Errorf("expected DisplayName 'Test Opus Model', got %q", opus.DisplayName) + } + if opus.Thinking == nil { + t.Fatalf("expected Thinking to be populated for test-opus-model") + } + wantOpusLevels := []string{"low", "high"} + if !reflect.DeepEqual(opus.Thinking.Levels, wantOpusLevels) { + t.Errorf("test-opus-model levels = %v, want %v", opus.Thinking.Levels, wantOpusLevels) + } + + // Verify test-astra-model + astra := aggregated[1] + if astra.ID != "test-astra-model" { + t.Errorf("expected ID test-astra-model, got %q", astra.ID) + } + if astra.DisplayName != "Test Astra Model" { + t.Errorf("expected DisplayName 'Test Astra Model', got %q", astra.DisplayName) + } + if astra.Thinking == nil { + t.Fatalf("expected Thinking to be populated for test-astra-model") + } + wantAstraLevels := []string{"low", "high"} + if !reflect.DeepEqual(astra.Thinking.Levels, wantAstraLevels) { + t.Errorf("test-astra-model levels = %v, want %v", astra.Thinking.Levels, wantAstraLevels) + } +} + +func TestAggregateModels_PreservesCatalogThinkingLevels(t *testing.T) { + raw := []rawDevinModel{ + { + UID: "claude-fable-5-1", + Label: "Claude Fable 5.1", + ContextLength: 1000000, + Multimodal: true, + VendorID: 3, + }, + } + aggregated := aggregateModels(raw) + if len(aggregated) != 1 { + t.Fatalf("expected 1 model, got %d", len(aggregated)) + } + if aggregated[0].Thinking == nil || len(aggregated[0].Thinking.Levels) == 0 { + t.Fatalf("expected thinking levels seeded from catalog for claude-fable-5-1, got nil") + } +} diff --git a/internal/registry/devin_models.go b/internal/registry/devin_models.go index 063c59960..59af4aefc 100644 --- a/internal/registry/devin_models.go +++ b/internal/registry/devin_models.go @@ -5,6 +5,8 @@ import ( _ "embed" "encoding/json" "fmt" + "slices" + "sort" "strings" "sync" @@ -108,6 +110,17 @@ func LookupDevinModel(modelID string) *ModelInfo { return cloneModelInfo(m) } } + + // Fallback: If clean is a thinking variant (e.g. claude-opus-5-low-fast or gpt-6-astra-high), + // look up its base model (e.g. claude-opus-5 or gpt-6-astra). + if base, _ := splitDevinModelID(clean); base != clean && base != "" { + for _, m := range models { + mClean := strings.ToLower(strings.TrimPrefix(m.ID, "devin/")) + if mClean == base { + return cloneModelInfo(m) + } + } + } return nil } @@ -181,30 +194,245 @@ func ValidateDevinModelsJSON(data []byte) ([]*ModelInfo, error) { return nil, fmt.Errorf("invalid Devin models JSON: expected non-empty 'devin'/'models' array or model list") } -func sanitizeAndValidateDevinModels(models []*ModelInfo) ([]*ModelInfo, error) { - seen := make(map[string]struct{}, len(models)) - out := make([]*ModelInfo, 0, len(models)) +var devinCompoundSuffixes = []struct { + suffix string + effort string + readd string +}{ + {suffix: "-low-fast", effort: "low"}, + {suffix: "-medium-fast", effort: "medium"}, + {suffix: "-high-fast", effort: "high"}, + {suffix: "-xhigh-fast", effort: "xhigh"}, + {suffix: "-max-fast", effort: "max"}, + {suffix: "-none-fast", effort: "none"}, + {suffix: "-low-priority", effort: "low"}, + {suffix: "-medium-priority", effort: "medium"}, + {suffix: "-high-priority", effort: "high"}, + {suffix: "-xhigh-priority", effort: "xhigh"}, + {suffix: "-max-priority", effort: "max"}, + {suffix: "-none-priority", effort: "none"}, + {suffix: "-thinking-1m", effort: "", readd: "-1m"}, + {suffix: "-thinking", effort: ""}, + {suffix: "-max-1m", effort: "max", readd: "-1m"}, + {suffix: "-none-1m", effort: "none", readd: "-1m"}, +} - for i, m := range models { +var devinSimpleEffortSuffixes = []struct { + suffix string + effort string +}{ + {suffix: "-none", effort: "none"}, + {suffix: "-minimal", effort: "minimal"}, + {suffix: "-low", effort: "low"}, + {suffix: "-medium", effort: "medium"}, + {suffix: "-high", effort: "high"}, + {suffix: "-xhigh", effort: "xhigh"}, + {suffix: "-max", effort: "max"}, +} + +var devinDisplayNameSuffixes = []string{ + " Low Fast", " Medium Fast", " High Fast", " XHigh Fast", " Max Fast", + " Low Thinking Fast", " Medium Thinking Fast", " High Thinking Fast", + " XHigh Thinking Fast", " Max Thinking Fast", " No Thinking Fast", + " Low Thinking", " Medium Thinking", " High Thinking", " XHigh Thinking", + " Max Thinking", " No Thinking", + " Low", " Medium", " High", " XHigh", " Max", " None", " Minimal", + " Thinking", " Fast", +} + +var devinLevelOrder = map[string]int{ + "none": 0, + "minimal": 1, + "low": 2, + "medium": 3, + "high": 4, + "xhigh": 5, + "max": 6, + "fast": 7, + "priority": 8, +} + +func splitDevinModelID(cleanID string) (string, string) { + if cleanID == "swe-1-6-slow" { + return cleanID, "" + } + if cleanID == "swe-1-6-fast" { + return "swe-1-6", "" + } + + upper := strings.ToUpper(cleanID) + for _, s := range []struct { + suffix string + effort string + }{ + {"_NONE", "none"}, + {"_MINIMAL", "minimal"}, + {"_LOW", "low"}, + {"_MEDIUM", "medium"}, + {"_HIGH", "high"}, + {"_XHIGH", "xhigh"}, + {"_MAX", "max"}, + {"_THINKING", "high"}, + } { + if strings.HasSuffix(upper, s.suffix) { + base := cleanID[:len(cleanID)-len(s.suffix)] + return base, s.effort + } + } + + for _, s := range devinCompoundSuffixes { + if strings.HasSuffix(cleanID, s.suffix) { + base := cleanID[:len(cleanID)-len(s.suffix)] + if s.readd != "" { + base += s.readd + } + return base, s.effort + } + } + + for _, s := range devinSimpleEffortSuffixes { + if strings.HasSuffix(cleanID, s.suffix) { + base := cleanID[:len(cleanID)-len(s.suffix)] + return base, s.effort + } + } + + return cleanID, "" +} + +func cleanDevinDisplayName(name string) string { + trimmed := strings.TrimSpace(name) + for { + changed := false + for _, s := range devinDisplayNameSuffixes { + if strings.HasSuffix(strings.ToLower(trimmed), strings.ToLower(s)) { + trimmed = strings.TrimSpace(trimmed[:len(trimmed)-len(s)]) + changed = true + break + } + } + if !changed { + break + } + } + return trimmed +} + +func aggregateDevinModels(models []*ModelInfo) []*ModelInfo { + type aggEntry struct { + model *ModelInfo + levels map[string]struct{} + } + + order := make([]string, 0, len(models)) + aggregated := make(map[string]*aggEntry, len(models)) + + for _, m := range models { if m == nil { - return nil, fmt.Errorf("model at index %d is null", i) + continue } - id := strings.TrimSpace(m.ID) - if id == "" { - return nil, fmt.Errorf("model at index %d has empty id", i) + cleanID := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(m.ID), "devin/")) + baseID, effort := splitDevinModelID(cleanID) + if baseID == "" { + baseID = cleanID } - // Automatically namespace model IDs under devin/ if not already prefixed - if !strings.HasPrefix(strings.ToLower(id), "devin/") { - id = "devin/" + id - } - id = strings.ToLower(id) - m.ID = id - if _, exists := seen[id]; exists { - return nil, fmt.Errorf("duplicate model id: %q", id) - } - seen[id] = struct{}{} + namespacedBase := "devin/" + baseID + isBase := (baseID == cleanID) - // Ensure proper default fields + entry, exists := aggregated[namespacedBase] + if !exists { + clone := *m + clone.ID = namespacedBase + clone.DisplayName = cleanDevinDisplayName(m.DisplayName) + if clone.DisplayName == "" { + clone.DisplayName = m.DisplayName + } + entry = &aggEntry{ + model: &clone, + levels: make(map[string]struct{}), + } + aggregated[namespacedBase] = entry + order = append(order, namespacedBase) + } + + if isBase { + if m.DisplayName != "" { + entry.model.DisplayName = cleanDevinDisplayName(m.DisplayName) + } + if m.OwnedBy != "" { + entry.model.OwnedBy = m.OwnedBy + } + } + if m.ContextLength > entry.model.ContextLength { + entry.model.ContextLength = m.ContextLength + } + if m.MaxCompletionTokens > entry.model.MaxCompletionTokens { + entry.model.MaxCompletionTokens = m.MaxCompletionTokens + } + if m.InputTokenLimit > entry.model.InputTokenLimit { + entry.model.InputTokenLimit = m.InputTokenLimit + } + if m.OutputTokenLimit > entry.model.OutputTokenLimit { + entry.model.OutputTokenLimit = m.OutputTokenLimit + } + for _, mod := range m.SupportedInputModalities { + if !slices.Contains(entry.model.SupportedInputModalities, mod) { + entry.model.SupportedInputModalities = append(entry.model.SupportedInputModalities, mod) + } + } + for _, mod := range m.SupportedOutputModalities { + if !slices.Contains(entry.model.SupportedOutputModalities, mod) { + entry.model.SupportedOutputModalities = append(entry.model.SupportedOutputModalities, mod) + } + } + for _, gen := range m.SupportedGenerationMethods { + if !slices.Contains(entry.model.SupportedGenerationMethods, gen) { + entry.model.SupportedGenerationMethods = append(entry.model.SupportedGenerationMethods, gen) + } + } + + if m.Thinking != nil && len(m.Thinking.Levels) > 0 { + for _, l := range m.Thinking.Levels { + if l != "" && l != "priority" { + entry.levels[l] = struct{}{} + } + } + } + if effort != "" && effort != "priority" { + entry.levels[effort] = struct{}{} + } + } + + out := make([]*ModelInfo, 0, len(order)) + for _, id := range order { + entry := aggregated[id] + m := entry.model + + if len(entry.levels) > 0 { + lvls := make([]string, 0, len(entry.levels)) + for l := range entry.levels { + lvls = append(lvls, l) + } + sort.Slice(lvls, func(i, j int) bool { + rI, okI := devinLevelOrder[lvls[i]] + if !okI { + rI = 99 + } + rJ, okJ := devinLevelOrder[lvls[j]] + if !okJ { + rJ = 99 + } + if rI != rJ { + return rI < rJ + } + return lvls[i] < lvls[j] + }) + m.Thinking = &ThinkingSupport{ + Levels: lvls, + } + } + + // Ensure defaults if m.Type == "" { m.Type = "devin" } @@ -226,8 +454,35 @@ func sanitizeAndValidateDevinModels(models []*ModelInfo) ([]*ModelInfo, error) { if len(m.SupportedGenerationMethods) == 0 { m.SupportedGenerationMethods = []string{"generateContent", "countTokens"} } + out = append(out, m) } - return out, nil + return out +} + +func sanitizeAndValidateDevinModels(models []*ModelInfo) ([]*ModelInfo, error) { + seenExact := make(map[string]struct{}, len(models)) + + for i, m := range models { + if m == nil { + return nil, fmt.Errorf("model at index %d is null", i) + } + id := strings.TrimSpace(m.ID) + if id == "" { + return nil, fmt.Errorf("model at index %d has empty id", i) + } + // Automatically namespace model IDs under devin/ if not already prefixed + if !strings.HasPrefix(strings.ToLower(id), "devin/") { + id = "devin/" + id + } + id = strings.ToLower(id) + m.ID = id + if _, exists := seenExact[id]; exists { + return nil, fmt.Errorf("duplicate model id: %q", id) + } + seenExact[id] = struct{}{} + } + + return aggregateDevinModels(models), nil } diff --git a/internal/registry/devin_models_test.go b/internal/registry/devin_models_test.go index b228822ea..cda60dafa 100644 --- a/internal/registry/devin_models_test.go +++ b/internal/registry/devin_models_test.go @@ -4,6 +4,8 @@ import ( "context" "net/http" "net/http/httptest" + "reflect" + "strings" "testing" "time" ) @@ -351,3 +353,66 @@ func TestWithDevinBuiltins(t *testing.T) { t.Errorf("expected DisplayName 'SWE-1.6 Slow', got %q", replaced[0].DisplayName) } } + +func TestEmbeddedDevinModels_NoThinkingSuffixes_Issue6141(t *testing.T) { + models := GetDevinModels() + disallowedSuffixes := []string{ + "-low-fast", "-medium-fast", "-high-fast", "-xhigh-fast", "-max-fast", + "-none-fast", "-low", "-medium", "-high", "-xhigh", "-max", "-none", + "-thinking-1m", "-thinking", "_none", "_minimal", "_low", "_medium", + "_high", "_xhigh", "_max", "_thinking", + } + var exposedThinkingVariants []string + for _, m := range models { + cleanID := strings.TrimPrefix(m.ID, "devin/") + for _, s := range disallowedSuffixes { + if strings.HasSuffix(cleanID, s) { + exposedThinkingVariants = append(exposedThinkingVariants, m.ID) + break + } + } + } + if len(exposedThinkingVariants) > 0 { + t.Fatalf("Devin catalog exposes thinking-parameter model variants (count=%d): %v", len(exposedThinkingVariants), exposedThinkingVariants) + } +} + +func TestValidateDevinModelsJSON_AggregatesThinkingVariants_Issue6141(t *testing.T) { + payload := []byte(`{ + "devin": [ + { + "id": "claude-opus-5", + "display_name": "Claude Opus 5", + "owned_by": "anthropic", + "thinking": {"levels": ["medium"]} + }, + { + "id": "claude-opus-5-low-fast", + "display_name": "Claude Opus 5 Low Fast", + "owned_by": "anthropic" + }, + { + "id": "claude-opus-5-high-fast", + "display_name": "Claude Opus 5 High Fast", + "owned_by": "anthropic" + } + ] + }`) + models, err := ValidateDevinModelsJSON(payload) + if err != nil { + t.Fatalf("ValidateDevinModelsJSON failed: %v", err) + } + if len(models) != 1 { + t.Fatalf("expected 1 aggregated model, got %d: %+v", len(models), models) + } + if models[0].ID != "devin/claude-opus-5" { + t.Errorf("expected ID devin/claude-opus-5, got %q", models[0].ID) + } + if models[0].Thinking == nil { + t.Fatalf("expected Thinking to be populated") + } + expectedLevels := []string{"low", "medium", "high"} + if !reflect.DeepEqual(models[0].Thinking.Levels, expectedLevels) { + t.Fatalf("expected thinking levels %v, got %v", expectedLevels, models[0].Thinking.Levels) + } +} diff --git a/internal/registry/models/devin_models.json b/internal/registry/models/devin_models.json index d7980dfa7..6f1e69ae1 100644 --- a/internal/registry/models/devin_models.json +++ b/internal/registry/models/devin_models.json @@ -103,7 +103,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-6 Astra Medium Thinking", + "display_name": "GPT-6 Astra", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -128,7 +128,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-6 Sol Medium Thinking", + "display_name": "GPT-6 Sol", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -154,7 +154,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-6 Luna Medium Thinking", + "display_name": "GPT-6 Luna", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -335,86 +335,6 @@ ] } }, - { - "id": "claude-opus-4-8-low-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.8 Low Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-4-8-medium-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.8 Medium Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-4-8-high-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.8 High Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-4-8-xhigh-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.8 XHigh Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-4-8-max-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.8 Max Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "claude-opus-5", "object": "model", @@ -440,166 +360,6 @@ ] } }, - { - "id": "claude-opus-5-low-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5 Low Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-medium-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5 Medium Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-high-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5 High Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-xhigh-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5 XHigh Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-max-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5 Max Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-5-low-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5.5 Low Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-5-medium-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5.5 Medium Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-5-high-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5.5 High Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-5-xhigh-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5.5 XHigh Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "claude-opus-5-5-max-fast", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 5.5 Max Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "claude-5-fable", "object": "model", @@ -701,7 +461,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.6 Sol No Thinking", + "display_name": "GPT-5.6 Sol", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -722,138 +482,12 @@ ] } }, - { - "id": "gpt-5-6-sol-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol No Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-sol-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-sol-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-sol-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-sol-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-sol-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Sol Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, { "id": "gpt-5-6-terra", "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.6 Terra No Thinking", + "display_name": "GPT-5.6 Terra", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -874,138 +508,12 @@ ] } }, - { - "id": "gpt-5-6-terra-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra No Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-terra-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-terra-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-terra-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-terra-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-terra-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Terra Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, { "id": "gpt-5-6-luna", "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.6 Luna No Thinking", + "display_name": "GPT-5.6 Luna", "context_length": 1000000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -1026,489 +534,6 @@ ] } }, - { - "id": "gpt-5-6-luna-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna No Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-luna-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-luna-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-luna-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-luna-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-6-luna-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.6 Luna Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-astra-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Astra Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-astra-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Astra Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-astra-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Astra High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-astra-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Astra XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-astra-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Astra Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol No Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-sol-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Sol Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna No Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna Low Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna Medium Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna High Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna XHigh Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-6-luna-max", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-6 Luna Max Thinking Fast", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, { "id": "glm-5-2-1m", "object": "model", @@ -1522,37 +547,13 @@ ], "supportedOutputModalities": [ "text" - ] - }, - { - "id": "glm-5-2-max-1m", - "object": "model", - "type": "devin", - "owned_by": "zhipu", - "display_name": "GLM-5.2 Max 1M", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text" ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "glm-5-2-none-1m", - "object": "model", - "type": "devin", - "owned_by": "zhipu", - "display_name": "GLM-5.2 No Thinking 1M", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text" - ], - "supportedOutputModalities": [ - "text" - ] + "thinking": { + "levels": [ + "none", + "max" + ] + } }, { "id": "grok-4-5", @@ -1753,22 +754,6 @@ "text" ] }, - { - "id": "claude-opus-4-6-thinking", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.6 Thinking", - "context_length": 200000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "claude-opus-4-6-1m", "object": "model", @@ -1785,28 +770,12 @@ "text" ] }, - { - "id": "claude-opus-4-6-thinking-1m", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.6 Thinking 1M", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "gpt-5-4", "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.4 No Thinking", + "display_name": "GPT-5.4", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -1831,7 +800,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.5 No Thinking", + "display_name": "GPT-5.5", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -1851,222 +820,12 @@ ] } }, - { - "id": "gpt-5-5-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.5 No Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-5-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.5 Low Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-5-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.5 Medium Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-5-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.5 High Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-5-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.5 XHigh Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-4-none", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.4 No Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-4-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.4 Low Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-4-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.4 Medium Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-4-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.4 High Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-4-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.4 XHigh Thinking Fast", - "context_length": 272000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, { "id": "gpt-5-4-mini", "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.4 Mini Low Thinking", + "display_name": "GPT-5.4 Mini", "context_length": 400000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2101,22 +860,6 @@ "text" ] }, - { - "id": "claude-sonnet-4-6-thinking", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Sonnet 4.6 Thinking", - "context_length": 200000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "claude-sonnet-4-6-1m", "object": "model", @@ -2134,27 +877,11 @@ ] }, { - "id": "claude-sonnet-4-6-thinking-1m", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Sonnet 4.6 Thinking 1M", - "context_length": 1000000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "MODEL_GPT_5_2_LOW", + "id": "MODEL_GPT_5_2", "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.2 Low Thinking", + "display_name": "GPT-5.2", "context_length": 384000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2163,23 +890,16 @@ ], "supportedOutputModalities": [ "text" - ] - }, - { - "id": "MODEL_GPT_5_2_MEDIUM", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.2 Medium Thinking", - "context_length": 384000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" ], - "supportedOutputModalities": [ - "text" - ] + "thinking": { + "levels": [ + "none", + "low", + "medium", + "high", + "xhigh" + ] + } }, { "id": "MODEL_CLAUDE_4_5_OPUS", @@ -2195,23 +915,12 @@ ], "supportedOutputModalities": [ "text" - ] - }, - { - "id": "MODEL_CLAUDE_4_5_OPUS_THINKING", - "object": "model", - "type": "devin", - "owned_by": "anthropic", - "display_name": "Claude Opus 4.5 Thinking", - "context_length": 200000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" ], - "supportedOutputModalities": [ - "text" - ] + "thinking": { + "levels": [ + "high" + ] + } }, { "id": "MODEL_PRIVATE_11", @@ -2250,7 +959,7 @@ "object": "model", "type": "devin", "owned_by": "anthropic", - "display_name": "Claude Sonnet 4.5 Thinking", + "display_name": "Claude Sonnet 4.5", "context_length": 200000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2282,7 +991,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.1 No Thinking", + "display_name": "GPT-5.1", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2298,7 +1007,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.1 Low Thinking", + "display_name": "GPT-5.1", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2314,7 +1023,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.1 Medium Thinking", + "display_name": "GPT-5.1", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2330,7 +1039,7 @@ "object": "model", "type": "devin", "owned_by": "openai", - "display_name": "GPT-5.1 High Thinking", + "display_name": "GPT-5.1", "context_length": 272000, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2341,54 +1050,6 @@ "text" ] }, - { - "id": "MODEL_GPT_5_2_NONE", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.2 No Thinking", - "context_length": 384000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "MODEL_GPT_5_2_HIGH", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.2 High Thinking", - "context_length": 384000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "MODEL_GPT_5_2_XHIGH", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.2 XHigh Thinking", - "context_length": 384000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "gpt-5-3-codex", "object": "model", @@ -2413,90 +1074,6 @@ ] } }, - { - "id": "gpt-5-3-codex-low", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.3-Codex Low Fast", - "context_length": 400000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-3-codex-medium", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.3-Codex Medium Fast", - "context_length": 400000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-3-codex-high", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.3-Codex High Fast", - "context_length": 400000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, - { - "id": "gpt-5-3-codex-xhigh", - "object": "model", - "type": "devin", - "owned_by": "openai", - "display_name": "GPT-5.3-Codex XHigh Fast", - "context_length": 400000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ], - "thinking": { - "levels": [ - "priority" - ] - } - }, { "id": "kimi-k2-6", "object": "model", @@ -2567,28 +1144,12 @@ "text" ] }, - { - "id": "swe-1-6-fast", - "object": "model", - "type": "devin", - "owned_by": "cognition", - "display_name": "SWE-1.6 Fast", - "context_length": 200000, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, { "id": "gemini-3-1-pro", "object": "model", "type": "devin", "owned_by": "google", - "display_name": "Gemini 3.1 Pro Low Thinking", + "display_name": "Gemini 3.1 Pro", "context_length": 1048576, "max_completion_tokens": 64000, "supportedInputModalities": [ @@ -2606,7 +1167,7 @@ } }, { - "id": "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL", + "id": "MODEL_GOOGLE_GEMINI_3_0_FLASH", "object": "model", "type": "devin", "owned_by": "google", @@ -2619,55 +1180,15 @@ ], "supportedOutputModalities": [ "text" - ] - }, - { - "id": "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW", - "object": "model", - "type": "devin", - "owned_by": "google", - "display_name": "Gemini 3 Flash", - "context_length": 1048576, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM", - "object": "model", - "type": "devin", - "owned_by": "google", - "display_name": "Gemini 3 Flash", - "context_length": 1048576, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] - }, - { - "id": "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH", - "object": "model", - "type": "devin", - "owned_by": "google", - "display_name": "Gemini 3 Flash", - "context_length": 1048576, - "max_completion_tokens": 64000, - "supportedInputModalities": [ - "text", - "image" - ], - "supportedOutputModalities": [ - "text" - ] + "thinking": { + "levels": [ + "minimal", + "low", + "medium", + "high" + ] + } }, { "id": "deepseek-v4-pro", @@ -2691,4 +1212,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/runtime/executor/helps/devin_models.go b/internal/runtime/executor/helps/devin_models.go index 4e9e38d96..2dcc72dbf 100644 --- a/internal/runtime/executor/helps/devin_models.go +++ b/internal/runtime/executor/helps/devin_models.go @@ -23,6 +23,24 @@ var knownDevinSuffixes = []string{ "-high-priority", "-xhigh-priority", "-max-priority", + "-low-fast", + "-medium-fast", + "-high-fast", + "-xhigh-fast", + "-max-fast", + "-none-fast", + "-thinking-1m", + "-thinking", + "-max-1m", + "-none-1m", + "_none", + "_minimal", + "_low", + "_medium", + "_high", + "_xhigh", + "_max", + "_thinking", } // Special private Devin upstream aliases that cannot be dynamically inferred. @@ -120,6 +138,23 @@ func ResolveDevinChatModelUID(rawModel string, thinkingLevel string, budgetToken canonicalBase = "gemini-3-8-flash" } + normalizedUnder := strings.ReplaceAll(canonicalBase, "-", "_") + switch normalizedUnder { + case "model_gpt_5_2": + gptLevels := []string{"none", "low", "medium", "high", "xhigh"} + eff := clampEffort(effort, gptLevels, "low") + return "MODEL_GPT_5_2_" + strings.ToUpper(eff) + case "model_google_gemini_3_0_flash": + gemLevels := []string{"minimal", "low", "medium", "high"} + eff := clampEffort(effort, gemLevels, "high") + return "MODEL_GOOGLE_GEMINI_3_0_FLASH_" + strings.ToUpper(eff) + case "model_claude_4_5_opus": + if effort != "" && effort != "none" { + return "MODEL_CLAUDE_4_5_OPUS_THINKING" + } + return "MODEL_CLAUDE_4_5_OPUS" + } + // 6. Look up dynamic model metadata from the Devin catalog (devin_models.json) modelInfo := registry.LookupDevinModel(canonicalBase) if modelInfo == nil && canonicalBase != lowerBase { @@ -151,6 +186,29 @@ func ResolveDevinChatModelUID(rawModel string, thinkingLevel string, budgetToken return "glm-5-2-max" } return "glm-5-2" + case "glm-5-2-1m": + if effort == "none" { + return "glm-5-2-none-1m" + } + if effort == "max" { + return "glm-5-2-max-1m" + } + return "glm-5-2-1m" + case "claude-opus-4-6", "claude-sonnet-4-6": + if effort != "" && effort != "none" { + return canonicalBase + "-thinking" + } + return canonicalBase + case "claude-opus-4-6-1m": + if effort != "" && effort != "none" { + return "claude-opus-4-6-thinking-1m" + } + return canonicalBase + case "claude-sonnet-4-6-1m": + if effort != "" && effort != "none" { + return "claude-sonnet-4-6-thinking-1m" + } + return canonicalBase } // 8. If model has no thinking levels defined in catalog, treat as bare model diff --git a/internal/runtime/executor/helps/devin_models_test.go b/internal/runtime/executor/helps/devin_models_test.go index d8c348183..e76323060 100644 --- a/internal/runtime/executor/helps/devin_models_test.go +++ b/internal/runtime/executor/helps/devin_models_test.go @@ -439,6 +439,132 @@ func TestResolveDevinChatModelUID(t *testing.T) { thinkingLevel: "", want: "claude-sonnet-4-6", }, + { + name: "claude-opus-4-6 with high maps to claude-opus-4-6-thinking", + rawModel: "devin/claude-opus-4-6", + thinkingLevel: "high", + want: "claude-opus-4-6-thinking", + }, + { + name: "claude-sonnet-4-6 with high maps to claude-sonnet-4-6-thinking", + rawModel: "devin/claude-sonnet-4-6", + thinkingLevel: "high", + want: "claude-sonnet-4-6-thinking", + }, + { + name: "claude-opus-4-6-1m default maps to claude-opus-4-6-1m", + rawModel: "devin/claude-opus-4-6-1m", + thinkingLevel: "", + want: "claude-opus-4-6-1m", + }, + { + name: "claude-opus-4-6-1m with high maps to claude-opus-4-6-thinking-1m", + rawModel: "devin/claude-opus-4-6-1m", + thinkingLevel: "high", + want: "claude-opus-4-6-thinking-1m", + }, + { + name: "claude-sonnet-4-6-1m default maps to claude-sonnet-4-6-1m", + rawModel: "devin/claude-sonnet-4-6-1m", + thinkingLevel: "", + want: "claude-sonnet-4-6-1m", + }, + { + name: "claude-sonnet-4-6-1m with high maps to claude-sonnet-4-6-thinking-1m", + rawModel: "devin/claude-sonnet-4-6-1m", + thinkingLevel: "high", + want: "claude-sonnet-4-6-thinking-1m", + }, + { + name: "glm-5-2-1m default maps to glm-5-2-1m", + rawModel: "devin/glm-5-2-1m", + thinkingLevel: "", + want: "glm-5-2-1m", + }, + { + name: "glm-5-2-1m with none maps to glm-5-2-none-1m", + rawModel: "devin/glm-5-2-1m", + thinkingLevel: "none", + want: "glm-5-2-none-1m", + }, + { + name: "glm-5-2-1m with max maps to glm-5-2-max-1m", + rawModel: "devin/glm-5-2-1m", + thinkingLevel: "max", + want: "glm-5-2-max-1m", + }, + { + name: "MODEL_GPT_5_2 default maps to MODEL_GPT_5_2_LOW", + rawModel: "devin/MODEL_GPT_5_2", + thinkingLevel: "", + want: "MODEL_GPT_5_2_LOW", + }, + { + name: "MODEL_GPT_5_2 with none maps to MODEL_GPT_5_2_NONE", + rawModel: "devin/MODEL_GPT_5_2", + thinkingLevel: "none", + want: "MODEL_GPT_5_2_NONE", + }, + { + name: "MODEL_GPT_5_2 with medium maps to MODEL_GPT_5_2_MEDIUM", + rawModel: "devin/MODEL_GPT_5_2", + thinkingLevel: "medium", + want: "MODEL_GPT_5_2_MEDIUM", + }, + { + name: "MODEL_GPT_5_2 with high maps to MODEL_GPT_5_2_HIGH", + rawModel: "devin/MODEL_GPT_5_2", + thinkingLevel: "high", + want: "MODEL_GPT_5_2_HIGH", + }, + { + name: "MODEL_GPT_5_2 with xhigh maps to MODEL_GPT_5_2_XHIGH", + rawModel: "devin/MODEL_GPT_5_2", + thinkingLevel: "xhigh", + want: "MODEL_GPT_5_2_XHIGH", + }, + { + name: "MODEL_GOOGLE_GEMINI_3_0_FLASH default maps to HIGH", + rawModel: "devin/MODEL_GOOGLE_GEMINI_3_0_FLASH", + thinkingLevel: "", + want: "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH", + }, + { + name: "MODEL_GOOGLE_GEMINI_3_0_FLASH with minimal maps to MINIMAL", + rawModel: "devin/MODEL_GOOGLE_GEMINI_3_0_FLASH", + thinkingLevel: "minimal", + want: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL", + }, + { + name: "MODEL_GOOGLE_GEMINI_3_0_FLASH with low maps to LOW", + rawModel: "devin/MODEL_GOOGLE_GEMINI_3_0_FLASH", + thinkingLevel: "low", + want: "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW", + }, + { + name: "MODEL_GOOGLE_GEMINI_3_0_FLASH with medium maps to MEDIUM", + rawModel: "devin/MODEL_GOOGLE_GEMINI_3_0_FLASH", + thinkingLevel: "medium", + want: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM", + }, + { + name: "MODEL_GOOGLE_GEMINI_3_0_FLASH with high maps to HIGH", + rawModel: "devin/MODEL_GOOGLE_GEMINI_3_0_FLASH", + thinkingLevel: "high", + want: "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH", + }, + { + name: "MODEL_CLAUDE_4_5_OPUS default maps to MODEL_CLAUDE_4_5_OPUS", + rawModel: "devin/MODEL_CLAUDE_4_5_OPUS", + thinkingLevel: "", + want: "MODEL_CLAUDE_4_5_OPUS", + }, + { + name: "MODEL_CLAUDE_4_5_OPUS with high maps to MODEL_CLAUDE_4_5_OPUS_THINKING", + rawModel: "devin/MODEL_CLAUDE_4_5_OPUS", + thinkingLevel: "high", + want: "MODEL_CLAUDE_4_5_OPUS_THINKING", + }, } for _, tt := range tests { @@ -467,7 +593,7 @@ func TestResolveDevinChatModelUID_AllCatalogModels(t *testing.T) { } // If model defines thinking levels, resolved UID must have an effort suffix if m.Thinking != nil && len(m.Thinking.Levels) > 0 { - if !HasDevinEffortSuffix(resolved) && baseID != "swe-1-7" && baseID != "glm-5-2" && baseID != "swe-1-6-slow" { + if !HasDevinEffortSuffix(resolved) && baseID != "swe-1-7" && baseID != "glm-5-2" && baseID != "glm-5-2-1m" && baseID != "swe-1-6-slow" && baseID != "model_claude_4_5_opus" { t.Errorf("thinking model %q with effort %q resolved to bare UID %q without effort suffix", baseID, eff, resolved) } }