mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
refactor(thinking): export thinking helpers
Expose thinking/effort normalization helpers from the executor package so conversion tests use production code and stay aligned with runtime validation behavior.
This commit is contained in:
@@ -322,7 +322,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
||||||
payload = applyThinkingMetadata(payload, req.Metadata, req.Model)
|
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
||||||
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
||||||
payload = util.ConvertThinkingLevelToBudget(payload)
|
payload = util.ConvertThinkingLevelToBudget(payload)
|
||||||
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
body = applyPayloadConfig(e.cfg, req.Model, body)
|
body = applyPayloadConfig(e.cfg, req.Model, body)
|
||||||
@@ -152,9 +152,9 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return nil, errValidate
|
return nil, errValidate
|
||||||
}
|
}
|
||||||
body = applyPayloadConfig(e.cfg, req.Model, body)
|
body = applyPayloadConfig(e.cfg, req.Model, body)
|
||||||
@@ -254,7 +254,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
modelForCounting := req.Model
|
modelForCounting := req.Model
|
||||||
|
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false)
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
body, _ = sjson.SetBytes(body, "stream", false)
|
body, _ = sjson.SetBytes(body, "stream", false)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
body = applyThinkingMetadata(body, req.Metadata, req.Model)
|
body = ApplyThinkingMetadata(body, req.Metadata, req.Model)
|
||||||
body = util.ApplyDefaultThinkingIfNeeded(req.Model, body)
|
body = util.ApplyDefaultThinkingIfNeeded(req.Model, body)
|
||||||
body = util.NormalizeGeminiThinkingBudget(req.Model, body)
|
body = util.NormalizeGeminiThinkingBudget(req.Model, body)
|
||||||
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
||||||
@@ -178,7 +178,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
body = applyThinkingMetadata(body, req.Metadata, req.Model)
|
body = ApplyThinkingMetadata(body, req.Metadata, req.Model)
|
||||||
body = util.ApplyDefaultThinkingIfNeeded(req.Model, body)
|
body = util.ApplyDefaultThinkingIfNeeded(req.Model, body)
|
||||||
body = util.NormalizeGeminiThinkingBudget(req.Model, body)
|
body = util.NormalizeGeminiThinkingBudget(req.Model, body)
|
||||||
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
||||||
@@ -290,7 +290,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
translatedReq = applyThinkingMetadata(translatedReq, req.Metadata, req.Model)
|
translatedReq = ApplyThinkingMetadata(translatedReq, req.Metadata, req.Model)
|
||||||
translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
|
translatedReq = util.StripThinkingConfigIfUnsupported(req.Model, translatedReq)
|
||||||
translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
|
translatedReq = fixGeminiImageAspectRatio(req.Model, translatedReq)
|
||||||
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" {
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
body = applyPayloadConfig(e.cfg, req.Model, body)
|
body = applyPayloadConfig(e.cfg, req.Model, body)
|
||||||
@@ -148,13 +148,13 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" {
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return nil, errValidate
|
return nil, errValidate
|
||||||
}
|
}
|
||||||
// Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour.
|
// Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour.
|
||||||
|
|||||||
@@ -60,13 +60,13 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
||||||
allowCompat := e.allowCompatReasoningEffort(req.Model, auth)
|
allowCompat := e.allowCompatReasoningEffort(req.Model, auth)
|
||||||
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort", allowCompat)
|
translated = ApplyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort", allowCompat)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" && modelOverride == "" {
|
if upstreamModel != "" && modelOverride == "" {
|
||||||
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
translated = normalizeThinkingConfig(translated, upstreamModel, allowCompat)
|
translated = NormalizeThinkingConfig(translated, upstreamModel, allowCompat)
|
||||||
if errValidate := validateThinkingConfig(translated, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(translated, upstreamModel); errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,13 +156,13 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
}
|
}
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
||||||
allowCompat := e.allowCompatReasoningEffort(req.Model, auth)
|
allowCompat := e.allowCompatReasoningEffort(req.Model, auth)
|
||||||
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort", allowCompat)
|
translated = ApplyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort", allowCompat)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" && modelOverride == "" {
|
if upstreamModel != "" && modelOverride == "" {
|
||||||
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
translated = normalizeThinkingConfig(translated, upstreamModel, allowCompat)
|
translated = NormalizeThinkingConfig(translated, upstreamModel, allowCompat)
|
||||||
if errValidate := validateThinkingConfig(translated, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(translated, upstreamModel); errValidate != nil {
|
||||||
return nil, errValidate
|
return nil, errValidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// applyThinkingMetadata applies thinking config from model suffix metadata (e.g., (high), (8192))
|
// ApplyThinkingMetadata applies thinking config from model suffix metadata (e.g., (high), (8192))
|
||||||
// for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
|
// for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
|
||||||
func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
|
func ApplyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
|
||||||
budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
|
budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
|
||||||
if !ok || (budgetOverride == nil && includeOverride == nil) {
|
if !ok || (budgetOverride == nil && includeOverride == nil) {
|
||||||
return payload
|
return payload
|
||||||
@@ -45,10 +45,10 @@ func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model str
|
|||||||
return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)
|
return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyReasoningEffortMetadata applies reasoning effort overrides from metadata to the given JSON path.
|
// ApplyReasoningEffortMetadata applies reasoning effort overrides from metadata to the given JSON path.
|
||||||
// Metadata values take precedence over any existing field when the model supports thinking, intentionally
|
// Metadata values take precedence over any existing field when the model supports thinking, intentionally
|
||||||
// overwriting caller-provided values to honor suffix/default metadata priority.
|
// overwriting caller-provided values to honor suffix/default metadata priority.
|
||||||
func applyReasoningEffortMetadata(payload []byte, metadata map[string]any, model, field string, allowCompat bool) []byte {
|
func ApplyReasoningEffortMetadata(payload []byte, metadata map[string]any, model, field string, allowCompat bool) []byte {
|
||||||
if len(metadata) == 0 {
|
if len(metadata) == 0 {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func applyReasoningEffortMetadata(payload []byte, metadata map[string]any, model
|
|||||||
if effort, ok := util.OpenAIThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" {
|
if effort, ok := util.OpenAIThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" {
|
||||||
if *budget == 0 && effort == "none" && util.ModelUsesThinkingLevels(baseModel) {
|
if *budget == 0 && effort == "none" && util.ModelUsesThinkingLevels(baseModel) {
|
||||||
if _, supported := util.NormalizeReasoningEffortLevel(baseModel, effort); !supported {
|
if _, supported := util.NormalizeReasoningEffortLevel(baseModel, effort); !supported {
|
||||||
return stripThinkingFields(payload, false)
|
return StripThinkingFields(payload, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +238,12 @@ func matchModelPattern(pattern, model string) bool {
|
|||||||
return pi == len(pattern)
|
return pi == len(pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeThinkingConfig normalizes thinking-related fields in the payload
|
// NormalizeThinkingConfig normalizes thinking-related fields in the payload
|
||||||
// based on model capabilities. For models without thinking support, it strips
|
// based on model capabilities. For models without thinking support, it strips
|
||||||
// reasoning fields. For models with level-based thinking, it validates and
|
// reasoning fields. For models with level-based thinking, it validates and
|
||||||
// normalizes the reasoning effort level. For models with numeric budget thinking,
|
// normalizes the reasoning effort level. For models with numeric budget thinking,
|
||||||
// it strips the effort string fields.
|
// it strips the effort string fields.
|
||||||
func normalizeThinkingConfig(payload []byte, model string, allowCompat bool) []byte {
|
func NormalizeThinkingConfig(payload []byte, model string, allowCompat bool) []byte {
|
||||||
if len(payload) == 0 || model == "" {
|
if len(payload) == 0 || model == "" {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -252,22 +252,22 @@ func normalizeThinkingConfig(payload []byte, model string, allowCompat bool) []b
|
|||||||
if allowCompat {
|
if allowCompat {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
return stripThinkingFields(payload, false)
|
return StripThinkingFields(payload, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.ModelUsesThinkingLevels(model) {
|
if util.ModelUsesThinkingLevels(model) {
|
||||||
return normalizeReasoningEffortLevel(payload, model)
|
return NormalizeReasoningEffortLevel(payload, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model supports thinking but uses numeric budgets, not levels.
|
// Model supports thinking but uses numeric budgets, not levels.
|
||||||
// Strip effort string fields since they are not applicable.
|
// Strip effort string fields since they are not applicable.
|
||||||
return stripThinkingFields(payload, true)
|
return StripThinkingFields(payload, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stripThinkingFields removes thinking-related fields from the payload for
|
// StripThinkingFields removes thinking-related fields from the payload for
|
||||||
// models that do not support thinking. If effortOnly is true, only removes
|
// models that do not support thinking. If effortOnly is true, only removes
|
||||||
// effort string fields (for models using numeric budgets).
|
// effort string fields (for models using numeric budgets).
|
||||||
func stripThinkingFields(payload []byte, effortOnly bool) []byte {
|
func StripThinkingFields(payload []byte, effortOnly bool) []byte {
|
||||||
fieldsToRemove := []string{
|
fieldsToRemove := []string{
|
||||||
"reasoning_effort",
|
"reasoning_effort",
|
||||||
"reasoning.effort",
|
"reasoning.effort",
|
||||||
@@ -284,9 +284,9 @@ func stripThinkingFields(payload []byte, effortOnly bool) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeReasoningEffortLevel validates and normalizes the reasoning_effort
|
// NormalizeReasoningEffortLevel validates and normalizes the reasoning_effort
|
||||||
// or reasoning.effort field for level-based thinking models.
|
// or reasoning.effort field for level-based thinking models.
|
||||||
func normalizeReasoningEffortLevel(payload []byte, model string) []byte {
|
func NormalizeReasoningEffortLevel(payload []byte, model string) []byte {
|
||||||
out := payload
|
out := payload
|
||||||
|
|
||||||
if effort := gjson.GetBytes(out, "reasoning_effort"); effort.Exists() {
|
if effort := gjson.GetBytes(out, "reasoning_effort"); effort.Exists() {
|
||||||
@@ -304,10 +304,10 @@ func normalizeReasoningEffortLevel(payload []byte, model string) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateThinkingConfig checks for unsupported reasoning levels on level-based models.
|
// ValidateThinkingConfig checks for unsupported reasoning levels on level-based models.
|
||||||
// Returns a statusErr with 400 when an unsupported level is supplied to avoid silently
|
// Returns a statusErr with 400 when an unsupported level is supplied to avoid silently
|
||||||
// downgrading requests.
|
// downgrading requests.
|
||||||
func validateThinkingConfig(payload []byte, model string) error {
|
func ValidateThinkingConfig(payload []byte, model string) error {
|
||||||
if len(payload) == 0 || model == "" {
|
if len(payload) == 0 || model == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" {
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
body = applyPayloadConfig(e.cfg, req.Model, body)
|
body = applyPayloadConfig(e.cfg, req.Model, body)
|
||||||
@@ -131,13 +131,13 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning_effort", false)
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" {
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
body = normalizeThinkingConfig(body, upstreamModel, false)
|
body = NormalizeThinkingConfig(body, upstreamModel, false)
|
||||||
if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil {
|
if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil {
|
||||||
return nil, errValidate
|
return nil, errValidate
|
||||||
}
|
}
|
||||||
toolsResult := gjson.GetBytes(body, "tools")
|
toolsResult := gjson.GetBytes(body, "tools")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,20 +9,13 @@ import (
|
|||||||
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
|
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// statusErr mirrors executor.statusErr to keep validation behavior aligned.
|
|
||||||
type statusErr struct {
|
|
||||||
code int
|
|
||||||
msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e statusErr) Error() string { return e.msg }
|
|
||||||
|
|
||||||
// isOpenAICompatModel returns true if the model is configured as an OpenAI-compatible
|
// isOpenAICompatModel returns true if the model is configured as an OpenAI-compatible
|
||||||
// model that should have reasoning effort passed through even if not in registry.
|
// model that should have reasoning effort passed through even if not in registry.
|
||||||
// This simulates the allowCompat behavior from OpenAICompatExecutor.
|
// This simulates the allowCompat behavior from OpenAICompatExecutor.
|
||||||
@@ -108,159 +100,10 @@ func buildRawPayload(fromProtocol, modelWithSuffix string) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyThinkingMetadataLocal mirrors executor.applyThinkingMetadata.
|
|
||||||
func applyThinkingMetadataLocal(payload []byte, metadata map[string]any, model string) []byte {
|
|
||||||
budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(model, metadata)
|
|
||||||
if !ok || (budgetOverride == nil && includeOverride == nil) {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
if !util.ModelSupportsThinking(model) {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
if budgetOverride != nil {
|
|
||||||
norm := util.NormalizeThinkingBudget(model, *budgetOverride)
|
|
||||||
budgetOverride = &norm
|
|
||||||
}
|
|
||||||
return util.ApplyGeminiThinkingConfig(payload, budgetOverride, includeOverride)
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyReasoningEffortMetadataLocal mirrors executor.applyReasoningEffortMetadata.
|
|
||||||
func applyReasoningEffortMetadataLocal(payload []byte, metadata map[string]any, model, field string, allowCompat bool) []byte {
|
|
||||||
if len(metadata) == 0 {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
if field == "" {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
baseModel := util.ResolveOriginalModel(model, metadata)
|
|
||||||
if baseModel == "" {
|
|
||||||
baseModel = model
|
|
||||||
}
|
|
||||||
if !util.ModelSupportsThinking(baseModel) && !allowCompat {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" {
|
|
||||||
if util.ModelUsesThinkingLevels(baseModel) || allowCompat {
|
|
||||||
if updated, err := sjson.SetBytes(payload, field, effort); err == nil {
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: numeric thinking_budget suffix for level-based (OpenAI-style) models.
|
|
||||||
if util.ModelUsesThinkingLevels(baseModel) || allowCompat {
|
|
||||||
if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil {
|
|
||||||
if effort, ok := util.OpenAIThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" {
|
|
||||||
if *budget == 0 && effort == "none" && util.ModelUsesThinkingLevels(baseModel) {
|
|
||||||
if _, supported := util.NormalizeReasoningEffortLevel(baseModel, effort); !supported {
|
|
||||||
return stripThinkingFieldsLocal(payload, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated, err := sjson.SetBytes(payload, field, effort); err == nil {
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeThinkingConfigLocal mirrors executor.normalizeThinkingConfig.
|
|
||||||
// When allowCompat is true, reasoning fields are preserved even for models
|
|
||||||
// without thinking support (simulating openai-compat passthrough behavior).
|
|
||||||
func normalizeThinkingConfigLocal(payload []byte, model string, allowCompat bool) []byte {
|
|
||||||
if len(payload) == 0 || model == "" {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
if !util.ModelSupportsThinking(model) {
|
|
||||||
if allowCompat {
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
return stripThinkingFieldsLocal(payload, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if util.ModelUsesThinkingLevels(model) {
|
|
||||||
return normalizeReasoningEffortLevelLocal(payload, model)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model supports thinking but uses numeric budgets, not levels.
|
|
||||||
// Strip effort string fields since they are not applicable.
|
|
||||||
return stripThinkingFieldsLocal(payload, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stripThinkingFieldsLocal mirrors executor.stripThinkingFields.
|
|
||||||
func stripThinkingFieldsLocal(payload []byte, effortOnly bool) []byte {
|
|
||||||
fieldsToRemove := []string{
|
|
||||||
"reasoning_effort",
|
|
||||||
"reasoning.effort",
|
|
||||||
}
|
|
||||||
if !effortOnly {
|
|
||||||
fieldsToRemove = append([]string{"reasoning"}, fieldsToRemove...)
|
|
||||||
}
|
|
||||||
out := payload
|
|
||||||
for _, field := range fieldsToRemove {
|
|
||||||
if gjson.GetBytes(out, field).Exists() {
|
|
||||||
out, _ = sjson.DeleteBytes(out, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeReasoningEffortLevelLocal mirrors executor.normalizeReasoningEffortLevel.
|
|
||||||
func normalizeReasoningEffortLevelLocal(payload []byte, model string) []byte {
|
|
||||||
out := payload
|
|
||||||
|
|
||||||
if effort := gjson.GetBytes(out, "reasoning_effort"); effort.Exists() {
|
|
||||||
if normalized, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); ok {
|
|
||||||
out, _ = sjson.SetBytes(out, "reasoning_effort", normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if effort := gjson.GetBytes(out, "reasoning.effort"); effort.Exists() {
|
|
||||||
if normalized, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); ok {
|
|
||||||
out, _ = sjson.SetBytes(out, "reasoning.effort", normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateThinkingConfigLocal mirrors executor.validateThinkingConfig.
|
|
||||||
func validateThinkingConfigLocal(payload []byte, model string) error {
|
|
||||||
if len(payload) == 0 || model == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !util.ModelSupportsThinking(model) || !util.ModelUsesThinkingLevels(model) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
levels := util.GetModelThinkingLevels(model)
|
|
||||||
checkField := func(path string) error {
|
|
||||||
if effort := gjson.GetBytes(payload, path); effort.Exists() {
|
|
||||||
if _, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); !ok {
|
|
||||||
return statusErr{
|
|
||||||
code: http.StatusBadRequest,
|
|
||||||
msg: fmt.Sprintf("unsupported reasoning effort level %q for model %s (supported: %s)", effort.String(), model, strings.Join(levels, ", ")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkField("reasoning_effort"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := checkField("reasoning.effort"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeCodexPayload mirrors codex_executor's reasoning + streaming tweaks.
|
// normalizeCodexPayload mirrors codex_executor's reasoning + streaming tweaks.
|
||||||
func normalizeCodexPayload(body []byte, upstreamModel string, allowCompat bool) ([]byte, error) {
|
func normalizeCodexPayload(body []byte, upstreamModel string, allowCompat bool) ([]byte, error) {
|
||||||
body = normalizeThinkingConfigLocal(body, upstreamModel, allowCompat)
|
body = executor.NormalizeThinkingConfig(body, upstreamModel, allowCompat)
|
||||||
if err := validateThinkingConfigLocal(body, upstreamModel); err != nil {
|
if err := executor.ValidateThinkingConfig(body, upstreamModel); err != nil {
|
||||||
return body, err
|
return body, err
|
||||||
}
|
}
|
||||||
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
body, _ = sjson.SetBytes(body, "model", upstreamModel)
|
||||||
@@ -290,7 +133,7 @@ func buildBodyForProtocol(t *testing.T, fromProtocol, toProtocol, modelWithSuffi
|
|||||||
allowCompat := isOpenAICompatModel(normalizedModel)
|
allowCompat := isOpenAICompatModel(normalizedModel)
|
||||||
switch toProtocol {
|
switch toProtocol {
|
||||||
case "gemini":
|
case "gemini":
|
||||||
body = applyThinkingMetadataLocal(body, metadata, normalizedModel)
|
body = executor.ApplyThinkingMetadata(body, metadata, normalizedModel)
|
||||||
body = util.ApplyDefaultThinkingIfNeeded(normalizedModel, body)
|
body = util.ApplyDefaultThinkingIfNeeded(normalizedModel, body)
|
||||||
body = util.NormalizeGeminiThinkingBudget(normalizedModel, body)
|
body = util.NormalizeGeminiThinkingBudget(normalizedModel, body)
|
||||||
body = util.StripThinkingConfigIfUnsupported(normalizedModel, body)
|
body = util.StripThinkingConfigIfUnsupported(normalizedModel, body)
|
||||||
@@ -299,12 +142,12 @@ func buildBodyForProtocol(t *testing.T, fromProtocol, toProtocol, modelWithSuffi
|
|||||||
body = util.ApplyClaudeThinkingConfig(body, budget)
|
body = util.ApplyClaudeThinkingConfig(body, budget)
|
||||||
}
|
}
|
||||||
case "openai":
|
case "openai":
|
||||||
body = applyReasoningEffortMetadataLocal(body, metadata, normalizedModel, "reasoning_effort", allowCompat)
|
body = executor.ApplyReasoningEffortMetadata(body, metadata, normalizedModel, "reasoning_effort", allowCompat)
|
||||||
body = normalizeThinkingConfigLocal(body, upstreamModel, allowCompat)
|
body = executor.NormalizeThinkingConfig(body, upstreamModel, allowCompat)
|
||||||
err = validateThinkingConfigLocal(body, upstreamModel)
|
err = executor.ValidateThinkingConfig(body, upstreamModel)
|
||||||
case "codex": // OpenAI responses / codex
|
case "codex": // OpenAI responses / codex
|
||||||
// Codex does not support allowCompat; always use false.
|
// Codex does not support allowCompat; always use false.
|
||||||
body = applyReasoningEffortMetadataLocal(body, metadata, normalizedModel, "reasoning.effort", false)
|
body = executor.ApplyReasoningEffortMetadata(body, metadata, normalizedModel, "reasoning.effort", false)
|
||||||
// Mirror CodexExecutor final normalization and model override so tests log the final body.
|
// Mirror CodexExecutor final normalization and model override so tests log the final body.
|
||||||
body, err = normalizeCodexPayload(body, upstreamModel, false)
|
body, err = normalizeCodexPayload(body, upstreamModel, false)
|
||||||
default:
|
default:
|
||||||
@@ -629,8 +472,8 @@ func buildBodyForProtocolWithRawThinking(t *testing.T, fromProtocol, toProtocol,
|
|||||||
// For raw payload, Claude thinking is passed through by translator
|
// For raw payload, Claude thinking is passed through by translator
|
||||||
// No additional processing needed as thinking is already in body
|
// No additional processing needed as thinking is already in body
|
||||||
case "openai":
|
case "openai":
|
||||||
body = normalizeThinkingConfigLocal(body, model, allowCompat)
|
body = executor.NormalizeThinkingConfig(body, model, allowCompat)
|
||||||
err = validateThinkingConfigLocal(body, model)
|
err = executor.ValidateThinkingConfig(body, model)
|
||||||
case "codex":
|
case "codex":
|
||||||
// Codex does not support allowCompat; always use false.
|
// Codex does not support allowCompat; always use false.
|
||||||
body, err = normalizeCodexPayload(body, model, false)
|
body, err = normalizeCodexPayload(body, model, false)
|
||||||
|
|||||||
Reference in New Issue
Block a user