Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions internal/thinking/provider/openai/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package openai

import (
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/tidwall/gjson"
)

func TestApplyLevelFallbackAfterDisabledThinking(t *testing.T) {
model := &registry.ModelInfo{
ID: "openrouter-3o",
Thinking: &registry.ThinkingSupport{
Levels: []string{"max", "xhigh", "high", "medium", "low"},
},
}
config, err := thinking.ValidateConfig(
thinking.ThinkingConfig{Mode: thinking.ModeNone}, model, "claude", "openai", false,
)
if err != nil {
t.Fatalf("ValidateConfig() error = %v", err)
}
body, err := NewApplier().Apply(
[]byte(`{"messages":[{"role":"user","content":"hi"}]}`), *config, model,
)
if err != nil {
t.Fatalf("Apply() error = %v", err)
}
if got := gjson.GetBytes(body, "reasoning_effort").String(); got != "low" {
t.Fatalf("reasoning_effort = %q, want low; body=%s", got, body)
}
}
26 changes: 19 additions & 7 deletions internal/thinking/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,30 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
config.Budget = 0
config.Level = ""
} else {
switch config.Mode {
case ModeBudget, ModeAuto, ModeNone:
config.Budget = clampBudget(config.Budget, modelInfo, toFormat)
}

// ModeNone for a model that cannot be disabled falls back to the lowest
// supported level. Budget-capable models reach this path with Budget > 0;
// level-only models need the capability flags checked explicitly because
// their Min/Max range is zero.
cannotDisableLevelModel := !support.ZeroAllowed && !isLevelSupported(string(LevelNone), support.Levels)
if config.Mode == ModeNone && len(support.Levels) > 0 && (config.Budget > 0 || cannotDisableLevelModel) {
config.Level = ThinkingLevel(support.Levels[0])
pureLevelOnly := capability == CapabilityLevelOnly && support.Min == 0 && support.Max == 0
if config.Mode == ModeNone && pureLevelOnly {
// Level-only models have no numeric budget range, so do not pass their
// disabled sentinel through the numeric budget clamp. If the target
// cannot disable thinking, resolve its lowest supported level from the
// canonical order; provider metadata is not guaranteed to be sorted.
config.Budget = 0
if cannotDisableLevelModel {
config.Mode = ModeLevel
config.Level = clampLevel(LevelMinimal, modelInfo, toFormat)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve provider-specific supported fallback levels

When a configured level-only model advertises only a provider-specific effort such as Levels: []string{"ultra"}, clampLevel(LevelMinimal, ...) ignores every unrecognized level and returns minimal, even though that value is not supported. Because this fallback is generated after level validation, the provider applier sends the invalid effort upstream; preserve an advertised fallback or validate the generated level before returning.

AGENTS.md reference: AGENTS.md:L25-L29

Useful? React with 👍 / 👎.

}
} else {
switch config.Mode {
case ModeBudget, ModeAuto, ModeNone:
config.Budget = clampBudget(config.Budget, modelInfo, toFormat)
}
if config.Mode == ModeNone && len(support.Levels) > 0 && (config.Budget > 0 || cannotDisableLevelModel) {
config.Level = ThinkingLevel(support.Levels[0])
}
}
}

Expand Down
84 changes: 84 additions & 0 deletions internal/thinking/validate_none_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package thinking

import (
"bytes"
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
log "github.com/sirupsen/logrus"
)

func TestValidateConfigDisabledLevelOnlyUsesLowestWithoutBudgetWarning(t *testing.T) {
previousOutput := log.StandardLogger().Out
previousLevel := log.GetLevel()
var output bytes.Buffer
log.SetOutput(&output)
log.SetLevel(log.WarnLevel)
t.Cleanup(func() {
log.SetOutput(previousOutput)
log.SetLevel(previousLevel)
})

model := &registry.ModelInfo{
ID: "openrouter-3o",
Thinking: &registry.ThinkingSupport{
Levels: []string{"max", "xhigh", "high", "medium", "low"},
},
}
got, err := ValidateConfig(ThinkingConfig{Mode: ModeNone}, model, "claude", "openai", false)
if err != nil {
t.Fatalf("ValidateConfig() error = %v", err)
}
if got.Mode != ModeLevel || got.Budget != 0 || got.Level != LevelLow {
t.Fatalf("ValidateConfig() = %+v, want ModeLevel low with zero budget", got)
}
if strings.Contains(output.String(), "budget zero not allowed") {
t.Fatalf("disabled level-only thinking emitted a misleading warning: %s", output.String())
}
}

func TestValidateConfigDisabledHybridKeepsBudgetFallback(t *testing.T) {
model := &registry.ModelInfo{
ID: "hybrid-model",
Thinking: &registry.ThinkingSupport{
Min: 1024, Max: 32768, Levels: []string{"low", "high"},
},
}
got, err := ValidateConfig(ThinkingConfig{Mode: ModeNone}, model, "claude", "gemini", false)
if err != nil {
t.Fatalf("ValidateConfig() error = %v", err)
}
if got.Mode != ModeNone || got.Budget != 1024 || got.Level != LevelLow {
t.Fatalf("ValidateConfig() = %+v, want prior hybrid fallback budget 1024 and level low", got)
}
}

func TestValidateConfigDisabledLevelOnlyKeepsSupportedNoneWithoutBudgetWarning(t *testing.T) {
previousOutput := log.StandardLogger().Out
previousLevel := log.GetLevel()
var output bytes.Buffer
log.SetOutput(&output)
log.SetLevel(log.WarnLevel)
t.Cleanup(func() {
log.SetOutput(previousOutput)
log.SetLevel(previousLevel)
})

model := &registry.ModelInfo{
ID: "level-model-with-none",
Thinking: &registry.ThinkingSupport{
Levels: []string{"high", "none", "low"},
},
}
got, err := ValidateConfig(ThinkingConfig{Mode: ModeNone}, model, "claude", "openai", false)
if err != nil {
t.Fatalf("ValidateConfig() error = %v", err)
}
if got.Mode != ModeNone || got.Budget != 0 || got.Level != "" {
t.Fatalf("ValidateConfig() = %+v, want unchanged disabled mode", got)
}
if strings.Contains(output.String(), "budget zero not allowed") {
t.Fatalf("supported disabled thinking emitted a misleading warning: %s", output.String())
}
}
Loading