diff --git a/internal/config/config.go b/internal/config/config.go index 21efe233..dab3297d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -959,13 +959,12 @@ func (c OpenAIConfig) MaxCompletionTokensEffective() int { } // IsDeepSeekEndpointOrModel reports whether the channel targets DeepSeek's -// official-compatible API or a DeepSeek model family. This is separate from the -// reasoning profile: profile controls field mapping, while DeepSeek has provider -// constraints such as default thinking mode and no tool_choice in thinking mode. +// official-compatible API endpoint. The historical name is kept for compatibility; +// model names alone are not enough to infer DeepSeek wire behavior behind +// OpenAI-compatible gateways. func (c OpenAIConfig) IsDeepSeekEndpointOrModel() bool { baseURL := strings.ToLower(strings.TrimSpace(c.BaseURL)) - model := strings.ToLower(strings.TrimSpace(c.Model)) - return strings.Contains(baseURL, "deepseek") || strings.Contains(model, "deepseek") + return strings.Contains(baseURL, "deepseek") } // OpenAIReasoningConfig 全局默认与网关 profile(对话页可通过 ChatRequest.reasoning 覆盖,受 AllowClientReasoning 约束)。 diff --git a/internal/openai/reasoning_payload_test.go b/internal/openai/reasoning_payload_test.go index 9f7dccfe..c026e47b 100644 --- a/internal/openai/reasoning_payload_test.go +++ b/internal/openai/reasoning_payload_test.go @@ -205,7 +205,7 @@ func TestReasoningToolChoiceCompatRoundTripperDeepSeek(t *testing.T) { } } -func TestReasoningToolChoiceCompatRoundTripperDeepSeekEndpointWinsOverProfile(t *testing.T) { +func TestReasoningToolChoiceCompatRoundTripperOpenAIProfileWinsOverDeepSeekEndpoint(t *testing.T) { var gotBody string rt := &reasoningToolChoiceCompatRoundTripper{ cfg: &config.OpenAIConfig{ @@ -235,11 +235,11 @@ func TestReasoningToolChoiceCompatRoundTripperDeepSeekEndpointWinsOverProfile(t if err != nil { t.Fatal(err) } - if strings.Contains(gotBody, "tool_choice") { - t.Fatalf("expected DeepSeek tool_choice stripped despite openai_compat profile, got %s", gotBody) + if !strings.Contains(gotBody, "tool_choice") { + t.Fatalf("expected tool_choice preserved for explicit openai_compat profile, got %s", gotBody) } if !strings.Contains(gotBody, "tools") { - t.Fatalf("expected tools preserved for DeepSeek, got %s", gotBody) + t.Fatalf("expected tools preserved for explicit openai_compat profile, got %s", gotBody) } } diff --git a/internal/openai/reasoning_tool_choice_compat.go b/internal/openai/reasoning_tool_choice_compat.go index fd703222..59490be7 100644 --- a/internal/openai/reasoning_tool_choice_compat.go +++ b/internal/openai/reasoning_tool_choice_compat.go @@ -55,9 +55,6 @@ func isDeepSeekToolChoiceCompatProfile(cfg *config.OpenAIConfig) bool { if cfg == nil { return false } - if cfg.IsDeepSeekEndpointOrModel() { - return true - } profile := strings.ToLower(strings.TrimSpace(cfg.Reasoning.ProfileEffective())) if profile == "deepseek" || profile == "deepseek_compat" { return true @@ -65,5 +62,5 @@ func isDeepSeekToolChoiceCompatProfile(cfg *config.OpenAIConfig) bool { if profile != "" && profile != "auto" { return false } - return false + return cfg.IsDeepSeekEndpointOrModel() } diff --git a/internal/reasoning/eino.go b/internal/reasoning/eino.go index 3d7c19ae..65138f46 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 || oa.IsDeepSeekEndpointOrModel() { + if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek { // DeepSeek enables thinking by default, so omission would not actually // disable it for the planner's forced tool-choice requests. applyThinkingDisabled(cfg) @@ -88,9 +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. Detect the actual DeepSeek target even - // when the configured reasoning profile was left as openai_compat. - if resolveWireProfile(oa, sr) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { + // thinking.type=disabled switch. The configured profile is authoritative; + // auto-detection only happens inside resolveWireProfile for profile=auto. + if resolveWireProfile(oa, sr) == wireDeepseek { applyThinkingDisabled(cfg) } return @@ -132,7 +132,7 @@ func AgenticOpenAIExtraFields(oa *config.OpenAIConfig, client *ClientIntent) map fields := cloneExtraRequestFields(sr.ExtraRequestFields) if mode == "off" { clearReasoningExtraFields(fields) - if resolveWireProfile(oa, sr) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { + if resolveWireProfile(oa, sr) == wireDeepseek { if fields == nil { fields = make(map[string]any) } @@ -194,7 +194,7 @@ func AgenticOpenAIPlannerExtraFields(oa *config.OpenAIConfig) map[string]any { } fields := cloneExtraRequestFields(oa.Reasoning.ExtraRequestFields) clearReasoningExtraFields(fields) - if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek || oa.IsDeepSeekEndpointOrModel() { + if resolveWireProfile(oa, &oa.Reasoning) == wireDeepseek { if fields == nil { fields = make(map[string]any) } diff --git a/internal/reasoning/eino_test.go b/internal/reasoning/eino_test.go index c9eadf8a..f8445175 100644 --- a/internal/reasoning/eino_test.go +++ b/internal/reasoning/eino_test.go @@ -140,7 +140,7 @@ func TestAgenticOpenAIPlannerExtraFields_deepseekDisablesThinking(t *testing.T) } } -func TestAgenticOpenAIPlannerExtraFields_deepseekEndpointWinsOverOpenAIProfile(t *testing.T) { +func TestAgenticOpenAIPlannerExtraFields_openAIProfileWinsOverDeepseekEndpoint(t *testing.T) { oa := &config.OpenAIConfig{ BaseURL: "https://api.deepseek.com/v1", Model: "deepseek-v4-flash", @@ -155,12 +155,10 @@ func TestAgenticOpenAIPlannerExtraFields_deepseekEndpointWinsOverOpenAIProfile(t }, } 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) + for _, key := range reasoningPayloadKeysForTest { + if _, ok := got[key]; ok { + t.Fatalf("planner fields unexpectedly contain %q: %#v", key, got) + } } if got["vendor_option"] != true { t.Fatalf("vendor option not preserved: %#v", got) @@ -189,7 +187,7 @@ func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testi } } -func TestApplyPlanExecutePlannerModelConfig_deepseekEndpointWinsOverOpenAIProfile(t *testing.T) { +func TestApplyPlanExecutePlannerModelConfig_openAIProfileWinsOverDeepseekEndpoint(t *testing.T) { cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ "thinking": map[string]any{"type": "enabled"}, "reasoning_effort": "high", @@ -205,16 +203,7 @@ func TestApplyPlanExecutePlannerModelConfig_deepseekEndpointWinsOverOpenAIProfil }, } 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) - } + assertNoReasoningFields(t, cfg) if cfg.ExtraFields["vendor_option"] != true { t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields) } @@ -246,6 +235,89 @@ func TestApplyReasoningOff_omitsAllReasoningFields(t *testing.T) { } } +func TestApplyReasoningOff_openAICompatDeepseekModelOmitsAllReasoningFields(t *testing.T) { + allowClient := false + cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ + "thinking": map[string]any{"type": "enabled"}, + "reasoning_effort": "high", + }} + oa := &config.OpenAIConfig{ + Provider: "openai_compatible", + BaseURL: "http://your-gateway:port/v1", + Model: "deepseek-v4-flash-0731", + Reasoning: config.OpenAIReasoningConfig{ + Mode: "off", + Effort: "high", + Profile: "openai_compat", + AllowClientReasoning: &allowClient, + ExtraRequestFields: map[string]interface{}{ + "thinking": map[string]any{"type": "disabled"}, + "output_config": map[string]any{"effort": "high"}, + "vendor_option": true, + }, + }, + } + ApplyToEinoChatModelConfig(cfg, oa, nil) + assertNoReasoningFields(t, cfg) + if cfg.ExtraFields["vendor_option"] != true { + t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields) + } +} + +func TestAgenticOpenAIExtraFields_openAICompatDeepseekModelOmitsAllReasoningFields(t *testing.T) { + oa := &config.OpenAIConfig{ + Provider: "openai_compatible", + BaseURL: "http://your-gateway:port/v1", + Model: "deepseek-v4-flash-0731", + Reasoning: config.OpenAIReasoningConfig{ + Mode: "off", + Effort: "high", + Profile: "openai_compat", + ExtraRequestFields: map[string]interface{}{ + "thinking": map[string]any{"type": "disabled"}, + "reasoning_effort": "high", + "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_openAICompatDeepseekModelOmitsAllReasoningFields(t *testing.T) { + oa := &config.OpenAIConfig{ + Provider: "openai_compatible", + BaseURL: "http://your-gateway:port/v1", + Model: "deepseek-v4-flash-0731", + Reasoning: config.OpenAIReasoningConfig{ + Mode: "on", + Effort: "high", + Profile: "openai_compat", + ExtraRequestFields: map[string]interface{}{ + "thinking": map[string]any{"type": "enabled"}, + "reasoning_effort": "high", + "vendor_option": true, + }, + }, + } + got := AgenticOpenAIPlannerExtraFields(oa) + for _, key := range reasoningPayloadKeysForTest { + if _, ok := got[key]; ok { + t.Fatalf("planner fields unexpectedly contain %q: %#v", key, got) + } + } + if got["vendor_option"] != true { + t.Fatalf("vendor option not preserved: %#v", got) + } +} + func TestApplyReasoningOff_clientOverrideOmit(t *testing.T) { cfg := &einoopenai.ChatModelConfig{} oa := &config.OpenAIConfig{Reasoning: config.OpenAIReasoningConfig{ @@ -256,7 +328,7 @@ func TestApplyReasoningOff_clientOverrideOmit(t *testing.T) { } func TestApplyReasoningOff_deepseekExplicitlyDisablesDefaultThinking(t *testing.T) { - for _, profile := range []string{"deepseek_compat", "auto", "openai_compat"} { + for _, profile := range []string{"deepseek_compat", "auto"} { t.Run(profile, func(t *testing.T) { cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ "reasoning_effort": "high", @@ -287,6 +359,25 @@ func TestApplyReasoningOff_deepseekExplicitlyDisablesDefaultThinking(t *testing. } } +func TestApplyReasoningOff_openAIProfileWinsOverDeepseekEndpoint(t *testing.T) { + cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{ + "reasoning_effort": "high", + "vendor_option": true, + }} + oa := &config.OpenAIConfig{ + BaseURL: "https://api.deepseek.com", + Model: "deepseek-v4-pro", + Reasoning: config.OpenAIReasoningConfig{ + Mode: "off", Effort: "high", Profile: "openai_compat", + }, + } + ApplyToEinoChatModelConfig(cfg, oa, nil) + assertNoReasoningFields(t, cfg) + if cfg.ExtraFields["vendor_option"] != true { + t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields) + } +} + func TestApplyReasoningOff_wirePayloadOmitsThinking(t *testing.T) { var requestBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {