mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-08 00:33:35 +08:00
267 lines
6.9 KiB
Go
267 lines
6.9 KiB
Go
package helps
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
|
)
|
|
|
|
// knownDevinSuffixes lists recognized model uid suffixes.
|
|
var knownDevinSuffixes = []string{
|
|
"-none",
|
|
"-low",
|
|
"-medium",
|
|
"-high",
|
|
"-xhigh",
|
|
"-max",
|
|
"-fast",
|
|
"-priority",
|
|
"-low-priority",
|
|
"-medium-priority",
|
|
"-high-priority",
|
|
"-xhigh-priority",
|
|
"-max-priority",
|
|
}
|
|
|
|
// Special private Devin upstream aliases that cannot be dynamically inferred.
|
|
var specialDevinAliases = map[string]string{
|
|
"claude-haiku-4-5": "MODEL_PRIVATE_11",
|
|
"gpt-4-1": "MODEL_CHAT_GPT_4_1_2025_04_14",
|
|
}
|
|
|
|
// HasDevinEffortSuffix reports whether the model name already ends with a known Devin effort suffix.
|
|
func HasDevinEffortSuffix(model string) bool {
|
|
lower := strings.ToLower(strings.TrimSpace(model))
|
|
for _, s := range knownDevinSuffixes {
|
|
if strings.HasSuffix(lower, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NormalizeThinkingLevel converts numeric budgets or loose effort strings to canonical Devin efforts.
|
|
func NormalizeThinkingLevel(level string, budgetTokens int) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(level))
|
|
switch normalized {
|
|
case "minimal", "low", "medium", "high", "xhigh", "max", "fast":
|
|
return normalized
|
|
case "none", "off", "disabled":
|
|
return "none"
|
|
case "auto", "adaptive":
|
|
return "high"
|
|
}
|
|
|
|
if budgetTokens > 0 {
|
|
switch {
|
|
case budgetTokens <= 4096:
|
|
return "low"
|
|
case budgetTokens <= 16384:
|
|
return "medium"
|
|
case budgetTokens <= 32768:
|
|
return "high"
|
|
default:
|
|
return "max"
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// ResolveDevinChatModelUID resolves a model identifier into a valid upstream Devin chat_model_uid.
|
|
// It prioritizes dynamic catalog metadata from devin_models.json, automatically clamps
|
|
// requested efforts to supported levels, applies sensible default efforts for thinking models,
|
|
// and ensures bare non-thinking models remain bare.
|
|
func ResolveDevinChatModelUID(rawModel string, thinkingLevel string, budgetTokens int) string {
|
|
model := strings.TrimSpace(rawModel)
|
|
if model == "" {
|
|
return "swe-2-high"
|
|
}
|
|
|
|
// 1. Strip devin/ prefix if present (case-insensitive)
|
|
cleanModel := model
|
|
if strings.HasPrefix(strings.ToLower(cleanModel), "devin/") {
|
|
cleanModel = cleanModel[6:]
|
|
}
|
|
|
|
// 2. If already ends with an exact Devin effort suffix, use directly
|
|
if HasDevinEffortSuffix(cleanModel) {
|
|
return cleanModel
|
|
}
|
|
|
|
// 3. Strip CPA colon or parenthesis suffix (suffix overrides body per CPA convention)
|
|
parsedSuffix := thinking.ParseSuffix(cleanModel)
|
|
baseModel := strings.TrimSpace(parsedSuffix.ModelName)
|
|
if parsedSuffix.HasSuffix {
|
|
thinkingLevel = parsedSuffix.RawSuffix
|
|
} else if colonIdx := strings.LastIndex(cleanModel, ":"); colonIdx != -1 {
|
|
baseModel = strings.TrimSpace(cleanModel[:colonIdx])
|
|
thinkingLevel = strings.TrimSpace(cleanModel[colonIdx+1:])
|
|
}
|
|
|
|
// 4. Normalize requested effort
|
|
effort := NormalizeThinkingLevel(thinkingLevel, budgetTokens)
|
|
lowerBase := strings.ToLower(baseModel)
|
|
canonicalBase := strings.ReplaceAll(lowerBase, ".", "-")
|
|
|
|
// 5. Check special private upstream aliases
|
|
if alias, exists := specialDevinAliases[canonicalBase]; exists {
|
|
return alias
|
|
}
|
|
if canonicalBase == "claude-sonnet-4-5" || strings.Contains(canonicalBase, "sonnet-4-5") {
|
|
if effort != "" && effort != "none" {
|
|
return "MODEL_PRIVATE_3"
|
|
}
|
|
return "MODEL_PRIVATE_2"
|
|
}
|
|
if canonicalBase == "gemini-3-flash" {
|
|
canonicalBase = "gemini-3-8-flash"
|
|
}
|
|
|
|
// 6. Look up dynamic model metadata from the Devin catalog (devin_models.json)
|
|
modelInfo := registry.LookupDevinModel(canonicalBase)
|
|
if modelInfo == nil && canonicalBase != lowerBase {
|
|
modelInfo = registry.LookupDevinModel(lowerBase)
|
|
}
|
|
|
|
var allowedLevels []string
|
|
if modelInfo != nil && modelInfo.Thinking != nil && len(modelInfo.Thinking.Levels) > 0 {
|
|
allowedLevels = modelInfo.Thinking.Levels
|
|
}
|
|
|
|
// 7. Special base models that default to bare name unless specific variant requested
|
|
switch canonicalBase {
|
|
case "swe-1-7":
|
|
if effort == "medium" {
|
|
return "swe-1-7-medium"
|
|
}
|
|
return "swe-1-7"
|
|
case "swe-1-6":
|
|
if effort == "fast" {
|
|
return "swe-1-6-fast"
|
|
}
|
|
return "swe-1-6"
|
|
case "glm-5-2":
|
|
if effort == "none" {
|
|
return "glm-5-2-none"
|
|
}
|
|
if effort == "max" {
|
|
return "glm-5-2-max"
|
|
}
|
|
return "glm-5-2"
|
|
}
|
|
|
|
// 8. If model has no thinking levels defined in catalog, treat as bare model
|
|
if len(allowedLevels) == 0 {
|
|
return canonicalBase
|
|
}
|
|
|
|
// 9. Model has thinking levels: determine default effort and clamp
|
|
defaultEffort := selectDefaultDevinEffort(canonicalBase, allowedLevels)
|
|
clamped := clampEffort(effort, allowedLevels, defaultEffort)
|
|
return canonicalBase + "-" + clamped
|
|
}
|
|
|
|
func selectDefaultDevinEffort(baseModel string, levels []string) string {
|
|
if strings.Contains(baseModel, "swe-2") {
|
|
return "high"
|
|
}
|
|
hasNone := false
|
|
hasLow := false
|
|
hasMedium := false
|
|
hasHigh := false
|
|
for _, l := range levels {
|
|
switch l {
|
|
case "none":
|
|
hasNone = true
|
|
case "low":
|
|
hasLow = true
|
|
case "medium":
|
|
hasMedium = true
|
|
case "high":
|
|
hasHigh = true
|
|
}
|
|
}
|
|
// For OpenAI GPT-5.x families in Devin CLI (which support "none" and "low"), default to low
|
|
if hasNone && hasLow && strings.HasPrefix(baseModel, "gpt-5") {
|
|
return "low"
|
|
}
|
|
// For models with high thinking (e.g. deepseek, gemini, grok, glm, kimi, nemotron), prefer high
|
|
if hasHigh && (strings.Contains(baseModel, "gemini") ||
|
|
strings.Contains(baseModel, "grok") ||
|
|
strings.Contains(baseModel, "glm") ||
|
|
strings.Contains(baseModel, "deepseek") ||
|
|
strings.Contains(baseModel, "kimi") ||
|
|
strings.Contains(baseModel, "nemotron")) {
|
|
return "high"
|
|
}
|
|
if hasMedium {
|
|
return "medium"
|
|
}
|
|
if hasHigh {
|
|
return "high"
|
|
}
|
|
if hasLow {
|
|
return "low"
|
|
}
|
|
return levels[0]
|
|
}
|
|
|
|
var devinStandardLevelOrder = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
|
|
|
|
func devinLevelIndex(level string) int {
|
|
lower := strings.ToLower(strings.TrimSpace(level))
|
|
for i, l := range devinStandardLevelOrder {
|
|
if l == lower {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func clampEffort(requested string, allowed []string, defaultEffort string) string {
|
|
if requested == "" {
|
|
return defaultEffort
|
|
}
|
|
reqLower := strings.ToLower(strings.TrimSpace(requested))
|
|
for _, a := range allowed {
|
|
if reqLower == strings.ToLower(strings.TrimSpace(a)) {
|
|
return a
|
|
}
|
|
}
|
|
if reqLower == "none" {
|
|
return defaultEffort
|
|
}
|
|
|
|
reqIdx := devinLevelIndex(reqLower)
|
|
if reqIdx == -1 {
|
|
return defaultEffort
|
|
}
|
|
|
|
bestMatch := defaultEffort
|
|
bestDist := 999
|
|
bestIdx := -1
|
|
for _, a := range allowed {
|
|
aIdx := devinLevelIndex(a)
|
|
if aIdx == -1 {
|
|
continue
|
|
}
|
|
dist := reqIdx - aIdx
|
|
if dist < 0 {
|
|
dist = -dist
|
|
}
|
|
if dist < bestDist {
|
|
bestDist = dist
|
|
bestMatch = a
|
|
bestIdx = aIdx
|
|
} else if dist == bestDist && aIdx > bestIdx {
|
|
// On tie, prefer the higher effort (e.g. medium -> high for glm-5-3; xhigh -> max for swe-2)
|
|
bestMatch = a
|
|
bestIdx = aIdx
|
|
}
|
|
}
|
|
|
|
return bestMatch
|
|
}
|