feat: sync deployed v1.0.0-rc.38-custom.42

Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
cneicy committed 2026-09-26 04:49:11 +08:00
1 parent aa6c3f50f4
commit ceddfd3d6b
828 files changed
+122253 -13702

No files matched your search

+30 -10
View File
@@ -15,7 +15,10 @@ type ClaudeRender struct {
EffectiveEffort Effort
ClearSampling bool
ConstrainThinkingSampling bool
Diagnostics []types.ConversionDiagnostic
// MaxTokens is set when manual thinking needed a larger max_tokens than
// the request carried; callers apply it to the outgoing request.
MaxTokens *uint
Diagnostics []types.ConversionDiagnostic
}
type claudeCapabilities struct {
@@ -82,15 +85,18 @@ func RenderClaude(model string, intent Intent, maxTokens *uint, adapterBudgetPer
return ClaudeRender{}, err
}
intent.Effort = effort
} else {
}
diagnostics := make([]types.ConversionDiagnostic, 0, 1)
if !disabledWithEffort {
var normalizeDiagnostics []types.ConversionDiagnostic
var err error
intent, err = normalizeIntent(intent)
intent, normalizeDiagnostics, err = normalizeIntent(intent)
if err != nil {
return ClaudeRender{}, err
}
diagnostics = append(diagnostics, normalizeDiagnostics...)
}
capabilities := claudeCapabilitiesFor(model)
diagnostics := make([]types.ConversionDiagnostic, 0, 1)
if disabledWithEffort {
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
"claude_disabled_effort_ignored",
@@ -225,13 +231,26 @@ func RenderClaude(model string, intent Intent, maxTokens *uint, adapterBudgetPer
}
}
if intent.Mode == ModeUnset {
return ClaudeRender{OutputEffort: intent.Effort, EffectiveEffort: intent.Effort}, nil
return ClaudeRender{OutputEffort: intent.Effort, EffectiveEffort: intent.Effort, Diagnostics: diagnostics}, nil
}
if maxTokens == nil {
return ClaudeRender{}, fmt.Errorf("max_tokens is required for manual Claude thinking")
}
if *maxTokens <= 1024 {
return ClaudeRender{}, fmt.Errorf("max_tokens must be greater than 1024 for manual Claude thinking")
var raisedMaxTokens *uint
if maxTokens == nil || *maxTokens <= 1024 {
// Manual thinking needs room above the 1024-token minimum budget.
// Raising max_tokens keeps the request usable instead of rejecting it.
raised := uint(1280)
if intent.BudgetTokens != nil && *intent.BudgetTokens >= 0 && *intent.BudgetTokens < math.MaxInt32/2 {
raised = max(raised, uint(*intent.BudgetTokens)+1)
}
current := "a missing max_tokens"
if maxTokens != nil {
current = fmt.Sprintf("max_tokens %d", *maxTokens)
}
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
"claude_max_tokens_raised",
fmt.Sprintf("model %q requires max_tokens greater than 1024 for manual thinking; raised %s to %d", model, current, raised),
))
raisedMaxTokens = &raised
maxTokens = &raised
}
if uint64(*maxTokens) > uint64(math.MaxInt) {
return ClaudeRender{}, fmt.Errorf("max_tokens is too large for a thinking budget")
@@ -288,6 +307,7 @@ func RenderClaude(model string, intent Intent, maxTokens *uint, adapterBudgetPer
OutputEffort: outputEffort,
EffectiveEffort: effectiveEffort,
ConstrainThinkingSampling: true,
MaxTokens: raisedMaxTokens,
Diagnostics: diagnostics,
}, nil
}
+219 -68
View File
@@ -6,11 +6,13 @@ import (
"strings"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
)
type GeminiRender struct {
Config *dto.GeminiThinkingConfig
EffectiveEffort Effort
Diagnostics []types.ConversionDiagnostic
}
type geminiThinkingKind int
@@ -61,46 +63,85 @@ func geminiCapabilitiesFor(model string) geminiCapabilities {
}
}
// RenderGemini maps a portable intent onto the target model's thinkingConfig.
// The capability table only maps and clamps: budgets become levels on Gemini 3,
// efforts become budgets on Gemini 2.5, out-of-range values are clamped, and
// models that cannot disable thinking get their lowest setting. Every such
// coercion is reported in Diagnostics. Errors are reserved for unknown effort
// values and numeric overflow.
func RenderGemini(model string, intent Intent, maxOutputTokens *uint, adapterBudgetPercentage float64) (GeminiRender, error) {
intent, err := normalizeIntent(intent)
intent, diagnostics, err := normalizeIntent(intent)
if err != nil {
return GeminiRender{}, err
}
if intent.IsEmpty() {
return GeminiRender{}, nil
return GeminiRender{Diagnostics: diagnostics}, nil
}
capabilities := geminiCapabilitiesFor(model)
if capabilities.kind == geminiThinkingNotConfigurable {
if !intent.HasStrength() && capabilities.supportsIncludeThoughts {
return GeminiRender{Config: &dto.GeminiThinkingConfig{IncludeThoughts: intent.IncludeThoughts}, EffectiveEffort: EffortHigh}, nil
render := GeminiRender{Diagnostics: diagnostics}
if capabilities.supportsIncludeThoughts {
render.EffectiveEffort = EffortHigh
if intent.IncludeThoughts != nil {
render.Config = &dto.GeminiThinkingConfig{IncludeThoughts: intent.IncludeThoughts}
}
}
return GeminiRender{}, fmt.Errorf("model %q does not support configurable thinking", model)
if intent.HasStrength() || (intent.IncludeThoughts != nil && !capabilities.supportsIncludeThoughts) {
render.Diagnostics = append(render.Diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_unsupported",
fmt.Sprintf("model %q does not support configurable thinking; the requested thinking controls were dropped", model),
))
}
return render, nil
}
if capabilities.kind == geminiThinkingUnknown {
// Without a capability entry there is no way to know whether the model
// takes a budget or a level, so no strength is synthesized. Provider-
// native controls the client sent stay untouched by the caller.
render := GeminiRender{Diagnostics: diagnostics}
if intent.HasStrength() {
return GeminiRender{}, fmt.Errorf("model %q does not have a known Gemini thinking configuration", model)
render.Diagnostics = append(render.Diagnostics, geminiReasoningDiagnostic(
"gemini_unknown_capability",
fmt.Sprintf("model %q has no known Gemini thinking configuration; the requested thinking strength was not applied", model),
))
}
return GeminiRender{Config: &dto.GeminiThinkingConfig{IncludeThoughts: intent.IncludeThoughts}}, nil
if intent.IncludeThoughts != nil {
render.Config = &dto.GeminiThinkingConfig{IncludeThoughts: intent.IncludeThoughts}
}
return render, nil
}
config := &dto.GeminiThinkingConfig{IncludeThoughts: intent.IncludeThoughts}
if capabilities.kind == geminiThinkingBudget {
if intent.Mode == ModeDisabled || intent.Effort == EffortNone {
if !capabilities.supportsDisable {
return GeminiRender{}, fmt.Errorf("%w for model %q", ErrThinkingNotDisabled, model)
}
budget := 0
effort := EffortNone
if !capabilities.supportsDisable {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_disable_unsupported",
fmt.Sprintf("model %q cannot disable thinking; using its minimum thinking budget %d", model, capabilities.minBudget),
))
budget = capabilities.minBudget
effort = EffortFromBudget(budget)
}
config.ThinkingBudget = &budget
return GeminiRender{Config: config, EffectiveEffort: EffortNone}, nil
return GeminiRender{Config: config, EffectiveEffort: effort, Diagnostics: diagnostics}, nil
}
budget := 0
hasBudget := false
if intent.BudgetTokens != nil {
budget = *intent.BudgetTokens
if intent.BudgetSource != SourceNative && budget != -1 {
budget = clampGeminiBudget(budget, capabilities)
if budget != -1 {
clamped := clampGeminiBudget(budget, capabilities)
if clamped != budget {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_clamped",
fmt.Sprintf("thinking budget %d is outside the supported range [%d,%d] for model %q; using %d", budget, capabilities.minBudget, capabilities.maxBudget, model, clamped),
))
budget = clamped
}
}
hasBudget = true
} else if intent.Effort != "" {
@@ -121,9 +162,6 @@ func RenderGemini(model string, intent Intent, maxOutputTokens *uint, adapterBud
hasBudget = true
}
if hasBudget {
if err := validateGeminiBudget(model, budget, capabilities); err != nil {
return GeminiRender{}, err
}
config.ThinkingBudget = &budget
}
effort := intent.Effort
@@ -132,27 +170,46 @@ func RenderGemini(model string, intent Intent, maxOutputTokens *uint, adapterBud
} else if intent.Mode == ModeEnabled || intent.Mode == ModeAdaptive {
effort = geminiDefaultEffort(model)
}
return GeminiRender{Config: config, EffectiveEffort: effort}, nil
return GeminiRender{Config: config, EffectiveEffort: effort, Diagnostics: diagnostics}, nil
}
if intent.Mode == ModeDisabled || intent.Effort == EffortNone {
return GeminiRender{}, fmt.Errorf("%w for model %q", ErrThinkingNotDisabled, model)
level, err := geminiLevelForEffort(model, EffortMinimal)
if err != nil {
return GeminiRender{}, err
}
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_disable_unsupported",
fmt.Sprintf("model %q cannot disable thinking; using its lowest thinking level %q", model, level),
))
config.ThinkingLevel = level
return GeminiRender{Config: config, EffectiveEffort: Effort(level), Diagnostics: diagnostics}, nil
}
effort := intent.Effort
if effort == "" && intent.BudgetTokens != nil {
effort = EffortFromBudget(*intent.BudgetTokens)
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_to_level",
fmt.Sprintf("model %q uses thinkingLevel; thinking budget %d was converted to effort %q", model, *intent.BudgetTokens, effort),
))
}
if effort != "" {
level, err := geminiLevelForEffort(model, effort)
if err != nil {
return GeminiRender{}, err
}
if !strings.EqualFold(level, string(effort)) {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_level_adjusted",
fmt.Sprintf("model %q does not support thinking level %q; using %q", model, effort, level),
))
}
config.ThinkingLevel = level
effort = Effort(level)
} else if intent.Mode == ModeEnabled || intent.Mode == ModeAdaptive {
effort = geminiDefaultEffort(model)
}
return GeminiRender{Config: config, EffectiveEffort: effort}, nil
return GeminiRender{Config: config, EffectiveEffort: effort, Diagnostics: diagnostics}, nil
}
func geminiDefaultEffort(model string) Effort {
@@ -176,49 +233,165 @@ func geminiDefaultEffort(model string) Effort {
}
}
func ValidateGeminiThinkingConfig(model string, config *dto.GeminiThinkingConfig) (Effort, error) {
if config == nil {
return "", nil
}
intent, err := FromGemini(&dto.GeminiChatRequest{GenerationConfig: dto.GeminiChatGenerationConfig{ThinkingConfig: config}})
if err != nil {
return "", err
// NormalizeGeminiThinkingConfig rewrites a provider-native thinkingConfig in
// place into the dialect the target model accepts: budgets become levels on
// Gemini 3, levels become budgets on Gemini 2.5, out-of-range budgets are
// clamped, unsupported levels move to the nearest supported one, and models
// without configurable thinking lose the config entirely. Models missing from
// the capability table keep their config verbatim. The returned effort is the
// accounting label for whatever is now in the config.
//
// The only error is a config that sets both thinkingBudget and thinkingLevel
// on a model whose dialect is unknown; when the dialect is known the field the
// model does not use is dropped with a diagnostic.
func NormalizeGeminiThinkingConfig(model string, generation *dto.GeminiChatGenerationConfig) (Effort, []types.ConversionDiagnostic, error) {
if generation == nil || generation.ThinkingConfig == nil {
return "", nil, nil
}
config := generation.ThinkingConfig
capabilities := geminiCapabilitiesFor(model)
if capabilities.kind == geminiThinkingNotConfigurable {
if !intent.HasStrength() && capabilities.supportsIncludeThoughts {
return EffortHigh, nil
var diagnostics []types.ConversionDiagnostic
if config.ThinkingBudget != nil && config.ThinkingLevel != "" {
switch capabilities.kind {
case geminiThinkingBudget:
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_level_conflict",
fmt.Sprintf("model %q uses thinkingBudget; thinkingLevel %q was dropped because both were set", model, config.ThinkingLevel),
))
config.ThinkingLevel = ""
case geminiThinkingLevel:
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_level_conflict",
fmt.Sprintf("model %q uses thinkingLevel; thinkingBudget %d was dropped because both were set", model, *config.ThinkingBudget),
))
config.ThinkingBudget = nil
}
return "", fmt.Errorf("model %q does not support configurable thinking", model)
}
if capabilities.kind == geminiThinkingNotConfigurable {
dropped := config.ThinkingBudget != nil || config.ThinkingLevel != "" || (config.IncludeThoughts != nil && !capabilities.supportsIncludeThoughts)
if dropped {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_unsupported",
fmt.Sprintf("model %q does not support configurable thinking; the thinking configuration was dropped", model),
))
}
if !capabilities.supportsIncludeThoughts || config.IncludeThoughts == nil {
generation.ThinkingConfig = nil
return "", diagnostics, nil
}
config.ThinkingBudget = nil
config.ThinkingLevel = ""
return EffortHigh, diagnostics, nil
}
intent, intentDiagnostics, err := FromGemini(&dto.GeminiChatRequest{GenerationConfig: *generation})
if err != nil {
return "", diagnostics, err
}
diagnostics = append(diagnostics, intentDiagnostics...)
if capabilities.kind == geminiThinkingUnknown {
return EffectiveEffort(intent), nil
return EffectiveEffort(intent), diagnostics, nil
}
if capabilities.kind == geminiThinkingBudget {
if config.ThinkingLevel != "" {
return "", fmt.Errorf("Gemini 2.5 model %q requires thinkingBudget, not thinkingLevel", model)
budget := gemini25BudgetForEffort(intent.Effort)
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_level_to_budget",
fmt.Sprintf("model %q uses thinkingBudget; thinkingLevel %q was converted to budget %d", model, config.ThinkingLevel, budget),
))
config.ThinkingLevel = ""
config.ThinkingBudget = &budget
}
if config.ThinkingBudget != nil {
if err := validateGeminiBudget(model, *config.ThinkingBudget, capabilities); err != nil {
return "", err
if config.ThinkingBudget == nil {
return EffectiveEffort(intent), diagnostics, nil
}
budget := *config.ThinkingBudget
switch {
case budget < -1:
budget = -1
case budget == 0 && !capabilities.supportsDisable:
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_disable_unsupported",
fmt.Sprintf("model %q cannot disable thinking; using its minimum thinking budget %d", model, capabilities.minBudget),
))
budget = capabilities.minBudget
case budget != 0 && budget != -1:
clamped := clampGeminiBudget(budget, capabilities)
if clamped != budget {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_clamped",
fmt.Sprintf("thinking budget %d is outside the supported range [%d,%d] for model %q; using %d", budget, capabilities.minBudget, capabilities.maxBudget, model, clamped),
))
budget = clamped
}
}
return EffectiveEffort(intent), nil
config.ThinkingBudget = &budget
return EffortFromBudget(budget), diagnostics, nil
}
if config.ThinkingBudget != nil {
return "", fmt.Errorf("Gemini 3 model %q requires thinkingLevel, not thinkingBudget", model)
}
if config.ThinkingLevel != "" {
level, err := geminiLevelForEffort(model, intent.Effort)
budget := *config.ThinkingBudget
config.ThinkingBudget = nil
if budget == 0 {
level, err := geminiLevelForEffort(model, EffortMinimal)
if err != nil {
return "", diagnostics, err
}
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_disable_unsupported",
fmt.Sprintf("model %q cannot disable thinking; using its lowest thinking level %q", model, level),
))
config.ThinkingLevel = level
return Effort(level), diagnostics, nil
}
level, err := geminiLevelForEffort(model, EffortFromBudget(budget))
if err != nil {
return "", err
return "", diagnostics, err
}
if level != config.ThinkingLevel {
return "", fmt.Errorf("thinkingLevel %q is not supported by model %q", config.ThinkingLevel, model)
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_budget_to_level",
fmt.Sprintf("model %q uses thinkingLevel; thinkingBudget %d was converted to thinkingLevel %q", model, budget, level),
))
config.ThinkingLevel = level
return Effort(level), diagnostics, nil
}
if config.ThinkingLevel == "" {
return "", diagnostics, nil
}
if intent.Effort == EffortNone {
level, err := geminiLevelForEffort(model, EffortMinimal)
if err != nil {
return "", diagnostics, err
}
return Effort(level), nil
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_thinking_disable_unsupported",
fmt.Sprintf("model %q cannot disable thinking; using its lowest thinking level %q", model, level),
))
config.ThinkingLevel = level
return Effort(level), diagnostics, nil
}
level, err := geminiLevelForEffort(model, intent.Effort)
if err != nil {
return "", diagnostics, err
}
if level != string(intent.Effort) {
diagnostics = append(diagnostics, geminiReasoningDiagnostic(
"gemini_level_adjusted",
fmt.Sprintf("model %q does not support thinkingLevel %q; using %q", model, config.ThinkingLevel, level),
))
}
config.ThinkingLevel = level
return Effort(level), diagnostics, nil
}
func geminiReasoningDiagnostic(code string, message string) types.ConversionDiagnostic {
return types.ConversionDiagnostic{
Code: code,
Path: "generationConfig.thinkingConfig",
Message: message,
Severity: types.ConversionDiagnosticWarning,
}
return "", nil
}
// ResolveGeminiDefault materializes documented family defaults when a
@@ -347,28 +520,6 @@ func geminiLevelForEffort(model string, effort Effort) (string, error) {
}
}
func validateGeminiBudget(model string, budget int, capabilities geminiCapabilities) error {
if budget == -1 {
return nil
}
if budget == 0 {
if capabilities.supportsDisable {
return nil
}
return fmt.Errorf("%w for model %q", ErrThinkingNotDisabled, model)
}
if budget < capabilities.minBudget || budget > capabilities.maxBudget {
return fmt.Errorf("thinking budget %d is outside the supported range [%d,%d] for model %q", budget, capabilities.minBudget, capabilities.maxBudget, model)
}
return nil
}
func clampGeminiBudget(budget int, capabilities geminiCapabilities) int {
if budget < capabilities.minBudget {
return capabilities.minBudget
}
if budget > capabilities.maxBudget {
return capabilities.maxBudget
}
return budget
return min(max(budget, capabilities.minBudget), capabilities.maxBudget)
}
@@ -0,0 +1,365 @@
package reasoning
import (
"testing"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRenderGeminiMapsAndClampsInsteadOfRejecting(t *testing.T) {
t.Parallel()
budget8192 := 8192
budget100000 := 100000
budget100 := 100
tests := []struct {
name string
model string
intent Intent
wantNil bool
wantLevel string
wantBudget *int
wantEffort Effort
wantCodes []string
}{
{
name: "budget on gemini 3.1 pro becomes level",
model: "gemini-3.1-pro-preview",
intent: Intent{BudgetTokens: &budget8192},
wantLevel: "medium",
wantEffort: EffortMedium,
wantCodes: []string{"gemini_budget_to_level"},
},
{
name: "medium on gemini 3 pro moves to high",
model: "gemini-3-pro-preview",
intent: Intent{Mode: ModeEnabled, Effort: EffortMedium},
wantLevel: "high",
wantEffort: EffortHigh,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "xhigh on gemini 3 flash moves to high",
model: "gemini-3-flash-preview",
intent: Intent{Mode: ModeEnabled, Effort: EffortXHigh},
wantLevel: "high",
wantEffort: EffortHigh,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "effort on gemini 2.5 flash becomes budget",
model: "gemini-2.5-flash",
intent: Intent{Mode: ModeEnabled, Effort: EffortHigh},
wantBudget: intPtr(24576),
wantEffort: EffortHigh,
},
{
name: "oversized budget on gemini 2.5 flash is clamped",
model: "gemini-2.5-flash",
intent: Intent{BudgetTokens: &budget100000},
wantBudget: intPtr(24576),
wantEffort: EffortHigh,
wantCodes: []string{"gemini_budget_clamped"},
},
{
name: "undersized budget on gemini 2.5 flash lite is raised",
model: "gemini-2.5-flash-lite",
intent: Intent{BudgetTokens: &budget100},
wantBudget: intPtr(512),
wantEffort: EffortLow,
wantCodes: []string{"gemini_budget_clamped"},
},
{
name: "nothinking on gemini 3.1 pro uses lowest level",
model: "gemini-3.1-pro-preview",
intent: Intent{Mode: ModeDisabled, Effort: EffortNone, Source: SourceSuffix},
wantLevel: "low",
wantEffort: EffortLow,
wantCodes: []string{"gemini_thinking_disable_unsupported"},
},
{
name: "nothinking on gemini 2.5 pro uses minimum budget",
model: "gemini-2.5-pro",
intent: Intent{Mode: ModeDisabled, Effort: EffortNone, Source: SourceSuffix},
wantBudget: intPtr(128),
wantEffort: EffortLow,
wantCodes: []string{"gemini_thinking_disable_unsupported"},
},
{
name: "nothinking on gemini 2.5 flash disables",
model: "gemini-2.5-flash",
intent: Intent{Mode: ModeDisabled, Effort: EffortNone, Source: SourceSuffix},
wantBudget: intPtr(0),
wantEffort: EffortNone,
},
{
name: "unknown model gets no synthesized config",
model: "gemini-2.0-flash-thinking-exp",
intent: Intent{Mode: ModeEnabled, Source: SourceSuffix},
wantNil: true,
wantCodes: []string{"gemini_unknown_capability"},
},
{
name: "image model drops thinking controls",
model: "gemini-2.5-flash-image",
intent: Intent{Mode: ModeEnabled, Source: SourceSuffix},
wantNil: true,
wantCodes: []string{"gemini_thinking_unsupported"},
},
{
name: "gemini 3 pro image keeps include thoughts only",
model: "gemini-3-pro-image-preview",
intent: Intent{Mode: ModeEnabled, Effort: EffortLow, IncludeThoughts: boolPtr(true)},
wantEffort: EffortHigh,
wantCodes: []string{"gemini_thinking_unsupported"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := RenderGemini(tt.model, tt.intent, nil, 0)
require.NoError(t, err)
assert.Equal(t, tt.wantEffort, got.EffectiveEffort)
assert.Equal(t, tt.wantCodes, diagnosticCodes(got.Diagnostics))
if tt.wantNil {
assert.Nil(t, got.Config)
return
}
require.NotNil(t, got.Config)
assert.Equal(t, tt.wantLevel, got.Config.ThinkingLevel)
if tt.wantBudget == nil {
assert.Nil(t, got.Config.ThinkingBudget)
return
}
require.NotNil(t, got.Config.ThinkingBudget)
assert.Equal(t, *tt.wantBudget, *got.Config.ThinkingBudget)
})
}
}
func TestNormalizeGeminiThinkingConfigRewritesNativeConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
model string
config dto.GeminiThinkingConfig
wantErr error
wantNil bool
wantLevel string
wantBudget *int
wantThoughts *bool
wantEffort Effort
wantCodes []string
}{
{
name: "budget on gemini 3.1 pro becomes level",
model: "gemini-3.1-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(8192)},
wantLevel: "medium",
wantEffort: EffortMedium,
wantCodes: []string{"gemini_budget_to_level"},
},
{
name: "dynamic budget on gemini 3 becomes high",
model: "gemini-3-flash-preview",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(-1)},
wantLevel: "high",
wantEffort: EffortHigh,
wantCodes: []string{"gemini_budget_to_level"},
},
{
name: "level on gemini 2.5 flash becomes budget",
model: "gemini-2.5-flash",
config: dto.GeminiThinkingConfig{ThinkingLevel: "high"},
wantBudget: intPtr(24576),
wantEffort: EffortHigh,
wantCodes: []string{"gemini_level_to_budget"},
},
{
name: "medium on gemini 3 pro moves to high",
model: "gemini-3-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingLevel: "medium"},
wantLevel: "high",
wantEffort: EffortHigh,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "minimal on gemini 3.1 pro moves to low",
model: "gemini-3.1-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingLevel: "minimal"},
wantLevel: "low",
wantEffort: EffortLow,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "supported level is kept without diagnostics",
model: "gemini-3.1-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingLevel: "low"},
wantLevel: "low",
wantEffort: EffortLow,
},
{
name: "uppercase enum level is canonicalized without diagnostics",
model: "gemini-3.7-flash",
config: dto.GeminiThinkingConfig{ThinkingLevel: "MEDIUM"},
wantLevel: "medium",
wantEffort: EffortMedium,
},
{
name: "mixed case level with whitespace is canonicalized without diagnostics",
model: "gemini-3.7-flash",
config: dto.GeminiThinkingConfig{ThinkingLevel: " Medium "},
wantLevel: "medium",
wantEffort: EffortMedium,
},
{
name: "uppercase minimal on gemini 3 pro moves to low",
model: "gemini-3-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingLevel: "MINIMAL"},
wantLevel: "low",
wantEffort: EffortLow,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "xhigh on gemini 3 moves to high",
model: "gemini-3.7-flash",
config: dto.GeminiThinkingConfig{ThinkingLevel: "xhigh"},
wantLevel: "high",
wantEffort: EffortHigh,
wantCodes: []string{"gemini_level_adjusted"},
},
{
name: "unknown level is rejected",
model: "gemini-3.7-flash",
config: dto.GeminiThinkingConfig{ThinkingLevel: "ULTRA"},
wantErr: ErrUnsupportedEffort,
},
{
name: "oversized budget on gemini 2.5 flash is clamped",
model: "gemini-2.5-flash",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(100000)},
wantBudget: intPtr(24576),
wantEffort: EffortHigh,
wantCodes: []string{"gemini_budget_clamped"},
},
{
name: "undersized budget on gemini 2.5 flash lite is raised",
model: "gemini-2.5-flash-lite",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(100)},
wantBudget: intPtr(512),
wantEffort: EffortLow,
wantCodes: []string{"gemini_budget_clamped"},
},
{
name: "zero budget on gemini 2.5 pro uses minimum budget",
model: "gemini-2.5-pro",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(0)},
wantBudget: intPtr(128),
wantEffort: EffortLow,
wantCodes: []string{"gemini_thinking_disable_unsupported"},
},
{
name: "zero budget on gemini 2.5 flash stays off",
model: "gemini-2.5-flash",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(0)},
wantBudget: intPtr(0),
wantEffort: EffortNone,
},
{
name: "zero budget on gemini 3.1 pro uses lowest level",
model: "gemini-3.1-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(0)},
wantLevel: "low",
wantEffort: EffortLow,
wantCodes: []string{"gemini_thinking_disable_unsupported"},
},
{
name: "dynamic budget on gemini 2.5 passes through",
model: "gemini-2.5-flash",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(-1)},
wantBudget: intPtr(-1),
wantEffort: EffortHigh,
},
{
name: "tts model drops the config",
model: "gemini-2.5-flash-preview-tts",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(1024)},
wantNil: true,
wantCodes: []string{"gemini_thinking_unsupported"},
},
{
name: "gemini 3 pro image keeps include thoughts only",
model: "gemini-3-pro-image-preview",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(1024), IncludeThoughts: boolPtr(true)},
wantThoughts: boolPtr(true),
wantEffort: EffortHigh,
wantCodes: []string{"gemini_thinking_unsupported"},
},
{
name: "unknown model passes the config through",
model: "gemini-2.0-flash-thinking-exp",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(4096)},
wantBudget: intPtr(4096),
wantEffort: EffortMedium,
},
{
name: "budget and level on gemini 3 keeps the level",
model: "gemini-3.1-pro-preview",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(8192), ThinkingLevel: "low"},
wantLevel: "low",
wantEffort: EffortLow,
wantCodes: []string{"gemini_budget_level_conflict"},
},
{
name: "budget and level on gemini 2.5 keeps the budget",
model: "gemini-2.5-flash",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(4096), ThinkingLevel: "high"},
wantBudget: intPtr(4096),
wantEffort: EffortMedium,
wantCodes: []string{"gemini_budget_level_conflict"},
},
{
name: "budget and level on an unknown model is rejected",
model: "gemini-2.0-flash-thinking-exp",
config: dto.GeminiThinkingConfig{ThinkingBudget: intPtr(1024), ThinkingLevel: "high"},
wantErr: ErrEffortConflict,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
config := tt.config
generation := &dto.GeminiChatGenerationConfig{ThinkingConfig: &config}
effort, diagnostics, err := NormalizeGeminiThinkingConfig(tt.model, generation)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantEffort, effort)
assert.Equal(t, tt.wantCodes, diagnosticCodes(diagnostics))
if tt.wantNil {
assert.Nil(t, generation.ThinkingConfig)
return
}
require.NotNil(t, generation.ThinkingConfig)
assert.Equal(t, tt.wantLevel, generation.ThinkingConfig.ThinkingLevel)
if tt.wantBudget == nil {
assert.Nil(t, generation.ThinkingConfig.ThinkingBudget)
} else {
require.NotNil(t, generation.ThinkingConfig.ThinkingBudget)
assert.Equal(t, *tt.wantBudget, *generation.ThinkingConfig.ThinkingBudget)
}
if tt.wantThoughts != nil {
require.NotNil(t, generation.ThinkingConfig.IncludeThoughts)
assert.Equal(t, *tt.wantThoughts, *generation.ThinkingConfig.IncludeThoughts)
}
})
}
}
+185 -96
View File
@@ -7,6 +7,7 @@ import (
"github.com/QuantumNous/new-api/relaykit/dto"
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
"github.com/QuantumNous/new-api/relaykit/types"
)
type Effort string
@@ -133,32 +134,62 @@ func ParseEffort(value string) (Effort, error) {
}
}
func normalizeIntent(intent Intent) (Intent, error) {
// reasoningDiagnostic describes a best-effort resolution between reasoning
// controls that is not tied to a single provider's field layout. The target
// format is filled in by the conversion pipeline.
func reasoningDiagnostic(code string, message string) types.ConversionDiagnostic {
return types.ConversionDiagnostic{
Code: code,
Path: "reasoning",
Message: message,
Severity: types.ConversionDiagnosticWarning,
}
}
// normalizeIntent canonicalizes one intent. Only unknown effort or mode values
// are errors; contradictions between mode, effort, and budget are resolved in
// favor of the more specific control (a numeric budget beats a mode or effort,
// effort none beats an enabling mode) and reported as diagnostics.
func normalizeIntent(intent Intent) (Intent, []types.ConversionDiagnostic, error) {
effort, err := ParseEffort(string(intent.Effort))
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent.Effort = effort
switch intent.Mode {
case ModeUnset, ModeEnabled, ModeAdaptive, ModeDisabled:
default:
return Intent{}, fmt.Errorf("unsupported reasoning mode %q", intent.Mode)
return Intent{}, nil, fmt.Errorf("unsupported reasoning mode %q", intent.Mode)
}
var diagnostics []types.ConversionDiagnostic
if intent.BudgetTokens != nil {
budget := *intent.BudgetTokens
if budget < -1 {
return Intent{}, fmt.Errorf("thinking budget must be -1 or non-negative, got %d", budget)
diagnostics = append(diagnostics, reasoningDiagnostic(
"reasoning_budget_adjusted",
fmt.Sprintf("thinking budget %d is below -1; using the dynamic budget -1", budget),
))
budget = -1
intent.BudgetTokens = &budget
}
if budget == 0 {
if intent.Mode == ModeEnabled || intent.Mode == ModeAdaptive || (intent.Effort != "" && intent.Effort != EffortNone) {
return Intent{}, fmt.Errorf("%w: zero budget disables thinking", ErrEffortConflict)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
"a zero thinking budget disables thinking; the enabling mode or effort was ignored",
))
}
intent.Mode = ModeDisabled
intent.Effort = EffortNone
} else if intent.Mode == ModeDisabled || intent.Effort == EffortNone {
return Intent{}, fmt.Errorf("%w: a non-zero budget enables thinking", ErrEffortConflict)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
fmt.Sprintf("a non-zero thinking budget %d enables thinking; the disabling mode or effort was ignored", budget),
))
intent.Mode = ModeEnabled
intent.Effort = ""
} else if intent.Mode == ModeUnset {
intent.Mode = ModeEnabled
}
@@ -166,108 +197,137 @@ func normalizeIntent(intent Intent) (Intent, error) {
if intent.Effort == EffortNone {
if intent.Mode == ModeEnabled || intent.Mode == ModeAdaptive {
return Intent{}, fmt.Errorf("%w: effort none disables thinking", ErrEffortConflict)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
"effort none disables thinking; the enabling mode was ignored",
))
}
intent.Mode = ModeDisabled
}
return intent, nil
return intent, diagnostics, nil
}
// MergeExplicitAndSuffix combines structured request fields with a model-name
// alias. Contradictions are rejected because the alias may carry a distinct
// billing identity; silently choosing either side would make request semantics
// and accounting disagree.
func MergeExplicitAndSuffix(explicit Intent, suffix Intent, model string) (Intent, error) {
var err error
explicit, err = normalizeIntent(explicit)
// alias or @ modifier. The model name wins wherever the two disagree: it is
// also the billing identity, so upstream behavior and accounting must follow
// it. The alias only overrides what it states explicitly; a bare enable such
// as -thinking keeps the strength supplied by request fields. Every override
// is reported as a suffix_overrode_request diagnostic.
func MergeExplicitAndSuffix(explicit Intent, suffix Intent, model string) (Intent, []types.ConversionDiagnostic, error) {
explicit, diagnostics, err := normalizeIntent(explicit)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
suffix, err = normalizeIntent(suffix)
suffix, suffixDiagnostics, err := normalizeIntent(suffix)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
diagnostics = append(diagnostics, suffixDiagnostics...)
if !explicit.HasStrength() {
if explicit.IncludeThoughts != nil {
suffix.IncludeThoughts = explicit.IncludeThoughts
}
return suffix, nil
return suffix, diagnostics, nil
}
if !suffix.HasStrength() {
if explicit.IncludeThoughts == nil {
explicit.IncludeThoughts = suffix.IncludeThoughts
}
return explicit, nil
}
explicitDisabled := explicit.Mode == ModeDisabled || explicit.Effort == EffortNone
suffixDisabled := suffix.Mode == ModeDisabled || suffix.Effort == EffortNone
if explicitDisabled != suffixDisabled {
return Intent{}, fmt.Errorf("%w for model %q: explicit fields and model suffix disagree about whether thinking is enabled", ErrEffortConflict, model)
}
if !explicitDisabled && explicit.Effort != "" && suffix.Effort != "" && explicit.Effort != suffix.Effort {
return Intent{}, fmt.Errorf("%w for model %q: explicit effort %q differs from suffix effort %q", ErrEffortConflict, model, explicit.Effort, suffix.Effort)
}
if explicit.BudgetTokens != nil && suffix.BudgetTokens != nil && *explicit.BudgetTokens != *suffix.BudgetTokens {
return Intent{}, fmt.Errorf("%w for model %q: explicit budget %d differs from suffix budget %d", ErrEffortConflict, model, *explicit.BudgetTokens, *suffix.BudgetTokens)
}
if (explicit.Effort != "" && explicit.Effort != EffortNone && suffix.BudgetTokens != nil) ||
(explicit.BudgetTokens != nil && suffix.Effort != "" && suffix.Effort != EffortNone) {
return Intent{}, fmt.Errorf("%w for model %q: effort and an exact suffix budget cannot both select reasoning strength", ErrEffortConflict, model)
return explicit, diagnostics, nil
}
merged := suffix
if explicit.Mode != ModeUnset {
merged.Mode = explicit.Mode
}
if explicit.Effort != "" {
merged.Effort = explicit.Effort
}
if explicit.BudgetTokens != nil {
merged.BudgetTokens = explicit.BudgetTokens
merged.BudgetSource = explicit.BudgetSource
}
if explicit.IncludeThoughts != nil {
merged.IncludeThoughts = explicit.IncludeThoughts
}
return normalizeIntent(merged)
explicitDisabled := explicit.Mode == ModeDisabled || explicit.Effort == EffortNone
suffixDisabled := suffix.Mode == ModeDisabled || suffix.Effort == EffortNone
if explicitDisabled != suffixDisabled {
diagnostics = append(diagnostics, reasoningDiagnostic(
"suffix_overrode_request",
fmt.Sprintf("model %q: the model name and request fields disagree about whether thinking is enabled; the model name wins", model),
))
return merged, diagnostics, nil
}
if suffixDisabled {
return merged, diagnostics, nil
}
if merged.Mode == ModeEnabled && explicit.Mode == ModeAdaptive {
merged.Mode = ModeAdaptive
}
if suffix.Effort == "" && suffix.BudgetTokens == nil {
// A bare enable says nothing about strength; request fields supply it.
merged.Effort = explicit.Effort
merged.BudgetTokens = explicit.BudgetTokens
merged.BudgetSource = explicit.BudgetSource
return merged, diagnostics, nil
}
if explicit.Effort != "" && explicit.Effort != suffix.Effort {
diagnostics = append(diagnostics, reasoningDiagnostic(
"suffix_overrode_request",
fmt.Sprintf("model %q: request effort %q was replaced by the reasoning strength in the model name", model, explicit.Effort),
))
}
if explicit.BudgetTokens != nil && (suffix.BudgetTokens == nil || *explicit.BudgetTokens != *suffix.BudgetTokens) {
diagnostics = append(diagnostics, reasoningDiagnostic(
"suffix_overrode_request",
fmt.Sprintf("model %q: request thinking budget %d was replaced by the reasoning strength in the model name", model, *explicit.BudgetTokens),
))
}
return merged, diagnostics, nil
}
// MergeExplicit combines two structured representations of the same request.
// A numeric budget and an effort may coexist: Claude and OpenRouter expose both
// controls, and keeping both is what lets an in-memory OpenAI pivot preserve an
// exact budget for budget-based targets while retaining an effort for
// level-based targets.
func MergeExplicit(primary Intent, secondary Intent, model string) (Intent, error) {
var err error
primary, err = normalizeIntent(primary)
// primary wins wherever they disagree; callers pass the more specific
// representation first. A numeric budget and an effort may coexist: Claude and
// OpenRouter expose both controls, and keeping both is what lets an in-memory
// OpenAI pivot preserve an exact budget for budget-based targets while
// retaining an effort for level-based targets. Disagreements are reported as
// explicit_fields_conflict diagnostics.
func MergeExplicit(primary Intent, secondary Intent, model string) (Intent, []types.ConversionDiagnostic, error) {
primary, diagnostics, err := normalizeIntent(primary)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
secondary, err = normalizeIntent(secondary)
secondary, secondaryDiagnostics, err := normalizeIntent(secondary)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
diagnostics = append(diagnostics, secondaryDiagnostics...)
if primary.IsEmpty() {
return secondary, nil
return secondary, diagnostics, nil
}
if secondary.IsEmpty() {
return primary, nil
return primary, diagnostics, nil
}
primaryDisabled := primary.Mode == ModeDisabled || primary.Effort == EffortNone
secondaryDisabled := secondary.Mode == ModeDisabled || secondary.Effort == EffortNone
if primary.HasStrength() && secondary.HasStrength() && primaryDisabled != secondaryDisabled {
return Intent{}, fmt.Errorf("%w for model %q: explicit fields disagree about whether thinking is enabled", ErrEffortConflict, model)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
fmt.Sprintf("model %q: reasoning fields disagree about whether thinking is enabled; keeping the more specific field", model),
))
if primary.IncludeThoughts == nil {
primary.IncludeThoughts = secondary.IncludeThoughts
}
return primary, diagnostics, nil
}
if primary.Effort != "" && secondary.Effort != "" && primary.Effort != secondary.Effort {
return Intent{}, fmt.Errorf("%w for model %q: explicit efforts %q and %q differ", ErrEffortConflict, model, primary.Effort, secondary.Effort)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
fmt.Sprintf("model %q: reasoning efforts %q and %q differ; keeping %q", model, primary.Effort, secondary.Effort, primary.Effort),
))
}
if primary.BudgetTokens != nil && secondary.BudgetTokens != nil && *primary.BudgetTokens != *secondary.BudgetTokens {
return Intent{}, fmt.Errorf("%w for model %q: explicit budgets %d and %d differ", ErrEffortConflict, model, *primary.BudgetTokens, *secondary.BudgetTokens)
diagnostics = append(diagnostics, reasoningDiagnostic(
"explicit_fields_conflict",
fmt.Sprintf("model %q: thinking budgets %d and %d differ; keeping %d", model, *primary.BudgetTokens, *secondary.BudgetTokens, *primary.BudgetTokens),
))
}
merged := secondary
@@ -284,7 +344,11 @@ func MergeExplicit(primary Intent, secondary Intent, model string) (Intent, erro
if primary.IncludeThoughts != nil {
merged.IncludeThoughts = primary.IncludeThoughts
}
return normalizeIntent(merged)
merged, mergedDiagnostics, err := normalizeIntent(merged)
if err != nil {
return Intent{}, nil, err
}
return merged, append(diagnostics, mergedDiagnostics...), nil
}
type openRouterReasoning struct {
@@ -294,9 +358,9 @@ type openRouterReasoning struct {
Exclude *bool `json:"exclude,omitempty"`
}
func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, []types.ConversionDiagnostic, error) {
if req == nil {
return Intent{}, nil
return Intent{}, nil, nil
}
var intent Intent
@@ -304,7 +368,7 @@ func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
if req.ReasoningEffort != "" {
effort, err := ParseEffort(req.ReasoningEffort)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent.Effort = effort
if effort == EffortNone {
@@ -314,10 +378,11 @@ func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
}
}
var diagnostics []types.ConversionDiagnostic
if len(req.Reasoning) > 0 {
var raw openRouterReasoning
if err := kitutil.Unmarshal(req.Reasoning, &raw); err != nil {
return Intent{}, fmt.Errorf("invalid reasoning config: %w", err)
return Intent{}, nil, fmt.Errorf("invalid reasoning config: %w", err)
}
nested := Intent{BudgetTokens: raw.MaxTokens, Source: SourceExplicit, BudgetSource: SourceExplicit}
if raw.Enabled != nil {
@@ -331,7 +396,7 @@ func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
if raw.Effort != "" {
effort, err := ParseEffort(raw.Effort)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
nested.Effort = effort
if effort == EffortNone {
@@ -344,15 +409,20 @@ func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
include := !*raw.Exclude
nested.IncludeThoughts = &include
}
var err error
intent, err = MergeExplicit(intent, nested, req.Model)
merged, mergeDiagnostics, err := MergeExplicit(intent, nested, req.Model)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent = merged
diagnostics = append(diagnostics, mergeDiagnostics...)
}
if req.ReasoningConversion == nil {
return normalizeIntent(intent)
normalized, normalizeDiagnostics, err := normalizeIntent(intent)
if err != nil {
return Intent{}, nil, err
}
return normalized, append(diagnostics, normalizeDiagnostics...), nil
}
pivot := Intent{
Mode: Mode(req.ReasoningConversion.Mode),
@@ -369,7 +439,11 @@ func FromOpenAIChat(req *dto.GeneralOpenAIRequest) (Intent, error) {
intent.Mode = ModeUnset
}
}
return MergeExplicit(intent, pivot, req.Model)
merged, mergeDiagnostics, err := MergeExplicit(intent, pivot, req.Model)
if err != nil {
return Intent{}, nil, err
}
return merged, append(diagnostics, mergeDiagnostics...), nil
}
// ApplyToOpenAIChat writes the portable portion of an intent to the OpenAI
@@ -379,7 +453,7 @@ func ApplyToOpenAIChat(req *dto.GeneralOpenAIRequest, intent Intent) error {
if req == nil {
return nil
}
intent, err := normalizeIntent(intent)
intent, _, err := normalizeIntent(intent)
if err != nil {
return err
}
@@ -407,7 +481,7 @@ func ApplyToOpenAIResponses(req *dto.OpenAIResponsesRequest, intent Intent) erro
if req == nil {
return nil
}
intent, err := normalizeIntent(intent)
intent, _, err := normalizeIntent(intent)
if err != nil {
return err
}
@@ -436,9 +510,9 @@ func ApplyToOpenAIResponses(req *dto.OpenAIResponsesRequest, intent Intent) erro
return nil
}
func FromOpenAIResponses(req *dto.OpenAIResponsesRequest) (Intent, error) {
func FromOpenAIResponses(req *dto.OpenAIResponsesRequest) (Intent, []types.ConversionDiagnostic, error) {
if req == nil {
return Intent{}, nil
return Intent{}, nil, nil
}
var intent Intent
if req.Reasoning != nil {
@@ -446,7 +520,7 @@ func FromOpenAIResponses(req *dto.OpenAIResponsesRequest) (Intent, error) {
if req.Reasoning.Effort != "" {
effort, err := ParseEffort(req.Reasoning.Effort)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent.Effort = effort
intent.Mode = ModeEnabled
@@ -480,11 +554,16 @@ func FromOpenAIResponses(req *dto.OpenAIResponsesRequest) (Intent, error) {
return MergeExplicit(intent, pivot, req.Model)
}
func FromClaude(req *dto.ClaudeRequest) (Intent, error) {
// FromClaude reads Claude's native thinking and output_config controls. Budget
// bounds are not enforced here: the Claude renderer clamps budgets into
// 1024 <= budget_tokens < max_tokens and reports the adjustment, and other
// targets map the raw budget to an effort.
func FromClaude(req *dto.ClaudeRequest) (Intent, []types.ConversionDiagnostic, error) {
if req == nil {
return Intent{}, nil
return Intent{}, nil, nil
}
var intent Intent
var diagnostics []types.ConversionDiagnostic
intent.Source = SourceNative
if req.Thinking != nil {
switch req.Thinking.Type {
@@ -496,17 +575,16 @@ func FromClaude(req *dto.ClaudeRequest) (Intent, error) {
intent.Mode = ModeDisabled
intent.Effort = EffortNone
default:
return Intent{}, fmt.Errorf("unsupported Claude thinking type %q", req.Thinking.Type)
diagnostics = append(diagnostics, types.ConversionDiagnostic{
Code: "claude_thinking_type_coerced",
Path: "thinking",
Message: fmt.Sprintf("unsupported Claude thinking type %q was treated as enabled", req.Thinking.Type),
Severity: types.ConversionDiagnosticWarning,
})
intent.Mode = ModeEnabled
}
intent.BudgetTokens = req.Thinking.BudgetTokens
if req.Thinking.BudgetTokens != nil {
budget := *req.Thinking.BudgetTokens
if budget < 1024 {
return Intent{}, fmt.Errorf("Claude thinking budget_tokens must be at least 1024, got %d", budget)
}
if req.MaxTokens != nil && uint(budget) >= *req.MaxTokens {
return Intent{}, fmt.Errorf("Claude thinking budget_tokens must be less than max_tokens")
}
intent.BudgetSource = SourceNative
}
switch req.Thinking.Display {
@@ -521,29 +599,37 @@ func FromClaude(req *dto.ClaudeRequest) (Intent, error) {
if len(req.OutputConfig) > 0 {
var output dto.OutputConfigForEffort
if err := kitutil.Unmarshal(req.OutputConfig, &output); err != nil {
return Intent{}, fmt.Errorf("invalid Claude output_config: %w", err)
return Intent{}, nil, fmt.Errorf("invalid Claude output_config: %w", err)
}
if output.Effort != "" {
effort, err := ParseEffort(output.Effort)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent.Effort = effort
}
}
if intent.Mode == ModeDisabled && intent.Effort != "" && intent.Effort != EffortNone {
return intent, nil
return intent, diagnostics, nil
}
return normalizeIntent(intent)
normalized, normalizeDiagnostics, err := normalizeIntent(intent)
if err != nil {
return Intent{}, nil, err
}
return normalized, append(diagnostics, normalizeDiagnostics...), nil
}
func FromGemini(req *dto.GeminiChatRequest) (Intent, error) {
// FromGemini reads a native thinkingConfig. A config carrying both a budget
// and a level is rejected here because the intent alone cannot tell which one
// the target model accepts; NormalizeGeminiThinkingConfig resolves that pair
// first whenever the model's dialect is known.
func FromGemini(req *dto.GeminiChatRequest) (Intent, []types.ConversionDiagnostic, error) {
if req == nil || req.GenerationConfig.ThinkingConfig == nil {
return Intent{}, nil
return Intent{}, nil, nil
}
config := req.GenerationConfig.ThinkingConfig
if config.ThinkingBudget != nil && config.ThinkingLevel != "" {
return Intent{}, fmt.Errorf("%w: Gemini thinkingBudget and thinkingLevel cannot both be set", ErrEffortConflict)
return Intent{}, nil, fmt.Errorf("%w: Gemini thinkingBudget and thinkingLevel cannot both be set", ErrEffortConflict)
}
intent := Intent{
BudgetTokens: config.ThinkingBudget,
@@ -554,16 +640,19 @@ func FromGemini(req *dto.GeminiChatRequest) (Intent, error) {
if config.ThinkingLevel != "" {
effort, err := ParseEffort(config.ThinkingLevel)
if err != nil {
return Intent{}, err
return Intent{}, nil, err
}
intent.Effort = effort
intent.Mode = ModeEnabled
if effort == EffortNone {
intent.Mode = ModeDisabled
}
}
return normalizeIntent(intent)
}
func EffectiveEffort(intent Intent) Effort {
intent, err := normalizeIntent(intent)
intent, _, err := normalizeIntent(intent)
if err != nil {
return ""
}
+133 -30
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -13,16 +14,18 @@ func TestMergeExplicitAndSuffix(t *testing.T) {
budget1024 := 1024
budget2048 := 2048
budget4096 := 4096
tests := []struct {
name string
explicit Intent
suffix Intent
wantErr bool
wantMode Mode
wantEffort Effort
wantBudget *int
wantNoBudget bool
wantThoughts *bool
wantCodes []string
}{
{
name: "enabled plus matching effort merges",
@@ -32,28 +35,62 @@ func TestMergeExplicitAndSuffix(t *testing.T) {
wantEffort: EffortHigh,
},
{
name: "enabled vs disabled conflict",
explicit: Intent{Mode: ModeEnabled, Effort: EffortHigh},
suffix: Intent{Mode: ModeDisabled, Effort: EffortNone, Source: SourceSuffix},
wantErr: true,
name: "suffix disable wins over explicit enable",
explicit: Intent{Mode: ModeEnabled, Effort: EffortHigh},
suffix: Intent{Mode: ModeDisabled, Effort: EffortNone, Source: SourceSuffix},
wantMode: ModeDisabled,
wantEffort: EffortNone,
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "different efforts conflict",
explicit: Intent{Mode: ModeEnabled, Effort: EffortLow},
suffix: Intent{Mode: ModeEnabled, Effort: EffortHigh, Source: SourceSuffix},
wantErr: true,
name: "bare suffix enable wins over explicit disable",
explicit: Intent{Mode: ModeDisabled, Effort: EffortNone},
suffix: Intent{Mode: ModeEnabled, Source: SourceSuffix},
wantMode: ModeEnabled,
wantEffort: "",
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "different budgets conflict",
explicit: Intent{BudgetTokens: &budget1024},
suffix: Intent{BudgetTokens: &budget2048, Source: SourceSuffix, BudgetSource: SourceSuffix},
wantErr: true,
name: "suffix effort wins over explicit effort",
explicit: Intent{Mode: ModeEnabled, Effort: EffortLow},
suffix: Intent{Mode: ModeEnabled, Effort: EffortHigh, Source: SourceSuffix},
wantMode: ModeEnabled,
wantEffort: EffortHigh,
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "effort versus exact suffix budget conflict",
explicit: Intent{Mode: ModeEnabled, Effort: EffortHigh},
suffix: Intent{BudgetTokens: &budget1024, Source: SourceSuffix, BudgetSource: SourceSuffix},
wantErr: true,
name: "suffix budget wins over explicit budget",
explicit: Intent{BudgetTokens: &budget1024},
suffix: Intent{BudgetTokens: &budget2048, Source: SourceSuffix, BudgetSource: SourceSuffix},
wantMode: ModeEnabled,
wantBudget: &budget2048,
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "suffix budget drops explicit effort",
explicit: Intent{Mode: ModeEnabled, Effort: EffortHigh},
suffix: Intent{BudgetTokens: &budget1024, Source: SourceSuffix, BudgetSource: SourceSuffix},
wantMode: ModeEnabled,
wantEffort: "",
wantBudget: &budget1024,
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "suffix effort drops explicit budget",
explicit: Intent{BudgetTokens: &budget4096},
suffix: Intent{Mode: ModeEnabled, Effort: EffortHigh, Source: SourceSuffix},
wantMode: ModeEnabled,
wantEffort: EffortHigh,
wantNoBudget: true,
wantCodes: []string{"suffix_overrode_request"},
},
{
name: "bare suffix enable keeps explicit strength",
explicit: Intent{Mode: ModeEnabled, Effort: EffortLow, BudgetTokens: &budget4096},
suffix: Intent{Mode: ModeEnabled, Source: SourceSuffix},
wantMode: ModeEnabled,
wantEffort: EffortLow,
wantBudget: &budget4096,
},
{
name: "suffix only is adopted",
@@ -75,12 +112,7 @@ func TestMergeExplicitAndSuffix(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := MergeExplicitAndSuffix(tt.explicit, tt.suffix, "claude-opus-4-8")
if tt.wantErr {
require.Error(t, err)
assert.ErrorIs(t, err, ErrEffortConflict)
return
}
got, diagnostics, err := MergeExplicitAndSuffix(tt.explicit, tt.suffix, "claude-opus-4-8")
require.NoError(t, err)
assert.Equal(t, tt.wantMode, got.Mode)
assert.Equal(t, tt.wantEffort, got.Effort)
@@ -88,14 +120,73 @@ func TestMergeExplicitAndSuffix(t *testing.T) {
require.NotNil(t, got.BudgetTokens)
assert.Equal(t, *tt.wantBudget, *got.BudgetTokens)
}
if tt.wantNoBudget {
assert.Nil(t, got.BudgetTokens)
}
if tt.wantThoughts != nil {
require.NotNil(t, got.IncludeThoughts)
assert.Equal(t, *tt.wantThoughts, *got.IncludeThoughts)
}
assert.Equal(t, tt.wantCodes, diagnosticCodes(diagnostics))
})
}
}
func TestFromOpenAIChatResolvesFieldConflictsWithDiagnostics(t *testing.T) {
t.Parallel()
t.Run("reasoning_effort wins over nested effort", func(t *testing.T) {
t.Parallel()
got, diagnostics, err := FromOpenAIChat(&dto.GeneralOpenAIRequest{
ReasoningEffort: "low",
Reasoning: []byte(`{"effort":"high"}`),
})
require.NoError(t, err)
assert.Equal(t, ModeEnabled, got.Mode)
assert.Equal(t, EffortLow, got.Effort)
assert.Equal(t, []string{"explicit_fields_conflict"}, diagnosticCodes(diagnostics))
})
t.Run("zero nested budget disables despite enabled flag", func(t *testing.T) {
t.Parallel()
got, diagnostics, err := FromOpenAIChat(&dto.GeneralOpenAIRequest{
Reasoning: []byte(`{"enabled":true,"max_tokens":0}`),
})
require.NoError(t, err)
assert.Equal(t, ModeDisabled, got.Mode)
assert.Equal(t, EffortNone, got.Effort)
assert.Equal(t, []string{"explicit_fields_conflict"}, diagnosticCodes(diagnostics))
})
t.Run("unknown effort value is still rejected", func(t *testing.T) {
t.Parallel()
_, _, err := FromOpenAIChat(&dto.GeneralOpenAIRequest{ReasoningEffort: "ultra"})
require.ErrorIs(t, err, ErrUnsupportedEffort)
})
}
func TestFromClaudeKeepsOutOfRangeBudgetsForTheRenderer(t *testing.T) {
t.Parallel()
maxTokens := uint(4096)
for _, budget := range []int{512, 4096} {
got, diagnostics, err := FromClaude(&dto.ClaudeRequest{
MaxTokens: &maxTokens,
Thinking: &dto.Thinking{Type: "enabled", BudgetTokens: &budget},
})
require.NoError(t, err)
assert.Equal(t, ModeEnabled, got.Mode)
require.NotNil(t, got.BudgetTokens)
assert.Equal(t, budget, *got.BudgetTokens)
assert.Empty(t, diagnostics)
}
got, diagnostics, err := FromClaude(&dto.ClaudeRequest{Thinking: &dto.Thinking{Type: "auto"}})
require.NoError(t, err)
assert.Equal(t, ModeEnabled, got.Mode)
assert.Equal(t, []string{"claude_thinking_type_coerced"}, diagnosticCodes(diagnostics))
}
func TestIntentStateRoundTrip(t *testing.T) {
t.Parallel()
@@ -125,6 +216,14 @@ func boolPtr(v bool) *bool {
return &v
}
func diagnosticCodes(diagnostics []types.ConversionDiagnostic) []string {
var codes []string
for _, diagnostic := range diagnostics {
codes = append(codes, diagnostic.Code)
}
return codes
}
func TestOpenAIPivotRetainsExactStrengthAndBudget(t *testing.T) {
budget, include := 16384, false
for _, effort := range []Effort{EffortMax, EffortXHigh} {
@@ -133,7 +232,7 @@ func TestOpenAIPivotRetainsExactStrengthAndBudget(t *testing.T) {
chat := &dto.GeneralOpenAIRequest{}
require.NoError(t, ApplyToOpenAIChat(chat, intent))
assert.Equal(t, string(effort), chat.ReasoningEffort)
restored, err := FromOpenAIChat(chat)
restored, _, err := FromOpenAIChat(chat)
require.NoError(t, err)
assert.Equal(t, effort, restored.Effort)
require.NotNil(t, restored.BudgetTokens)
@@ -145,7 +244,7 @@ func TestOpenAIPivotRetainsExactStrengthAndBudget(t *testing.T) {
require.NoError(t, ApplyToOpenAIResponses(responses, restored))
require.NotNil(t, responses.Reasoning)
assert.Equal(t, string(effort), responses.Reasoning.Effort)
restored, err = FromOpenAIResponses(responses)
restored, _, err = FromOpenAIResponses(responses)
require.NoError(t, err)
assert.Equal(t, effort, restored.Effort)
require.NotNil(t, restored.BudgetTokens)
@@ -156,17 +255,21 @@ func TestOpenAIPivotRetainsExactStrengthAndBudget(t *testing.T) {
}
}
func TestOpenAIPivotDoesNotTreatMaxAndXHighAsEquivalent(t *testing.T) {
func TestOpenAIPivotExplicitEffortOverridesPivotWithDiagnostic(t *testing.T) {
intent := Intent{Mode: ModeEnabled, Effort: EffortMax}
chat := &dto.GeneralOpenAIRequest{}
require.NoError(t, ApplyToOpenAIChat(chat, intent))
chat.ReasoningEffort = "xhigh"
_, err := FromOpenAIChat(chat)
require.ErrorIs(t, err, ErrEffortConflict)
got, diagnostics, err := FromOpenAIChat(chat)
require.NoError(t, err)
assert.Equal(t, EffortXHigh, got.Effort)
assert.Equal(t, []string{"explicit_fields_conflict"}, diagnosticCodes(diagnostics))
responses := &dto.OpenAIResponsesRequest{}
require.NoError(t, ApplyToOpenAIResponses(responses, intent))
responses.Reasoning.Effort = "xhigh"
_, err = FromOpenAIResponses(responses)
require.ErrorIs(t, err, ErrEffortConflict)
got, diagnostics, err = FromOpenAIResponses(responses)
require.NoError(t, err)
assert.Equal(t, EffortXHigh, got.Effort)
assert.Equal(t, []string{"explicit_fields_conflict"}, diagnosticCodes(diagnostics))
}