mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-18 09:57:35 +02:00
Add files via upload
This commit is contained in:
@@ -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 | xhigh;max/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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user