feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s

This commit is contained in:
Codex committed 2026-09-11 05:07:48 +08:00
commit bb7968b612
2381 files changed
+551494

No files matched your search

+233
View File
@@ -0,0 +1,233 @@
// Package reasoning re-exports the pure model-name effort-suffix helpers,
// which moved to the conversion kit (relaykit/relayconvert/reasoning) as part
// of the relaykit extraction. Host code keeps importing this path unchanged.
package reasoning
import (
"strings"
kitreasoning "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
"github.com/QuantumNous/new-api/setting/model_setting"
)
var (
EffortSuffixes = kitreasoning.EffortSuffixes
OpenAIEffortSuffixes = kitreasoning.OpenAIEffortSuffixes
DeepSeekV4EffortSuffixes = kitreasoning.DeepSeekV4EffortSuffixes
)
var (
TrimEffortSuffixWithSuffixes = kitreasoning.TrimEffortSuffixWithSuffixes
ParseDeepSeekV4ThinkingSuffix = kitreasoning.ParseDeepSeekV4ThinkingSuffix
TrimGeminiThinkingSuffix = kitreasoning.TrimGeminiThinkingSuffix
)
// ParseOpenAIReasoningEffortFromModelSuffix applies RelayKit's positive family
// whitelist and the host EffortTailModelIDs escape hatch so real model IDs
// such as gpt-5.1-codex-max remain opaque.
func ParseOpenAIReasoningEffortFromModelSuffix(modelName string) (string, string) {
return kitreasoning.ParseOpenAIReasoningEffortFromModelSuffix(modelName, model_setting.ShouldPreserveEffortTail)
}
// ParseLegacyModelSuffix parses the old naked aliases only for positively
// matched GPT/o-series, Claude, and Gemini model families. The provider prefix
// before the final path segment is kept opaque.
func ParseLegacyModelSuffix(modelName string, allowClaudeThinkingAlias bool, allowGeminiThinkingAlias bool) (string, kitreasoning.Intent, bool, error) {
prefix, bare := splitModelNamespace(modelName)
var (
base string
intent kitreasoning.Intent
found bool
err error
)
switch {
case strings.HasPrefix(bare, "claude-"):
base, intent, found, err = kitreasoning.ParseClaudeModelSuffix(bare, allowClaudeThinkingAlias)
case strings.HasPrefix(bare, "gemini-"):
base, intent, found, err = kitreasoning.ParseGeminiModelSuffix(bare, allowGeminiThinkingAlias)
default:
effort, openAIBase := ParseOpenAIReasoningEffortFromModelSuffix(bare)
if effort == "" {
return modelName, kitreasoning.Intent{}, false, nil
}
parsedEffort, parseErr := kitreasoning.ParseEffort(effort)
if parseErr != nil {
return modelName, kitreasoning.Intent{}, false, parseErr
}
mode := kitreasoning.ModeEnabled
if parsedEffort == kitreasoning.EffortNone {
mode = kitreasoning.ModeDisabled
}
base = openAIBase
intent = kitreasoning.Intent{Mode: mode, Effort: parsedEffort, Source: kitreasoning.SourceSuffix}
found = true
}
if err != nil || !found {
return modelName, kitreasoning.Intent{}, false, err
}
return prefix + base, intent, true, nil
}
// BaseModelName strips explicit model modifiers and any enabled legacy alias.
// Names on the thinking-suffix blacklist stay verbatim, including @ tails.
// Malformed legacy aliases stay intact so request conversion can report the
// precise validation error later.
func BaseModelName(modelName string) string {
if model_setting.ShouldPreserveThinkingSuffix(modelName) {
return modelName
}
base := kitreasoning.ParseModelModifiers(modelName).Base
if model_setting.ShouldPreserveThinkingSuffix(base) {
return base
}
legacyBase, _, found, err := ParseLegacyModelSuffix(
base,
model_setting.GetClaudeSettings().ThinkingAdapterEnabled,
model_setting.GetGeminiSettings().ThinkingAdapterEnabled,
)
if err != nil {
return base
}
if found {
return legacyBase
}
return base
}
func splitModelNamespace(modelName string) (string, string) {
if slash := strings.LastIndex(modelName, "/"); slash >= 0 {
return modelName[:slash+1], modelName[slash+1:]
}
return "", modelName
}
// CanonicalBillingModelNames returns specificity-descending canonical billing
// name candidates (without the raw request name or the bare base). Explicit
// @ modifiers and legacy aliases normalize through the same Intent, so order,
// duplicates, and case do not matter. Temperature and topp never appear.
func CanonicalBillingModelNames(modelName string) []string {
if model_setting.ShouldPreserveThinkingSuffix(modelName) {
return nil
}
spec := kitreasoning.ParseModelModifiers(modelName)
base := spec.Base
intent, hasThinking := billingIntentFromModifiers(spec)
if !model_setting.ShouldPreserveThinkingSuffix(base) {
legacyBase, legacyIntent, found, err := ParseLegacyModelSuffix(
base,
model_setting.GetClaudeSettings().ThinkingAdapterEnabled,
model_setting.GetGeminiSettings().ThinkingAdapterEnabled,
)
if err == nil && found {
base = legacyBase
if !hasThinking {
intent = legacyIntent
hasThinking = true
}
}
}
if !hasThinking {
return nil
}
return canonicalNamesFromIntent(base, intent)
}
func billingIntentFromModifiers(spec kitreasoning.ModelModifierSpec) (kitreasoning.Intent, bool) {
last := make(map[string]int, len(spec.Modifiers))
for index, modifier := range spec.Modifiers {
last[modifier.Key] = index
}
var (
intent kitreasoning.Intent
hasThinking bool
)
for index, modifier := range spec.Modifiers {
if last[modifier.Key] != index {
continue
}
switch modifier.Key {
case "thinking":
parsed, ok := kitreasoning.ParseThinkingModifier(modifier.Value)
if !ok {
continue
}
if hasThinking && parsed.Mode != kitreasoning.ModeDisabled && parsed.BudgetTokens == nil {
intent.Mode = parsed.Mode
intent.Source = kitreasoning.SourceSuffix
} else if hasThinking && parsed.Mode != kitreasoning.ModeDisabled {
intent.Mode = parsed.Mode
intent.BudgetTokens = parsed.BudgetTokens
intent.BudgetSource = parsed.BudgetSource
intent.Source = kitreasoning.SourceSuffix
} else {
intent = parsed
}
hasThinking = true
case "effort":
effort, err := kitreasoning.ParseEffort(modifier.Value)
if err != nil || effort == "" {
continue
}
if effort == kitreasoning.EffortNone {
intent = kitreasoning.Intent{Mode: kitreasoning.ModeDisabled, Effort: kitreasoning.EffortNone, Source: kitreasoning.SourceSuffix}
} else {
if intent.Mode == kitreasoning.ModeUnset || intent.Mode == kitreasoning.ModeDisabled {
intent.Mode = kitreasoning.ModeEnabled
}
intent.Effort = effort
intent.Source = kitreasoning.SourceSuffix
}
hasThinking = true
}
}
return intent, hasThinking
}
func canonicalNamesFromIntent(base string, intent kitreasoning.Intent) []string {
thinking, effort, ok := normalizeBillingThinking(intent)
if !ok || base == "" {
return nil
}
var names []string
if effort != "" {
names = append(names, base+"@effort:"+effort+"@thinking:"+thinking)
}
thinkingForm := base + "@thinking:" + thinking
if thinkingForm != base {
names = append(names, thinkingForm)
}
seen := make(map[string]struct{}, len(names))
out := make([]string, 0, len(names))
for _, name := range names {
if name == "" || name == base {
continue
}
if _, exists := seen[name]; exists {
continue
}
seen[name] = struct{}{}
out = append(out, name)
}
return out
}
func normalizeBillingThinking(intent kitreasoning.Intent) (thinking string, effort string, ok bool) {
if intent.Effort == kitreasoning.EffortNone || intent.Mode == kitreasoning.ModeDisabled {
return "off", "", true
}
if intent.BudgetTokens != nil && *intent.BudgetTokens == 0 {
return "off", "", true
}
if intent.Effort != "" && intent.Effort != kitreasoning.EffortNone {
return "on", strings.ToLower(string(intent.Effort)), true
}
if intent.Mode == kitreasoning.ModeEnabled || intent.Mode == kitreasoning.ModeAdaptive || intent.BudgetTokens != nil {
return "on", "", true
}
return "", "", false
}
+134
View File
@@ -0,0 +1,134 @@
package reasoning
import (
"testing"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCanonicalBillingModelNames(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{
name: "thinking on",
in: "qwen3-max@thinking:on",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "shuffled temperature and thinking",
in: "qwen3-max@temperature:0.2@thinking:on",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "thinking first then temperature",
in: "qwen3-max@thinking:on@temperature:0.2",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "budget normalizes to on",
in: "qwen3-max@thinking:8192",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "minus one normalizes to on",
in: "qwen3-max@thinking:-1",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "adaptive normalizes to on",
in: "qwen3-max@thinking:adaptive",
want: []string{"qwen3-max@thinking:on"},
},
{
name: "thinking off",
in: "qwen3-max@thinking:off",
want: []string{"qwen3-max@thinking:off"},
},
{
name: "effort none becomes thinking off",
in: "qwen3-max@effort:none",
want: []string{"qwen3-max@thinking:off"},
},
{
name: "effort high implies thinking on",
in: "qwen3-max@effort:high",
want: []string{"qwen3-max@effort:high@thinking:on", "qwen3-max@thinking:on"},
},
{
name: "effort and thinking keys sorted",
in: "qwen3-max@thinking:on@effort:high@temperature:0.2",
want: []string{"qwen3-max@effort:high@thinking:on", "qwen3-max@thinking:on"},
},
{
name: "duplicate last wins then normalize",
in: "qwen3-max@thinking:off@thinking:on@effort:low@effort:high",
want: []string{"qwen3-max@effort:high@thinking:on", "qwen3-max@thinking:on"},
},
{
name: "legacy thinking alias",
in: "claude-3-7-sonnet-thinking",
want: []string{"claude-3-7-sonnet@thinking:on"},
},
{
name: "legacy thinking budget matches explicit budget",
in: "gemini-2.5-flash-thinking-8192",
want: []string{"gemini-2.5-flash@thinking:on"},
},
{
name: "legacy nothinking",
in: "claude-3-7-sonnet-nothinking",
want: []string{"claude-3-7-sonnet@thinking:off"},
},
{
name: "temperature only has no reasoning state",
in: "qwen3-max@temperature:0.7",
want: nil,
},
}
geminiSettings := model_setting.GetGeminiSettings()
oldGemini := geminiSettings.ThinkingAdapterEnabled
geminiSettings.ThinkingAdapterEnabled = true
t.Cleanup(func() { geminiSettings.ThinkingAdapterEnabled = oldGemini })
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, CanonicalBillingModelNames(tt.in))
})
}
assert.Equal(t,
CanonicalBillingModelNames("gemini-2.5-flash@thinking:8192"),
CanonicalBillingModelNames("gemini-2.5-flash-thinking-8192"),
)
assert.Equal(t, "gpt-5.1-codex-max", BaseModelName("gpt-5.1-codex-max"))
assert.Empty(t, CanonicalBillingModelNames("gpt-5.1-codex-max"))
}
func TestParseOpenAIReasoningEffortPreservesCodexMax(t *testing.T) {
effort, base := ParseOpenAIReasoningEffortFromModelSuffix("gpt-5.1-codex-max")
assert.Empty(t, effort)
assert.Equal(t, "gpt-5.1-codex-max", base)
}
func TestBaseModelNameStripsModifiers(t *testing.T) {
require.Equal(t, "qwen3-max", BaseModelName("qwen3-max@thinking:on@temperature:0.2"))
}
func TestExemptAtNameIsOpaqueForBillingIdentity(t *testing.T) {
settings := model_setting.GetGlobalSettings()
original := append([]string(nil), settings.ThinkingModelBlacklist...)
t.Cleanup(func() { settings.ThinkingModelBlacklist = original })
settings.ThinkingModelBlacklist = append(original, "re:.*@sha256:.*")
const model = "opaque@sha256:deadbeef"
assert.Equal(t, model, BaseModelName(model))
assert.Empty(t, CanonicalBillingModelNames(model))
assert.Equal(t, "kimi-k2-thinking", BaseModelName("kimi-k2-thinking"))
assert.Empty(t, CanonicalBillingModelNames("kimi-k2-thinking"))
}