From 367a05bdf63645b678ee91e0a63ad08d0c1d6043 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:12:54 +0800 Subject: [PATCH] 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. --- .../runtime/executor/aistudio_executor.go | 2 +- internal/runtime/executor/codex_executor.go | 14 +- internal/runtime/executor/gemini_executor.go | 6 +- internal/runtime/executor/iflow_executor.go | 12 +- .../executor/openai_compat_executor.go | 12 +- internal/runtime/executor/payload_helpers.go | 32 ++-- internal/runtime/executor/qwen_executor.go | 12 +- test/thinking_conversion_test.go | 177 +----------------- 8 files changed, 55 insertions(+), 212 deletions(-) diff --git a/internal/runtime/executor/aistudio_executor.go b/internal/runtime/executor/aistudio_executor.go index 221fb648..ada0af39 100644 --- a/internal/runtime/executor/aistudio_executor.go +++ b/internal/runtime/executor/aistudio_executor.go @@ -322,7 +322,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c from := opts.SourceFormat to := sdktranslator.FromString("gemini") 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.ConvertThinkingLevelToBudget(payload) payload = util.NormalizeGeminiThinkingBudget(req.Model, payload) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 2e966237..c3e14701 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -54,9 +54,9 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re from := opts.SourceFormat to := sdktranslator.FromString("codex") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) - body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false) - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false) + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return resp, errValidate } 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") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) - body = applyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false) - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = ApplyReasoningEffortMetadata(body, req.Metadata, req.Model, "reasoning.effort", false) + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return nil, errValidate } body = applyPayloadConfig(e.cfg, req.Model, body) @@ -254,7 +254,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth 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.DeleteBytes(body, "previous_response_id") body, _ = sjson.SetBytes(body, "stream", false) diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 8dd3dc3b..f211ba62 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -83,7 +83,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r from := opts.SourceFormat to := sdktranslator.FromString("gemini") 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.NormalizeGeminiThinkingBudget(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 to := sdktranslator.FromString("gemini") 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.NormalizeGeminiThinkingBudget(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 to := sdktranslator.FromString("gemini") 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 = fixGeminiImageAspectRatio(req.Model, translatedReq) respCtx := context.WithValue(ctx, "alt", opts.Alt) diff --git a/internal/runtime/executor/iflow_executor.go b/internal/runtime/executor/iflow_executor.go index 03df1be0..ad0b4d2a 100644 --- a/internal/runtime/executor/iflow_executor.go +++ b/internal/runtime/executor/iflow_executor.go @@ -57,13 +57,13 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re from := opts.SourceFormat to := sdktranslator.FromString("openai") 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) if upstreamModel != "" { body, _ = sjson.SetBytes(body, "model", upstreamModel) } - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return resp, errValidate } 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") 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) if upstreamModel != "" { body, _ = sjson.SetBytes(body, "model", upstreamModel) } - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return nil, errValidate } // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour. diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 1bbd0c8e..1c57c9b7 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -60,13 +60,13 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A } translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated) 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) if upstreamModel != "" && modelOverride == "" { translated, _ = sjson.SetBytes(translated, "model", upstreamModel) } - translated = normalizeThinkingConfig(translated, upstreamModel, allowCompat) - if errValidate := validateThinkingConfig(translated, upstreamModel); errValidate != nil { + translated = NormalizeThinkingConfig(translated, upstreamModel, allowCompat) + if errValidate := ValidateThinkingConfig(translated, upstreamModel); errValidate != nil { 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) 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) if upstreamModel != "" && modelOverride == "" { translated, _ = sjson.SetBytes(translated, "model", upstreamModel) } - translated = normalizeThinkingConfig(translated, upstreamModel, allowCompat) - if errValidate := validateThinkingConfig(translated, upstreamModel); errValidate != nil { + translated = NormalizeThinkingConfig(translated, upstreamModel, allowCompat) + if errValidate := ValidateThinkingConfig(translated, upstreamModel); errValidate != nil { return nil, errValidate } diff --git a/internal/runtime/executor/payload_helpers.go b/internal/runtime/executor/payload_helpers.go index 667f29da..b0eafbb7 100644 --- a/internal/runtime/executor/payload_helpers.go +++ b/internal/runtime/executor/payload_helpers.go @@ -11,9 +11,9 @@ import ( "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. -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) if !ok || (budgetOverride == nil && includeOverride == nil) { return payload @@ -45,10 +45,10 @@ func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model str 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 // 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 { 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 *budget == 0 && effort == "none" && util.ModelUsesThinkingLevels(baseModel) { 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) } -// 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 // reasoning fields. For models with level-based thinking, it validates and // normalizes the reasoning effort level. For models with numeric budget thinking, // 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 == "" { return payload } @@ -252,22 +252,22 @@ func normalizeThinkingConfig(payload []byte, model string, allowCompat bool) []b if allowCompat { return payload } - return stripThinkingFields(payload, false) + return StripThinkingFields(payload, false) } if util.ModelUsesThinkingLevels(model) { - return normalizeReasoningEffortLevel(payload, model) + return NormalizeReasoningEffortLevel(payload, model) } // Model supports thinking but uses numeric budgets, not levels. // 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 // effort string fields (for models using numeric budgets). -func stripThinkingFields(payload []byte, effortOnly bool) []byte { +func StripThinkingFields(payload []byte, effortOnly bool) []byte { fieldsToRemove := []string{ "reasoning_effort", "reasoning.effort", @@ -284,9 +284,9 @@ func stripThinkingFields(payload []byte, effortOnly bool) []byte { 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. -func normalizeReasoningEffortLevel(payload []byte, model string) []byte { +func NormalizeReasoningEffortLevel(payload []byte, model string) []byte { out := payload if effort := gjson.GetBytes(out, "reasoning_effort"); effort.Exists() { @@ -304,10 +304,10 @@ func normalizeReasoningEffortLevel(payload []byte, model string) []byte { 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 // downgrading requests. -func validateThinkingConfig(payload []byte, model string) error { +func ValidateThinkingConfig(payload []byte, model string) error { if len(payload) == 0 || model == "" { return nil } diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index 3bd61021..1d4ef52d 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -51,13 +51,13 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req from := opts.SourceFormat to := sdktranslator.FromString("openai") 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) if upstreamModel != "" { body, _ = sjson.SetBytes(body, "model", upstreamModel) } - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return resp, errValidate } 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") 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) if upstreamModel != "" { body, _ = sjson.SetBytes(body, "model", upstreamModel) } - body = normalizeThinkingConfig(body, upstreamModel, false) - if errValidate := validateThinkingConfig(body, upstreamModel); errValidate != nil { + body = NormalizeThinkingConfig(body, upstreamModel, false) + if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return nil, errValidate } toolsResult := gjson.GetBytes(body, "tools") diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 9d15e128..6d156954 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -2,7 +2,6 @@ package test import ( "fmt" - "net/http" "strings" "testing" "time" @@ -10,20 +9,13 @@ import ( _ "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/runtime/executor" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "github.com/tidwall/gjson" "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 // model that should have reasoning effort passed through even if not in registry. // 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. func normalizeCodexPayload(body []byte, upstreamModel string, allowCompat bool) ([]byte, error) { - body = normalizeThinkingConfigLocal(body, upstreamModel, allowCompat) - if err := validateThinkingConfigLocal(body, upstreamModel); err != nil { + body = executor.NormalizeThinkingConfig(body, upstreamModel, allowCompat) + if err := executor.ValidateThinkingConfig(body, upstreamModel); err != nil { return body, err } body, _ = sjson.SetBytes(body, "model", upstreamModel) @@ -290,7 +133,7 @@ func buildBodyForProtocol(t *testing.T, fromProtocol, toProtocol, modelWithSuffi allowCompat := isOpenAICompatModel(normalizedModel) switch toProtocol { case "gemini": - body = applyThinkingMetadataLocal(body, metadata, normalizedModel) + body = executor.ApplyThinkingMetadata(body, metadata, normalizedModel) body = util.ApplyDefaultThinkingIfNeeded(normalizedModel, body) body = util.NormalizeGeminiThinkingBudget(normalizedModel, body) body = util.StripThinkingConfigIfUnsupported(normalizedModel, body) @@ -299,12 +142,12 @@ func buildBodyForProtocol(t *testing.T, fromProtocol, toProtocol, modelWithSuffi body = util.ApplyClaudeThinkingConfig(body, budget) } case "openai": - body = applyReasoningEffortMetadataLocal(body, metadata, normalizedModel, "reasoning_effort", allowCompat) - body = normalizeThinkingConfigLocal(body, upstreamModel, allowCompat) - err = validateThinkingConfigLocal(body, upstreamModel) + body = executor.ApplyReasoningEffortMetadata(body, metadata, normalizedModel, "reasoning_effort", allowCompat) + body = executor.NormalizeThinkingConfig(body, upstreamModel, allowCompat) + err = executor.ValidateThinkingConfig(body, upstreamModel) case "codex": // OpenAI responses / codex // 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. body, err = normalizeCodexPayload(body, upstreamModel, false) default: @@ -629,8 +472,8 @@ func buildBodyForProtocolWithRawThinking(t *testing.T, fromProtocol, toProtocol, // For raw payload, Claude thinking is passed through by translator // No additional processing needed as thinking is already in body case "openai": - body = normalizeThinkingConfigLocal(body, model, allowCompat) - err = validateThinkingConfigLocal(body, model) + body = executor.NormalizeThinkingConfig(body, model, allowCompat) + err = executor.ValidateThinkingConfig(body, model) case "codex": // Codex does not support allowCompat; always use false. body, err = normalizeCodexPayload(body, model, false)