refactor: improve thinking logic

This commit is contained in:
hkfires
2026-01-14 08:32:02 +08:00
parent 5a7e5bd870
commit 0b06d637e7
76 changed files with 8712 additions and 1815 deletions

View File

@@ -8,6 +8,8 @@ import (
// ModelSupportsThinking reports whether the given model has Thinking capability
// according to the model registry metadata (provider-agnostic).
//
// Deprecated: Use thinking.ApplyThinking with modelInfo.Thinking check.
func ModelSupportsThinking(model string) bool {
if model == "" {
return false
@@ -32,6 +34,8 @@ func ModelSupportsThinking(model string) bool {
// If the model is unknown or has no Thinking metadata, returns the original budget.
// For dynamic (-1), returns -1 if DynamicAllowed; otherwise approximates mid-range
// or min (0 if zero is allowed and mid <= 0).
//
// Deprecated: Use thinking.ValidateConfig for budget normalization.
func NormalizeThinkingBudget(model string, budget int) int {
if budget == -1 { // dynamic
if found, minBudget, maxBudget, zeroAllowed, dynamicAllowed := thinkingRangeFromRegistry(model); found {
@@ -89,6 +93,8 @@ func thinkingRangeFromRegistry(model string) (found bool, min int, max int, zero
// GetModelThinkingLevels returns the discrete reasoning effort levels for the model.
// Returns nil if the model has no thinking support or no levels defined.
//
// Deprecated: Access modelInfo.Thinking.Levels directly.
func GetModelThinkingLevels(model string) []string {
if model == "" {
return nil
@@ -102,6 +108,8 @@ func GetModelThinkingLevels(model string) []string {
// ModelUsesThinkingLevels reports whether the model uses discrete reasoning
// effort levels instead of numeric budgets.
//
// Deprecated: Check len(modelInfo.Thinking.Levels) > 0.
func ModelUsesThinkingLevels(model string) bool {
levels := GetModelThinkingLevels(model)
return len(levels) > 0
@@ -109,6 +117,8 @@ func ModelUsesThinkingLevels(model string) bool {
// NormalizeReasoningEffortLevel validates and normalizes a reasoning effort
// level for the given model. Returns false when the level is not supported.
//
// Deprecated: Use thinking.ValidateConfig for level validation.
func NormalizeReasoningEffortLevel(model, effort string) (string, bool) {
levels := GetModelThinkingLevels(model)
if len(levels) == 0 {
@@ -125,6 +135,8 @@ func NormalizeReasoningEffortLevel(model, effort string) (string, bool) {
// IsOpenAICompatibilityModel reports whether the model is registered as an OpenAI-compatibility model.
// These models may not advertise Thinking metadata in the registry.
//
// Deprecated: Check modelInfo.Type == "openai-compatibility".
func IsOpenAICompatibilityModel(model string) bool {
if model == "" {
return false
@@ -149,6 +161,8 @@ func IsOpenAICompatibilityModel(model string) bool {
// - "xhigh" -> 32768
//
// Returns false when the effort level is empty or unsupported.
//
// Deprecated: Use thinking.ConvertLevelToBudget instead.
func ThinkingEffortToBudget(model, effort string) (int, bool) {
if effort == "" {
return 0, false
@@ -186,6 +200,8 @@ func ThinkingEffortToBudget(model, effort string) (int, bool) {
// - "high" -> 32768
//
// Returns false when the level is empty or unsupported.
//
// Deprecated: Use thinking.ConvertLevelToBudget instead.
func ThinkingLevelToBudget(level string) (int, bool) {
if level == "" {
return 0, false
@@ -217,6 +233,8 @@ func ThinkingLevelToBudget(level string) (int, bool) {
// - 24577.. -> highest supported level for the model (defaults to "xhigh")
//
// Returns false when the budget is unsupported (negative values other than -1).
//
// Deprecated: Use thinking.ConvertBudgetToLevel instead.
func ThinkingBudgetToEffort(model string, budget int) (string, bool) {
switch {
case budget == -1:

View File

@@ -0,0 +1,130 @@
package util
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestThinkingUtilDeprecationComments(t *testing.T) {
dir, err := thinkingSourceDir()
if err != nil {
t.Fatalf("resolve thinking source dir: %v", err)
}
// Test thinking.go deprecation comments
t.Run("thinking.go", func(t *testing.T) {
docs := parseFuncDocs(t, filepath.Join(dir, "thinking.go"))
tests := []struct {
funcName string
want string
}{
{"ModelSupportsThinking", "Deprecated: Use thinking.ApplyThinking with modelInfo.Thinking check."},
{"NormalizeThinkingBudget", "Deprecated: Use thinking.ValidateConfig for budget normalization."},
{"ThinkingEffortToBudget", "Deprecated: Use thinking.ConvertLevelToBudget instead."},
{"ThinkingBudgetToEffort", "Deprecated: Use thinking.ConvertBudgetToLevel instead."},
{"GetModelThinkingLevels", "Deprecated: Access modelInfo.Thinking.Levels directly."},
{"ModelUsesThinkingLevels", "Deprecated: Check len(modelInfo.Thinking.Levels) > 0."},
{"NormalizeReasoningEffortLevel", "Deprecated: Use thinking.ValidateConfig for level validation."},
{"IsOpenAICompatibilityModel", "Deprecated: Check modelInfo.Type == \"openai-compatibility\"."},
{"ThinkingLevelToBudget", "Deprecated: Use thinking.ConvertLevelToBudget instead."},
}
for _, tt := range tests {
t.Run(tt.funcName, func(t *testing.T) {
doc, ok := docs[tt.funcName]
if !ok {
t.Fatalf("missing function %q in thinking.go", tt.funcName)
}
if !strings.Contains(doc, tt.want) {
t.Fatalf("missing deprecation note for %s: want %q, got %q", tt.funcName, tt.want, doc)
}
})
}
})
// Test thinking_suffix.go deprecation comments
t.Run("thinking_suffix.go", func(t *testing.T) {
docs := parseFuncDocs(t, filepath.Join(dir, "thinking_suffix.go"))
tests := []struct {
funcName string
want string
}{
{"NormalizeThinkingModel", "Deprecated: Use thinking.ParseSuffix instead."},
{"ThinkingFromMetadata", "Deprecated: Access ThinkingConfig fields directly."},
{"ResolveThinkingConfigFromMetadata", "Deprecated: Use thinking.ApplyThinking instead."},
{"ReasoningEffortFromMetadata", "Deprecated: Use thinking.ConvertBudgetToLevel instead."},
{"ResolveOriginalModel", "Deprecated: Parse model suffix with thinking.ParseSuffix."},
}
for _, tt := range tests {
t.Run(tt.funcName, func(t *testing.T) {
doc, ok := docs[tt.funcName]
if !ok {
t.Fatalf("missing function %q in thinking_suffix.go", tt.funcName)
}
if !strings.Contains(doc, tt.want) {
t.Fatalf("missing deprecation note for %s: want %q, got %q", tt.funcName, tt.want, doc)
}
})
}
})
// Test thinking_text.go deprecation comments
t.Run("thinking_text.go", func(t *testing.T) {
docs := parseFuncDocs(t, filepath.Join(dir, "thinking_text.go"))
tests := []struct {
funcName string
want string
}{
{"GetThinkingText", "Deprecated: Use thinking package for thinking text extraction."},
{"GetThinkingTextFromJSON", "Deprecated: Use thinking package for thinking text extraction."},
{"SanitizeThinkingPart", "Deprecated: Use thinking package for thinking part sanitization."},
{"StripCacheControl", "Deprecated: Use thinking package for cache control stripping."},
}
for _, tt := range tests {
t.Run(tt.funcName, func(t *testing.T) {
doc, ok := docs[tt.funcName]
if !ok {
t.Fatalf("missing function %q in thinking_text.go", tt.funcName)
}
if !strings.Contains(doc, tt.want) {
t.Fatalf("missing deprecation note for %s: want %q, got %q", tt.funcName, tt.want, doc)
}
})
}
})
}
func parseFuncDocs(t *testing.T, path string) map[string]string {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
t.Fatalf("parse %s: %v", path, err)
}
docs := map[string]string{}
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Recv != nil {
continue
}
if fn.Doc == nil {
docs[fn.Name.Name] = ""
continue
}
docs[fn.Name.Name] = fn.Doc.Text()
}
return docs
}
func thinkingSourceDir() (string, error) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
return "", os.ErrNotExist
}
return filepath.Dir(thisFile), nil
}

View File

@@ -7,15 +7,30 @@ import (
)
const (
ThinkingBudgetMetadataKey = "thinking_budget"
ThinkingIncludeThoughtsMetadataKey = "thinking_include_thoughts"
ReasoningEffortMetadataKey = "reasoning_effort"
ThinkingOriginalModelMetadataKey = "thinking_original_model"
// Deprecated: No longer used. Thinking configuration is now passed via
// model name suffix and processed by thinking.ApplyThinking().
ThinkingBudgetMetadataKey = "thinking_budget"
// Deprecated: No longer used. See ThinkingBudgetMetadataKey.
ThinkingIncludeThoughtsMetadataKey = "thinking_include_thoughts"
// Deprecated: No longer used. See ThinkingBudgetMetadataKey.
ReasoningEffortMetadataKey = "reasoning_effort"
// Deprecated: No longer used. The original model name (with suffix) is now
// preserved directly in the model field. Use thinking.ParseSuffix() to
// extract the base model name if needed.
ThinkingOriginalModelMetadataKey = "thinking_original_model"
// ModelMappingOriginalModelMetadataKey stores the client-requested model alias
// for OAuth model name mappings. This is NOT deprecated.
ModelMappingOriginalModelMetadataKey = "model_mapping_original_model"
)
// NormalizeThinkingModel parses dynamic thinking suffixes on model names and returns
// the normalized base model with extracted metadata. Supported pattern:
//
// Deprecated: Use thinking.ParseSuffix instead.
// - "(<value>)" where value can be:
// - A numeric budget (e.g., "(8192)", "(16384)")
// - A reasoning effort level (e.g., "(high)", "(medium)", "(low)")
@@ -89,6 +104,8 @@ func NormalizeThinkingModel(modelName string) (string, map[string]any) {
// ThinkingFromMetadata extracts thinking overrides from metadata produced by NormalizeThinkingModel.
// It accepts both the new generic keys and legacy Gemini-specific keys.
//
// Deprecated: Access ThinkingConfig fields directly.
func ThinkingFromMetadata(metadata map[string]any) (*int, *bool, *string, bool) {
if len(metadata) == 0 {
return nil, nil, nil, false
@@ -159,6 +176,8 @@ func ThinkingFromMetadata(metadata map[string]any) (*int, *bool, *string, bool)
// ResolveThinkingConfigFromMetadata derives thinking budget/include overrides,
// converting reasoning effort strings into budgets when possible.
//
// Deprecated: Use thinking.ApplyThinking instead.
func ResolveThinkingConfigFromMetadata(model string, metadata map[string]any) (*int, *bool, bool) {
budget, include, effort, matched := ThinkingFromMetadata(metadata)
if !matched {
@@ -180,6 +199,8 @@ func ResolveThinkingConfigFromMetadata(model string, metadata map[string]any) (*
// ReasoningEffortFromMetadata resolves a reasoning effort string from metadata,
// inferring "auto" and "none" when budgets request dynamic or disabled thinking.
//
// Deprecated: Use thinking.ConvertBudgetToLevel instead.
func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) {
budget, include, effort, matched := ThinkingFromMetadata(metadata)
if !matched {
@@ -204,6 +225,8 @@ func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) {
// ResolveOriginalModel returns the original model name stored in metadata (if present),
// otherwise falls back to the provided model.
//
// Deprecated: Parse model suffix with thinking.ParseSuffix.
func ResolveOriginalModel(model string, metadata map[string]any) string {
normalize := func(name string) string {
if name == "" {

View File

@@ -11,6 +11,8 @@ import (
// - Wrapped object: { "thinking": { "text": "text", "cache_control": {...} } }
// - Gemini-style: { "thought": true, "text": "text" }
// Returns the extracted text string.
//
// Deprecated: Use thinking package for thinking text extraction.
func GetThinkingText(part gjson.Result) string {
// Try direct text field first (Gemini-style)
if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
@@ -42,6 +44,8 @@ func GetThinkingText(part gjson.Result) string {
}
// GetThinkingTextFromJSON extracts thinking text from a raw JSON string.
//
// Deprecated: Use thinking package for thinking text extraction.
func GetThinkingTextFromJSON(jsonStr string) string {
return GetThinkingText(gjson.Parse(jsonStr))
}
@@ -49,6 +53,8 @@ func GetThinkingTextFromJSON(jsonStr string) string {
// SanitizeThinkingPart normalizes a thinking part to a canonical form.
// Strips cache_control and other non-essential fields.
// Returns the sanitized part as JSON string.
//
// Deprecated: Use thinking package for thinking part sanitization.
func SanitizeThinkingPart(part gjson.Result) string {
// Gemini-style: { thought: true, text, thoughtSignature }
if part.Get("thought").Bool() {
@@ -79,6 +85,8 @@ func SanitizeThinkingPart(part gjson.Result) string {
}
// StripCacheControl removes cache_control and providerOptions from a JSON object.
//
// Deprecated: Use thinking package for cache control stripping.
func StripCacheControl(jsonStr string) string {
result := jsonStr
result, _ = sjson.Delete(result, "cache_control")