refactor: improve thinking logic
This commit is contained in:
@@ -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:
|
||||
|
||||
130
internal/util/thinking_deprecation_test.go
Normal file
130
internal/util/thinking_deprecation_test.go
Normal 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
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user