refactor(thinking): use bracket tags for thinking meta

Align thinking suffix handling on a single bracket-style marker.

NormalizeThinkingModel strips a terminal `[value]` segment from
model identifiers and turns it into either a thinking budget (for
numeric values) or a reasoning effort hint (for strings). Emission
of `ThinkingIncludeThoughtsMetadataKey` is removed.

Executor helpers and the example config are updated so their
comments reference the new `[value]` suffix format instead of the
legacy dash variants.

BREAKING CHANGE: dash-based thinking suffixes (`-thinking`,
`-thinking-N`, `-reasoning`, `-nothinking`) are no longer parsed
for thinking metadata; only `[value]` annotations are recognized.
This commit is contained in:
hkfires
2025-12-11 18:17:28 +08:00
parent 6285459c08
commit facfe7c518
3 changed files with 41 additions and 87 deletions

View File

@@ -100,7 +100,7 @@ ws-auth: false
# excluded-models: # excluded-models:
# - "claude-opus-4-5-20251101" # exclude specific models (exact match) # - "claude-opus-4-5-20251101" # exclude specific models (exact match)
# - "claude-3-*" # wildcard matching prefix (e.g. claude-3-7-sonnet-20250219) # - "claude-3-*" # wildcard matching prefix (e.g. claude-3-7-sonnet-20250219)
# - "*-think" # wildcard matching suffix (e.g. claude-opus-4-5-thinking) # - "*-thinking" # wildcard matching suffix (e.g. claude-opus-4-5-thinking)
# - "*haiku*" # wildcard matching substring (e.g. claude-3-5-haiku-20241022) # - "*haiku*" # wildcard matching substring (e.g. claude-3-5-haiku-20241022)
# OpenAI compatibility providers # OpenAI compatibility providers

View File

@@ -11,7 +11,7 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
// applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) // applyThinkingMetadata applies thinking config from model suffix metadata (e.g., [high], [8192])
// for standard Gemini format payloads. It normalizes the budget when the model supports thinking. // for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte { func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
@@ -28,7 +28,7 @@ func applyThinkingMetadata(payload []byte, metadata map[string]any, model string
return util.ApplyGeminiThinkingConfig(payload, budgetOverride, includeOverride) return util.ApplyGeminiThinkingConfig(payload, budgetOverride, includeOverride)
} }
// applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N) // applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., [high], [8192])
// for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking. // for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking.
func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte { func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte {
budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata) budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)

View File

