Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
233 lines
7.6 KiB
Go
233 lines
7.6 KiB
Go
package helper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
|
|
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/QuantumNous/new-api/setting/model_setting"
|
|
hostreasoning "github.com/QuantumNous/new-api/setting/reasoning"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ApplyReasoningModelSuffix parses host-private reasoning suffixes from the
|
|
// origin and mapped model names, attaches the resulting intent to RelayInfo,
|
|
// and normalizes UpstreamModelName to the unsuffixed base. Optional outbound
|
|
// requests are the DeepCopy the handler will send upstream; they must be
|
|
// synced here because info.Request is the original, not that copy. Explicit
|
|
// model modifiers override request fields; mapped-model modifiers override
|
|
// origin-model modifiers. Pass-through (global or channel) is a no-op so the
|
|
// request body stays byte-identical. c is used only to correlate diagnostics.
|
|
func ApplyReasoningModelSuffix(c *gin.Context, info *relaycommon.RelayInfo, outbound ...dto.Request) error {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
if model_setting.GetGlobalSettings().PassThroughRequestEnabled ||
|
|
info.ChannelMeta != nil && info.ChannelSetting.PassThroughBodyEnabled {
|
|
return nil
|
|
}
|
|
|
|
opts := info.ConvOptions()
|
|
origin := info.GetOriginModelName()
|
|
upstream := ""
|
|
if info.ChannelMeta != nil {
|
|
upstream = info.UpstreamModelName
|
|
}
|
|
originParsed, err := parseRequestModelName(origin, opts)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
upstreamParsed := originParsed
|
|
if upstream != origin {
|
|
upstreamParsed, err = parseRequestModelName(upstream, opts)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
}
|
|
diagnostics := append([]types.ConversionDiagnostic(nil), originParsed.diagnostics...)
|
|
if upstream != origin {
|
|
diagnostics = append(diagnostics, upstreamParsed.diagnostics...)
|
|
}
|
|
|
|
selected := originParsed
|
|
if upstream != origin {
|
|
selected, diagnostics = overlayMappedModelModifiers(selected, upstreamParsed, diagnostics)
|
|
}
|
|
|
|
if selected.hasThinking {
|
|
explicit, explicitDiagnostics, err := explicitIntentFromRequest(info.Request)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
diagnostics = append(diagnostics, explicitDiagnostics...)
|
|
diagnostics = append(diagnostics, modifierRequestOverrideDiagnostics(explicit, selected.intent)...)
|
|
if selected.intent.IncludeThoughts == nil {
|
|
selected.intent.IncludeThoughts = explicit.IncludeThoughts
|
|
}
|
|
info.ReasoningConversion = reasoning.StateFromIntent(selected.intent)
|
|
}
|
|
if info.ChannelMeta != nil {
|
|
info.UpstreamModelName = selected.base
|
|
}
|
|
|
|
if selected.hasTemperature {
|
|
if current, exists := extractTemperature(info.Request); exists && *selected.temperature != current {
|
|
diagnostics = append(diagnostics, modelModifierDiagnostic(
|
|
"model_modifier_overrode_request",
|
|
"temperature",
|
|
fmt.Sprintf("model temperature modifier overrides request temperature %v", current),
|
|
))
|
|
}
|
|
}
|
|
if selected.hasTopP {
|
|
if current, exists := extractTopP(info.Request); exists && *selected.topP != current {
|
|
diagnostics = append(diagnostics, modelModifierDiagnostic(
|
|
"model_modifier_overrode_request",
|
|
"topp",
|
|
fmt.Sprintf("model topp modifier overrides request top_p %v", current),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Handlers DeepCopy before this helper; info.Request is the original.
|
|
// Sync every outbound copy the caller is about to send upstream.
|
|
for _, outbound := range outbound {
|
|
if outbound == nil {
|
|
continue
|
|
}
|
|
if err := applyModelControls(outbound, selected); err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
outbound.SetModelName(info.UpstreamModelName)
|
|
}
|
|
if info.Request != nil {
|
|
info.Request.SetModelName(info.UpstreamModelName)
|
|
}
|
|
if selected.hasThinking {
|
|
info.SetReasoningEffort(string(reasoning.EffectiveEffort(selected.intent)))
|
|
}
|
|
for i := range diagnostics {
|
|
diagnostics[i].From = info.RelayFormat
|
|
}
|
|
diagnosticContext := context.Background()
|
|
if c != nil {
|
|
diagnosticContext = c
|
|
}
|
|
info.RecordConversionDiagnostics(diagnosticContext, diagnostics)
|
|
return nil
|
|
}
|
|
|
|
func parseRequestModelName(name string, opts *convmeta.Options) (parsedModelModifiers, error) {
|
|
if opts.ShouldPreserveThinkingSuffix(name) {
|
|
return parsedModelModifiers{base: name}, nil
|
|
}
|
|
parsed, err := parseExplicitModelModifiers(name)
|
|
if err != nil {
|
|
return parsedModelModifiers{}, err
|
|
}
|
|
if opts.ShouldPreserveThinkingSuffix(parsed.base) {
|
|
return parsed, nil
|
|
}
|
|
legacyBase, legacyIntent, legacyFound, err := parseHostModelSuffix(parsed.base, opts)
|
|
if err != nil {
|
|
return parsedModelModifiers{}, err
|
|
}
|
|
parsed.base = legacyBase
|
|
if legacyFound && !parsed.hasThinking {
|
|
parsed.intent = legacyIntent
|
|
parsed.hasThinking = true
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func overlayMappedModelModifiers(origin parsedModelModifiers, mapped parsedModelModifiers, diagnostics []types.ConversionDiagnostic) (parsedModelModifiers, []types.ConversionDiagnostic) {
|
|
origin.base = mapped.base
|
|
origin.hasSyntax = origin.hasSyntax || mapped.hasSyntax
|
|
origin.diagnostics = nil
|
|
if mapped.hasThinking {
|
|
if origin.hasThinking && !sameModifierIntent(origin.intent, mapped.intent) {
|
|
diagnostics = append(diagnostics, modelModifierDiagnostic(
|
|
"mapped_model_modifier_override",
|
|
"thinking",
|
|
"mapped-model thinking modifier overrides the origin-model modifier",
|
|
))
|
|
}
|
|
origin.intent = mapped.intent
|
|
origin.hasThinking = true
|
|
}
|
|
if mapped.hasTemperature {
|
|
if origin.hasTemperature && *origin.temperature != *mapped.temperature {
|
|
diagnostics = append(diagnostics, modelModifierDiagnostic(
|
|
"mapped_model_modifier_override",
|
|
"temperature",
|
|
"mapped-model temperature modifier overrides the origin-model modifier",
|
|
))
|
|
}
|
|
origin.temperature = mapped.temperature
|
|
origin.hasTemperature = true
|
|
}
|
|
if mapped.hasTopP {
|
|
if origin.hasTopP && *origin.topP != *mapped.topP {
|
|
diagnostics = append(diagnostics, modelModifierDiagnostic(
|
|
"mapped_model_modifier_override",
|
|
"topp",
|
|
"mapped-model topp modifier overrides the origin-model modifier",
|
|
))
|
|
}
|
|
origin.topP = mapped.topP
|
|
origin.hasTopP = true
|
|
}
|
|
return origin, diagnostics
|
|
}
|
|
|
|
func modifierRequestOverrideDiagnostics(explicit reasoning.Intent, modifier reasoning.Intent) []types.ConversionDiagnostic {
|
|
if !explicit.HasStrength() || sameModifierIntent(explicit, modifier) {
|
|
return nil
|
|
}
|
|
return []types.ConversionDiagnostic{modelModifierDiagnostic(
|
|
"model_modifier_overrode_request",
|
|
"thinking",
|
|
"model thinking modifier overrides structured request reasoning fields",
|
|
)}
|
|
}
|
|
|
|
func sameModifierIntent(left reasoning.Intent, right reasoning.Intent) bool {
|
|
if left.Mode != right.Mode || left.Effort != right.Effort {
|
|
return false
|
|
}
|
|
if left.BudgetTokens == nil || right.BudgetTokens == nil {
|
|
return left.BudgetTokens == nil && right.BudgetTokens == nil
|
|
}
|
|
return *left.BudgetTokens == *right.BudgetTokens
|
|
}
|
|
|
|
func parseHostModelSuffix(name string, opts *convmeta.Options) (string, reasoning.Intent, bool, error) {
|
|
if name == "" {
|
|
return name, reasoning.Intent{}, false, nil
|
|
}
|
|
return hostreasoning.ParseLegacyModelSuffix(
|
|
name,
|
|
opts.Claude.ThinkingAdapterEnabled,
|
|
opts.Gemini.ThinkingAdapterEnabled,
|
|
)
|
|
}
|
|
|
|
func explicitIntentFromRequest(req dto.Request) (reasoning.Intent, []types.ConversionDiagnostic, error) {
|
|
switch r := req.(type) {
|
|
case *dto.ClaudeRequest:
|
|
return reasoning.FromClaude(r)
|
|
case *dto.GeminiChatRequest:
|
|
return reasoning.FromGemini(r)
|
|
case *dto.GeneralOpenAIRequest:
|
|
return reasoning.FromOpenAIChat(r)
|
|
case *dto.OpenAIResponsesRequest:
|
|
return reasoning.FromOpenAIResponses(r)
|
|
default:
|
|
return reasoning.Intent{}, nil, nil
|
|
}
|
|
}
|