mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 06:30:21 +00:00
* feat(relaykit): preserve hosted tools across conversions - add protocol-neutral hosted-tool DTOs, conversion metadata, and loss policies - bridge citations, grounding metadata, and hosted-tool stream lifecycles - document the public conversion behavior and channel policy controls * refactor(relaykit): normalize reasoning and thinking intent - centralize provider-neutral reasoning intent, effort, and budget mappings - parse model suffixes at the host entry boundary while preserving provider-owned tails - keep adaptive Claude thinking and explicit zero-token compatibility consistent * fix(billing): preserve authoritative usage across relay hops - carry native BillingUsage sidecars through direct and streamed protocol bridges - merge partial and terminal usage monotonically with safe fallback settlement - retain cache metadata, penultimate usage, and per-call Gemini tool surcharges * feat(relay): bridge Responses with Claude and Gemini protocols - add direct request, response, and stream converters across supported relay formats - expose Claude count_tokens and Chat-to-Responses compatibility endpoints - carry conversion diagnostics through the host while retaining the curated public goldens * fix(relay): wire relaykit conversions into host channels - connect handlers, adaptors, and channel settings to the standalone conversion layer - keep model mapping, pricing identity, retries, and provider-specific suffix behavior aligned - ignore local audit artifacts and retain focused public regression coverage
146 lines
4.8 KiB
Go
146 lines
4.8 KiB
Go
package helper
|
|
|
|
import (
|
|
"strings"
|
|
|
|
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/setting/model_setting"
|
|
)
|
|
|
|
// 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. Conflict
|
|
// between an explicit request field and a suffix is a client error.
|
|
func ApplyReasoningModelSuffix(info *relaycommon.RelayInfo, outbound ...dto.Request) error {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
passThrough := model_setting.GetGlobalSettings().PassThroughRequestEnabled
|
|
if info.ChannelMeta != nil && info.ChannelSetting.PassThroughBodyEnabled {
|
|
passThrough = true
|
|
}
|
|
if passThrough {
|
|
return nil
|
|
}
|
|
|
|
opts := info.ConvOptions()
|
|
origin := info.GetOriginModelName()
|
|
upstream := ""
|
|
if info.ChannelMeta != nil {
|
|
upstream = info.UpstreamModelName
|
|
}
|
|
if opts.ShouldPreserveThinkingSuffix(origin) || opts.ShouldPreserveThinkingSuffix(upstream) {
|
|
return nil
|
|
}
|
|
|
|
originBase, originIntent, originFound, err := parseHostModelSuffix(origin, opts)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
upstreamBase, upstreamIntent, upstreamFound, err := parseHostModelSuffix(upstream, opts)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
|
|
suffix := originIntent
|
|
if originFound && upstreamFound {
|
|
suffix, err = reasoning.MergeExplicitAndSuffix(originIntent, upstreamIntent, origin)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
} else if upstreamFound {
|
|
suffix = upstreamIntent
|
|
}
|
|
|
|
explicit, err := explicitIntentFromRequest(info.Request)
|
|
if err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
conflictModel := upstream
|
|
if conflictModel == "" {
|
|
conflictModel = origin
|
|
}
|
|
if _, err = reasoning.MergeExplicitAndSuffix(explicit, suffix, conflictModel); err != nil {
|
|
return reasoning.AsClientError(err)
|
|
}
|
|
|
|
if !suffix.IsEmpty() {
|
|
info.ReasoningConversion = reasoning.StateFromIntent(suffix)
|
|
}
|
|
|
|
if upstreamFound && info.ChannelMeta != nil {
|
|
info.UpstreamModelName = upstreamBase
|
|
} else if !info.IsModelMapped && originFound && info.ChannelMeta != nil {
|
|
info.UpstreamModelName = originBase
|
|
}
|
|
// 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 {
|
|
outbound.SetModelName(info.UpstreamModelName)
|
|
}
|
|
}
|
|
if info.Request != nil {
|
|
info.Request.SetModelName(info.UpstreamModelName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseHostModelSuffix(name string, opts *convmeta.Options) (string, reasoning.Intent, bool, error) {
|
|
if name == "" {
|
|
return name, reasoning.Intent{}, false, nil
|
|
}
|
|
if strings.HasPrefix(name, "claude-") {
|
|
return reasoning.ParseClaudeModelSuffix(name, opts.Claude.ThinkingAdapterEnabled)
|
|
}
|
|
if strings.HasPrefix(name, "gemini-") {
|
|
if !opts.Gemini.ThinkingAdapterEnabled {
|
|
return name, reasoning.Intent{}, false, nil
|
|
}
|
|
return reasoning.ParseGeminiModelSuffix(name, true)
|
|
}
|
|
// deepseek-v4 effort tails are consumed by ParseDeepSeekV4ThinkingSuffix
|
|
// in the DeepSeek adaptor; stripping them here drops THINKING+effort.
|
|
if strings.HasPrefix(name, "deepseek-v4-") {
|
|
return name, reasoning.Intent{}, false, nil
|
|
}
|
|
effort, base := reasoning.ParseOpenAIReasoningEffortFromModelSuffix(name, opts.PreserveEffortTail)
|
|
if effort != "" {
|
|
parsed, err := reasoning.ParseEffort(effort)
|
|
if err != nil {
|
|
return name, reasoning.Intent{}, false, err
|
|
}
|
|
mode := reasoning.ModeEnabled
|
|
if parsed == reasoning.EffortNone {
|
|
mode = reasoning.ModeDisabled
|
|
}
|
|
return base, reasoning.Intent{Mode: mode, Effort: parsed, Source: reasoning.SourceSuffix}, true, nil
|
|
}
|
|
// Generic -thinking trim is OpenRouter-only. Volcengine/DeepSeek adaptors
|
|
// read the suffix off UpstreamModelName themselves.
|
|
if opts != nil && opts.OpenRouterDialect && strings.HasSuffix(name, "-thinking") {
|
|
return strings.TrimSuffix(name, "-thinking"), reasoning.Intent{Mode: reasoning.ModeEnabled, Source: reasoning.SourceSuffix}, true, nil
|
|
}
|
|
return name, reasoning.Intent{}, false, nil
|
|
}
|
|
|
|
func explicitIntentFromRequest(req dto.Request) (reasoning.Intent, 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
|
|
}
|
|
}
|