@@ -14,100 +14,57 @@ const (
) )
// NormalizeThinkingModel parses dynamic thinking suffixes on model names and returns // NormalizeThinkingModel parses dynamic thinking suffixes on model names and returns
// the normalized base model with extracted metadata. Supported patterns: // the normalized base model with extracted metadata. Supported pattern:
// - "-thinking-<number>" extracts a numeric budget // - "[<value>]" where value can be:
// - "-thinking-<level>" extracts a reasoning effort level (minimal/low/medium/high/xhigh/auto/none) // - A numeric budget (e.g., "[8192]", "[16384]")
// - "-thinking" maps to a default reasoning effort of "medium" // - A reasoning effort level (e.g., "[high]", "[medium]", "[low]")
// - "-reasoning" maps to dynamic budget (-1) and include_thoughts=true //
// - "-nothinking" maps to budget=0 and include_thoughts=false // Examples:
// - "claude-sonnet-4-5-20250929[16384]" → budget=16384
// - "gpt-5.1[high]" → reasoning_effort="high"
// - "gemini-2.5-pro[32768]" → budget=32768
//
// Note: Empty brackets "[]" are not supported and will be ignored.
func NormalizeThinkingModel(modelName string) (string, map[string]any) { func NormalizeThinkingModel(modelName string) (string, map[string]any) {
if modelName == "" { if modelName == "" {
return modelName, nil return modelName, nil
} }
lower := strings.ToLower(modelName)
baseModel := modelName baseModel := modelName
var ( var (
budgetOverride *int budgetOverride *int
includeThoughts *bool
reasoningEffort *string reasoningEffort *string
matched bool matched bool
) )
switch { // Match "[value]" pattern at the end of the model name
case strings.HasSuffix(lower, "-nothinking"): if idx := strings.LastIndex(modelName, "["); idx != -1 {
baseModel = modelName[:len(modelName)-len("-nothinking")] if !strings.HasSuffix(modelName, "]") {
budget := 0 // Incomplete bracket, ignore
include := false return baseModel, nil
budgetOverride = &budget }
includeThoughts = &include
matched = true value := modelName[idx+1 : len(modelName)-1] // Extract content between [ and ]
case strings.HasSuffix(lower, "-reasoning"): if value == "" {
baseModel = modelName[:len(modelName)-len("-reasoning")] // Empty brackets not supported
budget := -1 return baseModel, nil
include := true }
budgetOverride = &budget
includeThoughts = &include candidateBase := modelName[:idx]
matched = true
default: // Auto-detect: pure numeric → budget, string → reasoning effort level
if idx := strings.LastIndex(lower, "-thinking-"); idx != -1 { if parsed, ok := parseIntPrefix(value); ok {
// Skip stripping if the original model is a registered thinking model. // Numeric value: treat as thinking budget
// This prevents "-thinking-2507" in "qwen3-235b-a22b-thinking-2507" from being parsed. baseModel = candidateBase
if ModelSupportsThinking(modelName) { budgetOverride = &parsed
break matched = true
} } else {
value := modelName[idx+len("-thinking-"):] // String value: treat as reasoning effort level
if value != "" { baseModel = candidateBase
if parsed, ok := parseIntPrefix(value); ok { raw := strings.ToLower(strings.TrimSpace(value))
candidateBase := modelName[:idx] if raw != "" {
if ModelUsesThinkingLevels(candidateBase) { reasoningEffort = &raw
baseModel = candidateBase
// Numeric suffix on level-aware models should still surface as reasoning effort metadata.
raw := strings.ToLower(strings.TrimSpace(value))
if raw != "" {
reasoningEffort = &raw
}
matched = true
} else {
baseModel = candidateBase
budgetOverride = &parsed
matched = true
}
} else {
baseModel = modelName[:idx]
if normalized, ok := NormalizeReasoningEffortLevel(baseModel, value); ok {
reasoningEffort = &normalized
matched = true
} else if !ModelUsesThinkingLevels(baseModel) {
// Keep unknown effort tokens so callers can honor user intent even without normalization.
raw := strings.ToLower(strings.TrimSpace(value))
if raw != "" {
reasoningEffort = &raw
matched = true
} else {
baseModel = modelName
}
} else {
raw := strings.ToLower(strings.TrimSpace(value))
if raw != "" {
reasoningEffort = &raw
matched = true
} else {
baseModel = modelName
}
}
}
}
} else if strings.HasSuffix(lower, "-thinking") {
candidateBase := modelName[:len(modelName)-len("-thinking")]
// Only strip the suffix if the original model is NOT a registered thinking model.
// This prevents stripping "-thinking" from models like "kimi-k2-thinking" where
// the suffix is part of the model's actual name.
if !ModelSupportsThinking(modelName) {
baseModel = candidateBase
effort := "medium"
reasoningEffort = &effort
matched = true matched = true
} }
} }
@@ -123,9 +80,6 @@ func NormalizeThinkingModel(modelName string) (string, map[string]any) {
if budgetOverride != nil { if budgetOverride != nil {
metadata[ThinkingBudgetMetadataKey] = *budgetOverride metadata[ThinkingBudgetMetadataKey] = *budgetOverride
} }
if includeThoughts != nil {
metadata[ThinkingIncludeThoughtsMetadataKey] = *includeThoughts
}
if reasoningEffort != nil { if reasoningEffort != nil {
metadata[ReasoningEffortMetadataKey] = *reasoningEffort metadata[ReasoningEffortMetadataKey] = *reasoningEffort
} }