package thinking import ( "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/tidwall/gjson" ) // hasThinkTrigger reports whether user prompt text contains the standalone word "think". // // It scans common prompt-bearing keys (system/content/text/instructions/prompt) // and avoids triggering on unrelated fields such as model names. func hasThinkTrigger(body []byte) bool { if len(body) == 0 || !gjson.ValidBytes(body) { return false } root := gjson.ParseBytes(body) return scanThinkInNode(root, "") } func scanThinkInNode(node gjson.Result, key string) bool { lk := strings.ToLower(strings.TrimSpace(key)) switch node.Type { case gjson.String: if isPromptKey(lk) { return containsThinkWord(node.String()) } return false case gjson.JSON: if node.IsArray() { for _, item := range node.Array() { if scanThinkInNode(item, key) { return true } } return false } if node.IsObject() { found := false node.ForEach(func(k, v gjson.Result) bool { if scanThinkInNode(v, k.String()) { found = true return false } return true }) return found } } return false } func isPromptKey(key string) bool { switch key { case "system", "content", "text", "instructions", "prompt": return true default: return false } } func containsThinkWord(text string) bool { if text == "" { return false } lower := strings.ToLower(text) needle := "think" start := 0 for { idx := strings.Index(lower[start:], needle) if idx < 0 { return false } idx += start leftOK := idx == 0 || !isWordChar(rune(lower[idx-1])) rightPos := idx + len(needle) rightOK := rightPos >= len(lower) || !isWordChar(rune(lower[rightPos])) if leftOK && rightOK { return true } start = idx + len(needle) if start >= len(lower) { return false } } } func isWordChar(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' } func defaultThinkConfig(modelInfo *registry.ModelInfo) ThinkingConfig { if modelInfo == nil || modelInfo.Thinking == nil { return ThinkingConfig{} } support := modelInfo.Thinking if support.DynamicAllowed { return ThinkingConfig{Mode: ModeAuto, Budget: -1} } if len(support.Levels) > 0 { for _, level := range support.Levels { l := strings.ToLower(strings.TrimSpace(level)) if l == string(LevelHigh) { return ThinkingConfig{Mode: ModeLevel, Level: LevelHigh} } } for _, level := range support.Levels { l := strings.ToLower(strings.TrimSpace(level)) switch l { case string(LevelXHigh): return ThinkingConfig{Mode: ModeLevel, Level: LevelXHigh} case string(LevelMedium): return ThinkingConfig{Mode: ModeLevel, Level: LevelMedium} case string(LevelLow): return ThinkingConfig{Mode: ModeLevel, Level: LevelLow} case string(LevelMinimal): return ThinkingConfig{Mode: ModeLevel, Level: LevelMinimal} case string(LevelNone): return ThinkingConfig{Mode: ModeNone, Budget: 0} } } } if support.Max > 0 { return ThinkingConfig{Mode: ModeBudget, Budget: support.Max} } if support.ZeroAllowed { return ThinkingConfig{Mode: ModeNone, Budget: 0} } if support.Min > 0 { return ThinkingConfig{Mode: ModeBudget, Budget: support.Min} } return ThinkingConfig{} }