From 5ce66ee2f8c3d4175a376b2e69945fc876fb0c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:51:13 +0800 Subject: [PATCH] Add files via upload --- internal/reasoning/eino.go | 112 +++++++++++++++++++++++++-- internal/reasoning/eino_test.go | 131 +++++++++++++++++++++++++++++++- 2 files changed, 236 insertions(+), 7 deletions(-) diff --git a/internal/reasoning/eino.go b/internal/reasoning/eino.go index 8cdd4909..f88741bd 100644 --- a/internal/reasoning/eino.go +++ b/internal/reasoning/eino.go @@ -36,7 +36,7 @@ func ApplyPlanExecutePlannerModelConfig(cfg *einoopenai.ChatModelConfig, oa *con } mergeExtraRequestFields(cfg, oa.Reasoning.ExtraRequestFields) clearReasoningFromChatModelConfig(cfg) - if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek { + if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { // DeepSeek enables thinking by default, so omission would not actually // disable it for the planner's forced tool-choice requests. applyThinkingDisabled(cfg) @@ -88,8 +88,9 @@ func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.Open clearReasoningFromChatModelConfig(cfg) // Strict OpenAI endpoints reject unknown `thinking` fields, whereas the // DeepSeek API enables thinking by default and requires an explicit - // thinking.type=disabled switch. Keep that wire difference profile-scoped. - if resolveWireProfile(oa, sr) == wireDeepseek { + // thinking.type=disabled switch. Detect the actual DeepSeek target even + // when the configured reasoning profile was left as openai_compat. + if resolveWireProfile(oa, sr) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { applyThinkingDisabled(cfg) } return @@ -118,6 +119,107 @@ func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.Open } } +// AgenticOpenAIExtraFields returns reasoning-related request fields for +// agenticopenai.ChatConfig. The agentic chat backend currently exposes provider +// extensions through ExtraFields instead of typed ReasoningEffort fields. +func AgenticOpenAIExtraFields(oa *config.OpenAIConfig, client *ClientIntent) map[string]any { + if oa == nil { + return nil + } + sr := &oa.Reasoning + allowClient := sr.AllowClientReasoningEffective() + mode := effectiveMode(sr, client, allowClient) + fields := cloneExtraRequestFields(sr.ExtraRequestFields) + if mode == "off" { + clearReasoningExtraFields(fields) + if resolveWireProfile(oa, sr) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { + if fields == nil { + fields = make(map[string]any) + } + fields["thinking"] = map[string]any{"type": "disabled"} + } + return fields + } + if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") || + strings.EqualFold(strings.TrimSpace(oa.Provider), "anthropic") { + return fields + } + effort := effectiveEffort(sr, client, allowClient) + switch resolveWireProfile(oa, sr) { + case wireDeepseek: + if mode == "auto" || mode == "on" { + if fields == nil { + fields = make(map[string]any) + } + fields["thinking"] = map[string]any{"type": "enabled"} + } + if effort != "" { + if fields == nil { + fields = make(map[string]any) + } + fields["reasoning_effort"] = effortStringForAPI(effort) + } + case wireOutputConfig: + e := effort + if mode == "on" && e == "" { + e = "high" + } + if e != "" { + if fields == nil { + fields = make(map[string]any) + } + fields["output_config"] = map[string]any{"effort": effortStringForAPI(e)} + } + default: + e := effort + if mode == "on" && e == "" { + e = "medium" + } + if e != "" { + if fields == nil { + fields = make(map[string]any) + } + fields["reasoning_effort"] = effortStringForAPI(e) + } + } + return fields +} + +// AgenticOpenAIPlannerExtraFields mirrors ApplyPlanExecutePlannerModelConfig for +// agenticopenai.ChatConfig: keep admin extras, strip reasoning controls, and +// explicitly disable DeepSeek thinking where omission would still think. +func AgenticOpenAIPlannerExtraFields(oa *config.OpenAIConfig) map[string]any { + if oa == nil { + return nil + } + fields := cloneExtraRequestFields(oa.Reasoning.ExtraRequestFields) + clearReasoningExtraFields(fields) + if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { + if fields == nil { + fields = make(map[string]any) + } + fields["thinking"] = map[string]any{"type": "disabled"} + } + return fields +} + +func cloneExtraRequestFields(fields map[string]interface{}) map[string]any { + if len(fields) == 0 { + return nil + } + out := make(map[string]any, len(fields)) + for k, v := range fields { + out[k] = v + } + return out +} + +func clearReasoningExtraFields(fields map[string]any) { + for _, key := range []string{"thinking", "reasoning_effort", "output_config", "reasoning"} { + delete(fields, key) + } +} + // applyClaudeExtendedThinking sets Anthropic Messages API fields per official guidance: // - Adaptive models (4.6+): thinking.type=adaptive; output_config.effort only when user sets effort (API default is high). // - Sonnet 3.7: thinking.type=enabled + budget_tokens=10000 (doc example); effort is not mapped — use extra_request_fields for custom budget. @@ -240,9 +342,7 @@ func resolveWireProfile(oa *config.OpenAIConfig, sr *config.OpenAIReasoningConfi case "deepseek", "deepseek_compat": return wireDeepseek case "auto", "": - bu := strings.ToLower(oa.BaseURL) - mo := strings.ToLower(oa.Model) - if strings.Contains(bu, "deepseek") || strings.Contains(mo, "deepseek") { + if oa.IsDeepSeekEndpointOrModel() { return wireDeepseek } return wireOpenAI diff --git a/internal/reasoning/eino_test.go b/internal/reasoning/eino_test.go index 50dee5be..c9eadf8a 100644 --- a/internal/reasoning/eino_test.go +++ b/internal/reasoning/eino_test.go @@ -69,6 +69,104 @@ func TestApplyOpenAICompat_xhighExtraField(t *testing.T) { } } +func TestAgenticOpenAIExtraFields_openAICompatReasoningEffort(t *testing.T) { + oa := &config.OpenAIConfig{ + Reasoning: config.OpenAIReasoningConfig{ + Profile: "openai_compat", + Mode: "on", + Effort: "high", + ExtraRequestFields: map[string]interface{}{ + "vendor_option": true, + }, + }, + } + got := AgenticOpenAIExtraFields(oa, nil) + if got["reasoning_effort"] != "high" { + t.Fatalf("reasoning_effort=%#v, want high in %#v", got["reasoning_effort"], got) + } + if got["vendor_option"] != true { + t.Fatalf("vendor option not preserved: %#v", got) + } +} + +func TestAgenticOpenAIExtraFields_reasoningOffPreservesUnrelatedFields(t *testing.T) { + oa := &config.OpenAIConfig{ + Model: "gpt-4o-mini", + Reasoning: config.OpenAIReasoningConfig{ + Profile: "openai_compat", + Mode: "off", + Effort: "high", + ExtraRequestFields: map[string]interface{}{ + "reasoning_effort": "high", + "thinking": map[string]any{"type": "enabled"}, + "vendor_option": true, + }, + }, + } + got := AgenticOpenAIExtraFields(oa, nil) + for _, key := range reasoningPayloadKeysForTest { + if _, ok := got[key]; ok { + t.Fatalf("agentic fields unexpectedly contain %q: %#v", key, got) + } + } + if got["vendor_option"] != true { + t.Fatalf("vendor option not preserved: %#v", got) + } +} + +func TestAgenticOpenAIPlannerExtraFields_deepseekDisablesThinking(t *testing.T) { + oa := &config.OpenAIConfig{ + BaseURL: "https://api.deepseek.com", + Model: "deepseek-chat", + Reasoning: config.OpenAIReasoningConfig{ + Profile: "auto", + Mode: "on", + ExtraRequestFields: map[string]interface{}{ + "reasoning_effort": "high", + "vendor_option": true, + }, + }, + } + got := AgenticOpenAIPlannerExtraFields(oa) + if got["reasoning_effort"] != nil { + t.Fatalf("planner should strip reasoning_effort: %#v", got) + } + thinking, ok := got["thinking"].(map[string]any) + if !ok || thinking["type"] != "disabled" { + t.Fatalf("expected deepseek thinking disabled, got %#v", got) + } + if got["vendor_option"] != true { + t.Fatalf("vendor option not preserved: %#v", got) + } +} + +func TestAgenticOpenAIPlannerExtraFields_deepseekEndpointWinsOverOpenAIProfile(t *testing.T) { + oa := &config.OpenAIConfig{ + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-v4-flash", + Reasoning: config.OpenAIReasoningConfig{ + Profile: "openai_compat", + Mode: "on", + Effort: "high", + ExtraRequestFields: map[string]interface{}{ + "reasoning_effort": "high", + "vendor_option": true, + }, + }, + } + got := AgenticOpenAIPlannerExtraFields(oa) + if _, ok := got["reasoning_effort"]; ok { + t.Fatalf("planner should strip reasoning_effort: %#v", got) + } + thinking, ok := got["thinking"].(map[string]any) + if !ok || thinking["type"] != "disabled" { + t.Fatalf("expected deepseek thinking disabled despite openai_compat profile, got %#v", got) + } + if got["vendor_option"] != true { + t.Fatalf("vendor option not preserved: %#v", got) + } +} + func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testing.T) { cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ "thinking": map[string]any{"type": "enabled"}, @@ -91,6 +189,37 @@ func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testi } } +func TestApplyPlanExecutePlannerModelConfig_deepseekEndpointWinsOverOpenAIProfile(t *testing.T) { + cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ + "thinking": map[string]any{"type": "enabled"}, + "reasoning_effort": "high", + "vendor_option": true, + }} + oa := &config.OpenAIConfig{ + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-v4-flash", + Reasoning: config.OpenAIReasoningConfig{ + Profile: "openai_compat", + Mode: "on", + Effort: "high", + }, + } + ApplyPlanExecutePlannerModelConfig(cfg, oa) + if cfg.ReasoningEffort != "" { + t.Fatalf("expected ReasoningEffort omitted, got %q", cfg.ReasoningEffort) + } + if _, ok := cfg.ExtraFields["reasoning_effort"]; ok { + t.Fatalf("expected reasoning_effort omitted, got %#v", cfg.ExtraFields) + } + thinking, ok := cfg.ExtraFields["thinking"].(map[string]any) + if !ok || thinking["type"] != "disabled" { + t.Fatalf("expected deepseek thinking disabled despite openai_compat profile, got %#v", cfg.ExtraFields) + } + if cfg.ExtraFields["vendor_option"] != true { + t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields) + } +} + func TestApplyReasoningOff_omitsAllReasoningFields(t *testing.T) { cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ "thinking": map[string]any{"type": "enabled"}, @@ -127,7 +256,7 @@ func TestApplyReasoningOff_clientOverrideOmit(t *testing.T) { } func TestApplyReasoningOff_deepseekExplicitlyDisablesDefaultThinking(t *testing.T) { - for _, profile := range []string{"deepseek_compat", "auto"} { + for _, profile := range []string{"deepseek_compat", "auto", "openai_compat"} { t.Run(profile, func(t *testing.T) { cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ "reasoning_effort": "high",