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
279 lines
8.4 KiB
Go
279 lines
8.4 KiB
Go
package helper
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
)
|
|
|
|
type parsedModelModifiers struct {
|
|
base string
|
|
hasSyntax bool
|
|
intent reasoning.Intent
|
|
hasThinking bool
|
|
temperature *float64
|
|
topP *float64
|
|
hasTemperature bool
|
|
hasTopP bool
|
|
diagnostics []types.ConversionDiagnostic
|
|
}
|
|
|
|
const modelModifierExemptionHint = `If this segment is part of the real model name, add the model to the "Models that skip thinking suffix processing" setting (re: regex entries are supported)`
|
|
|
|
func modelModifierClientError(message string) error {
|
|
return fmt.Errorf("%s. %s", message, modelModifierExemptionHint)
|
|
}
|
|
|
|
func parseExplicitModelModifiers(modelName string) (parsedModelModifiers, error) {
|
|
spec := reasoning.ParseModelModifiers(modelName)
|
|
parsed := parsedModelModifiers{base: spec.Base, hasSyntax: spec.HasModifiers()}
|
|
last := make(map[string]int, len(spec.Modifiers))
|
|
|
|
for index, modifier := range spec.Modifiers {
|
|
if _, duplicate := last[modifier.Key]; duplicate {
|
|
parsed.diagnostics = append(parsed.diagnostics, modelModifierDiagnostic(
|
|
"duplicate_model_modifier",
|
|
modifier.Key,
|
|
fmt.Sprintf("model modifier %q is repeated; the rightmost value is used", modifier.Key),
|
|
))
|
|
}
|
|
last[modifier.Key] = index
|
|
}
|
|
|
|
for index, modifier := range spec.Modifiers {
|
|
if last[modifier.Key] != index {
|
|
continue
|
|
}
|
|
switch modifier.Key {
|
|
case "thinking":
|
|
intent, ok := reasoning.ParseThinkingModifier(modifier.Value)
|
|
if !ok {
|
|
return parsedModelModifiers{}, modelModifierClientError(
|
|
fmt.Sprintf("invalid thinking modifier value %q", modifier.Value),
|
|
)
|
|
}
|
|
if parsed.hasThinking && intent.Mode != reasoning.ModeDisabled && intent.BudgetTokens == nil {
|
|
parsed.intent.Mode = intent.Mode
|
|
parsed.intent.Source = reasoning.SourceSuffix
|
|
} else if parsed.hasThinking && intent.Mode != reasoning.ModeDisabled {
|
|
parsed.intent.Mode = intent.Mode
|
|
parsed.intent.BudgetTokens = intent.BudgetTokens
|
|
parsed.intent.BudgetSource = intent.BudgetSource
|
|
parsed.intent.Source = reasoning.SourceSuffix
|
|
} else {
|
|
parsed.intent = intent
|
|
}
|
|
parsed.hasThinking = true
|
|
case "effort":
|
|
effort, err := reasoning.ParseEffort(modifier.Value)
|
|
if err != nil || effort == "" {
|
|
return parsedModelModifiers{}, modelModifierClientError(
|
|
fmt.Sprintf("invalid effort modifier value %q: must be one of none/low/medium/high/xhigh/max", modifier.Value),
|
|
)
|
|
}
|
|
if effort == reasoning.EffortNone {
|
|
parsed.intent = reasoning.Intent{Mode: reasoning.ModeDisabled, Effort: reasoning.EffortNone, Source: reasoning.SourceSuffix}
|
|
} else {
|
|
if parsed.intent.Mode == reasoning.ModeUnset || parsed.intent.Mode == reasoning.ModeDisabled {
|
|
parsed.intent.Mode = reasoning.ModeEnabled
|
|
}
|
|
parsed.intent.Effort = effort
|
|
parsed.intent.Source = reasoning.SourceSuffix
|
|
}
|
|
parsed.hasThinking = true
|
|
case "temperature":
|
|
value, ok := parseFiniteFloat(modifier.Value)
|
|
if !ok {
|
|
return parsedModelModifiers{}, modelModifierClientError(
|
|
fmt.Sprintf("invalid temperature modifier value %q: must be a finite number", modifier.Value),
|
|
)
|
|
}
|
|
parsed.temperature = &value
|
|
parsed.hasTemperature = true
|
|
case "topp":
|
|
value, ok := parseFiniteFloat(modifier.Value)
|
|
if !ok {
|
|
return parsedModelModifiers{}, modelModifierClientError(
|
|
fmt.Sprintf("invalid topp modifier value %q: must be a finite number", modifier.Value),
|
|
)
|
|
}
|
|
parsed.topP = &value
|
|
parsed.hasTopP = true
|
|
default:
|
|
return parsedModelModifiers{}, modelModifierClientError(
|
|
fmt.Sprintf("unsupported model modifier %q", modifier.Key),
|
|
)
|
|
}
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func parseFiniteFloat(raw string) (float64, bool) {
|
|
value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
|
|
if err != nil || math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return 0, false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func extractTemperature(req dto.Request) (float64, bool) {
|
|
switch request := req.(type) {
|
|
case *dto.GeneralOpenAIRequest:
|
|
if request != nil && request.Temperature != nil {
|
|
return *request.Temperature, true
|
|
}
|
|
case *dto.OpenAIResponsesRequest:
|
|
if request != nil && request.Temperature != nil {
|
|
return *request.Temperature, true
|
|
}
|
|
case *dto.ClaudeRequest:
|
|
if request != nil && request.Temperature != nil {
|
|
return *request.Temperature, true
|
|
}
|
|
case *dto.GeminiChatRequest:
|
|
if request != nil && request.GenerationConfig.Temperature != nil {
|
|
return *request.GenerationConfig.Temperature, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func extractTopP(req dto.Request) (float64, bool) {
|
|
switch request := req.(type) {
|
|
case *dto.GeneralOpenAIRequest:
|
|
if request != nil && request.TopP != nil {
|
|
return *request.TopP, true
|
|
}
|
|
case *dto.OpenAIResponsesRequest:
|
|
if request != nil && request.TopP != nil {
|
|
return *request.TopP, true
|
|
}
|
|
case *dto.ClaudeRequest:
|
|
if request != nil && request.TopP != nil {
|
|
return *request.TopP, true
|
|
}
|
|
case *dto.GeminiChatRequest:
|
|
if request != nil && request.GenerationConfig.TopP != nil {
|
|
return *request.GenerationConfig.TopP, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func modelModifierDiagnostic(code string, key string, message string) types.ConversionDiagnostic {
|
|
return types.ConversionDiagnostic{
|
|
Code: code,
|
|
Path: "model.@" + key,
|
|
Message: message,
|
|
Severity: types.ConversionDiagnosticWarning,
|
|
}
|
|
}
|
|
|
|
func applyModelControls(req dto.Request, parsed parsedModelModifiers) error {
|
|
if req == nil {
|
|
return nil
|
|
}
|
|
|
|
switch request := req.(type) {
|
|
case *dto.GeneralOpenAIRequest:
|
|
if parsed.hasTemperature {
|
|
request.Temperature = parsed.temperature
|
|
}
|
|
if parsed.hasTopP {
|
|
request.TopP = parsed.topP
|
|
}
|
|
if parsed.hasThinking {
|
|
request.ReasoningConversion = reasoning.StateFromIntent(parsed.intent)
|
|
reasoningConfig := make(map[string]any)
|
|
if len(request.Reasoning) > 0 {
|
|
if common.GetJsonType(request.Reasoning) != "object" {
|
|
return fmt.Errorf("OpenAI reasoning must be a JSON object")
|
|
}
|
|
if err := common.Unmarshal(request.Reasoning, &reasoningConfig); err != nil {
|
|
return fmt.Errorf("invalid OpenAI reasoning config: %w", err)
|
|
}
|
|
}
|
|
if parsed.intent.BudgetTokens != nil {
|
|
reasoningConfig["enabled"] = parsed.intent.Mode != reasoning.ModeDisabled
|
|
reasoningConfig["max_tokens"] = *parsed.intent.BudgetTokens
|
|
delete(reasoningConfig, "effort")
|
|
request.ReasoningEffort = ""
|
|
} else {
|
|
delete(reasoningConfig, "enabled")
|
|
delete(reasoningConfig, "effort")
|
|
delete(reasoningConfig, "max_tokens")
|
|
request.ReasoningEffort = ""
|
|
if parsed.intent.Effort != "" {
|
|
request.ReasoningEffort = string(parsed.intent.Effort)
|
|
}
|
|
}
|
|
if len(reasoningConfig) == 0 {
|
|
request.Reasoning = nil
|
|
} else {
|
|
encoded, err := common.Marshal(reasoningConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Reasoning = encoded
|
|
}
|
|
}
|
|
case *dto.OpenAIResponsesRequest:
|
|
if parsed.hasTemperature {
|
|
request.Temperature = parsed.temperature
|
|
}
|
|
if parsed.hasTopP {
|
|
request.TopP = parsed.topP
|
|
}
|
|
if parsed.hasThinking {
|
|
request.ReasoningConversion = reasoning.StateFromIntent(parsed.intent)
|
|
if parsed.intent.Effort != "" {
|
|
if request.Reasoning == nil {
|
|
request.Reasoning = &dto.Reasoning{}
|
|
}
|
|
request.Reasoning.Effort = string(parsed.intent.Effort)
|
|
} else if request.Reasoning != nil && parsed.intent.BudgetTokens == nil {
|
|
request.Reasoning.Effort = ""
|
|
}
|
|
}
|
|
case *dto.ClaudeRequest:
|
|
if parsed.hasTemperature {
|
|
request.Temperature = parsed.temperature
|
|
}
|
|
if parsed.hasTopP {
|
|
request.TopP = parsed.topP
|
|
}
|
|
if parsed.hasThinking {
|
|
request.Thinking = nil
|
|
if len(request.OutputConfig) > 0 && common.GetJsonType(request.OutputConfig) == "object" {
|
|
var output map[string]any
|
|
if err := common.Unmarshal(request.OutputConfig, &output); err == nil {
|
|
delete(output, "effort")
|
|
encoded, err := common.Marshal(output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.OutputConfig = encoded
|
|
}
|
|
}
|
|
}
|
|
case *dto.GeminiChatRequest:
|
|
if parsed.hasTemperature {
|
|
request.GenerationConfig.Temperature = parsed.temperature
|
|
}
|
|
if parsed.hasTopP {
|
|
request.GenerationConfig.TopP = parsed.topP
|
|
}
|
|
if parsed.hasThinking {
|
|
request.GenerationConfig.ThinkingConfig = nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|