266 lines · 6.9 KB · Go
RawDownload
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}