Add files via upload

This commit is contained in:
公明
2026-07-17 10:53:24 +08:00
committed by GitHub
parent 0b638791eb
commit 217f8f9648
3 changed files with 170 additions and 53 deletions
+3 -1
View File
@@ -846,7 +846,8 @@ func (c OpenAIConfig) MaxCompletionTokensEffective() int {
// OpenAIReasoningConfig 全局默认与网关 profile(对话页可通过 ChatRequest.reasoning 覆盖,受 AllowClientReasoning 约束)。
type OpenAIReasoningConfig struct {
// Mode: auto(默认)| on | off | default(与 auto 相同)。off 时不向模型附加推理扩展字段。
// Mode: auto(默认)| on | off | default(与 auto 相同)。
// off 在 OpenAI/Claude profile 下省略推理字段;DeepSeek profile 下发送 thinking.type=disabled(其默认开启思考)。
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
// Effort: low | medium | high | max | xhighmax/xhigh 为不同网关最高档命名,原样下发、不互转。空表示不单独指定强度。
Effort string `yaml:"effort,omitempty" json:"effort,omitempty"`
@@ -855,6 +856,7 @@ type OpenAIReasoningConfig struct {
// Profile: auto | deepseek_compat | openai_compat | output_config_effort
Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
// ExtraRequestFields 合并进 Chat Completions 根 JSON(管理员用;与自动字段同名时后者覆盖)。
// Mode=off 时会移除其中的推理控制字段,但保留其他扩展字段;DeepSeek profile 随后补充显式关闭开关。
ExtraRequestFields map[string]interface{} `yaml:"extra_request_fields,omitempty" json:"extra_request_fields,omitempty"`
}
+38 -35
View File
@@ -34,12 +34,13 @@ func ApplyPlanExecutePlannerModelConfig(cfg *einoopenai.ChatModelConfig, oa *con
if cfg == nil || oa == nil {
return
}
offOA := *oa
offReasoning := oa.Reasoning
offReasoning.Mode = "off"
offOA.Reasoning = offReasoning
ApplyToEinoChatModelConfig(cfg, &offOA, nil)
mergeExtraRequestFields(cfg, oa.Reasoning.ExtraRequestFields)
clearReasoningFromChatModelConfig(cfg)
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)
}
}
func clearReasoningFromChatModelConfig(cfg *einoopenai.ChatModelConfig) {
@@ -51,8 +52,22 @@ func clearReasoningFromChatModelConfig(cfg *einoopenai.ChatModelConfig) {
for _, key := range []string{"thinking", "reasoning_effort", "output_config", "reasoning"} {
delete(cfg.ExtraFields, key)
}
if len(cfg.ExtraFields) == 0 {
cfg.ExtraFields = nil
}
}
}
func mergeExtraRequestFields(cfg *einoopenai.ChatModelConfig, fields map[string]interface{}) {
if cfg == nil || len(fields) == 0 {
return
}
if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any, len(fields))
}
for k, v := range fields {
cfg.ExtraFields[k] = v
}
applyThinkingDisabled(cfg)
}
// ApplyToEinoChatModelConfig merges reasoning-related options into cfg.
@@ -65,42 +80,32 @@ func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.Open
allowClient := sr.AllowClientReasoningEffective()
mode := effectiveMode(sr, client, allowClient)
// Admin-defined root fields are independent of the selected reasoning wire
// profile. Merge them first so mode=off can remove only reasoning controls
// while preserving unrelated gateway options.
mergeExtraRequestFields(cfg, sr.ExtraRequestFields)
if mode == "off" {
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 {
applyThinkingDisabled(cfg)
}
return
}
// Claude (Anthropic): merge admin extras first; optional extended thinking maps to top-level `thinking`
// (see internal/openai convertOpenAIToClaude). DeepSeek/OpenAI-style fields are not sent.
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") ||
strings.EqualFold(strings.TrimSpace(oa.Provider), "anthropic") {
if len(sr.ExtraRequestFields) > 0 {
if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any)
}
for k, v := range sr.ExtraRequestFields {
cfg.ExtraFields[k] = v
}
}
if mode == "off" {
return
}
applyClaudeExtendedThinking(cfg, mode, effectiveEffort(sr, client, allowClient), oa.Model)
return
}
if mode == "off" {
applyThinkingDisabled(cfg)
return
}
effort := effectiveEffort(sr, client, allowClient)
prof := resolveWireProfile(oa, sr)
// Admin-defined extra root fields (merged first; automatic keys may follow).
if len(sr.ExtraRequestFields) > 0 {
if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any)
}
for k, v := range sr.ExtraRequestFields {
cfg.ExtraFields[k] = v
}
}
switch prof {
case wireClaude, wireNone:
return
@@ -222,7 +227,8 @@ func usesExtraFieldsReasoningEffort(e string) bool {
}
func resolveWireProfile(oa *config.OpenAIConfig, sr *config.OpenAIReasoningConfig) wireProfile {
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
provider := strings.TrimSpace(oa.Provider)
if strings.EqualFold(provider, "claude") || strings.EqualFold(provider, "anthropic") {
return wireClaude
}
p := strings.ToLower(strings.TrimSpace(sr.ProfileEffective()))
@@ -252,9 +258,6 @@ func applyThinkingDisabled(cfg *einoopenai.ChatModelConfig) {
if cfg.ExtraFields == nil {
cfg.ExtraFields = make(map[string]any)
}
if _, exists := cfg.ExtraFields["thinking"]; exists {
return
}
cfg.ExtraFields["thinking"] = map[string]any{"type": "disabled"}
}
+129 -17
View File
@@ -1,13 +1,33 @@
package reasoning
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"cyberstrike-ai/internal/config"
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/schema"
)
var reasoningPayloadKeysForTest = []string{"thinking", "reasoning_effort", "output_config", "reasoning"}
func assertNoReasoningFields(t *testing.T, cfg *einoopenai.ChatModelConfig) {
t.Helper()
if cfg.ReasoningEffort != "" {
t.Fatalf("expected ReasoningEffort omitted, got %q", cfg.ReasoningEffort)
}
for _, key := range reasoningPayloadKeysForTest {
if _, ok := cfg.ExtraFields[key]; ok {
t.Fatalf("expected %q omitted, got %#v", key, cfg.ExtraFields)
}
}
}
func TestEffortStringForAPI_passthrough(t *testing.T) {
cases := map[string]string{
"max": "max",
@@ -50,7 +70,11 @@ func TestApplyOpenAICompat_xhighExtraField(t *testing.T) {
}
func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{}
cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{
"thinking": map[string]any{"type": "enabled"},
"reasoning_effort": "high",
"vendor_option": true,
}}
oa := &config.OpenAIConfig{
BaseURL: "https://antchat.example.com/v1",
Model: "minimax-m3",
@@ -61,31 +85,119 @@ func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testi
},
}
ApplyPlanExecutePlannerModelConfig(cfg, oa)
if cfg.ReasoningEffort != "" {
t.Fatalf("expected ReasoningEffort cleared, got %q", cfg.ReasoningEffort)
}
th, ok := cfg.ExtraFields["thinking"].(map[string]any)
if !ok || th["type"] != "disabled" {
t.Fatalf("expected thinking disabled, got %#v", cfg.ExtraFields)
}
if _, ok := cfg.ExtraFields["reasoning_effort"]; ok {
t.Fatalf("expected reasoning_effort stripped, got %#v", cfg.ExtraFields)
assertNoReasoningFields(t, cfg)
if cfg.ExtraFields["vendor_option"] != true {
t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields)
}
}
func TestApplyReasoningOff_disablesThinking(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{}
func TestApplyReasoningOff_omitsAllReasoningFields(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{ExtraFields: map[string]any{
"thinking": map[string]any{"type": "enabled"},
"output_config": map[string]any{"effort": "high"},
}}
oa := &config.OpenAIConfig{
BaseURL: "https://api.openai.com/v1",
Model: "gpt-4o",
Model: "gpt-4o-mini",
Reasoning: config.OpenAIReasoningConfig{
Mode: "off",
Mode: "off",
Effort: "high",
Profile: "openai_compat",
ExtraRequestFields: map[string]interface{}{
"thinking": map[string]any{"type": "disabled"},
"reasoning": map[string]any{"effort": "high"},
"vendor_option": true,
},
},
}
ApplyToEinoChatModelConfig(cfg, oa, nil)
th, ok := cfg.ExtraFields["thinking"].(map[string]any)
if !ok || th["type"] != "disabled" {
t.Fatalf("expected thinking disabled, got %#v", cfg.ExtraFields)
assertNoReasoningFields(t, cfg)
if cfg.ExtraFields["vendor_option"] != true {
t.Fatalf("expected unrelated extra field preserved, got %#v", cfg.ExtraFields)
}
}
func TestApplyReasoningOff_clientOverrideOmit(t *testing.T) {
cfg := &einoopenai.ChatModelConfig{}
oa := &config.OpenAIConfig{Reasoning: config.OpenAIReasoningConfig{
Mode: "on", Effort: "high", Profile: "openai_compat",
}}
ApplyToEinoChatModelConfig(cfg, oa, &ClientIntent{Mode: "off", Effort: "high"})
assertNoReasoningFields(t, cfg)
}
func TestApplyReasoningOff_deepseekExplicitlyDisablesDefaultThinking(t *testing.T) {
for _, profile := range []string{"deepseek_compat", "auto"} {
t.Run(profile, func(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: profile,
},
}
ApplyToEinoChatModelConfig(cfg, oa, nil)
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, got %#v", cfg.ExtraFields)
}
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) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("read request body: %v", err)
}
if err := json.Unmarshal(body, &requestBody); err != nil {
t.Errorf("decode request body: %v; body=%s", err, body)
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"gpt-4o-mini","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)
}))
defer srv.Close()
cfg := &einoopenai.ChatModelConfig{
APIKey: "test-key",
BaseURL: srv.URL,
Model: "gpt-4o-mini",
}
oa := &config.OpenAIConfig{
BaseURL: "https://api.openai.com/v1",
Model: "gpt-4o-mini",
Reasoning: config.OpenAIReasoningConfig{
Mode: "off", Effort: "high", Profile: "openai_compat",
},
}
ApplyToEinoChatModelConfig(cfg, oa, nil)
model, err := einoopenai.NewChatModel(context.Background(), cfg)
if err != nil {
t.Fatalf("new chat model: %v", err)
}
if _, err := model.Generate(context.Background(), []*schema.Message{schema.UserMessage("hello")}); err != nil {
t.Fatalf("generate: %v", err)
}
for _, key := range reasoningPayloadKeysForTest {
if _, ok := requestBody[key]; ok {
t.Fatalf("wire payload unexpectedly contains %q: %#v", key, requestBody)
}
}
}