mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-30 17:55:32 +02:00
Add files via upload
This commit is contained in:
@@ -1,35 +1,12 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"github.com/bytedance/sonic"
|
||||
copenai "cyberstrike-ai/internal/openai"
|
||||
)
|
||||
|
||||
// stripReasoningFromSummarizationPayload removes thinking / reasoning fields from a
|
||||
// chat-completions JSON body. Applied only to summarization Generate calls via
|
||||
// model.ModelOptions on the shared ChatModel — main-agent requests are unchanged.
|
||||
func stripReasoningFromSummarizationPayload(rawBody []byte) ([]byte, error) {
|
||||
var payload map[string]any
|
||||
if err := sonic.Unmarshal(rawBody, &payload); err != nil {
|
||||
return rawBody, nil
|
||||
}
|
||||
changed := false
|
||||
for _, key := range []string{
|
||||
"thinking",
|
||||
"reasoning_effort",
|
||||
"output_config",
|
||||
"reasoning",
|
||||
} {
|
||||
if _, ok := payload[key]; ok {
|
||||
delete(payload, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return rawBody, nil
|
||||
}
|
||||
out, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
return rawBody, err
|
||||
}
|
||||
return out, nil
|
||||
return copenai.StripReasoningFromChatCompletionBody(rawBody)
|
||||
}
|
||||
|
||||
@@ -432,6 +432,22 @@ func RunDeepAgent(
|
||||
var da adk.Agent
|
||||
switch orchMode {
|
||||
case "plan_execute":
|
||||
plannerModelCfg := &einoopenai.ChatModelConfig{
|
||||
APIKey: appCfg.OpenAI.APIKey,
|
||||
BaseURL: strings.TrimSuffix(appCfg.OpenAI.BaseURL, "/"),
|
||||
Model: appCfg.OpenAI.Model,
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
reasoning.ApplyPlanExecutePlannerModelConfig(plannerModelCfg, &appCfg.OpenAI)
|
||||
peMainModel, perr := einoopenai.NewChatModel(ctx, plannerModelCfg)
|
||||
if perr != nil {
|
||||
return nil, fmt.Errorf("plan_execute 规划模型: %w", perr)
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Info("plan_execute: planner/replanner 使用无 reasoning 的独立 ChatModel(ToolChoiceForced 兼容)",
|
||||
zap.String("model", appCfg.OpenAI.Model),
|
||||
)
|
||||
}
|
||||
execModel, perr := einoopenai.NewChatModel(ctx, baseModelCfg)
|
||||
if perr != nil {
|
||||
return nil, fmt.Errorf("plan_execute 执行器模型: %w", perr)
|
||||
@@ -445,7 +461,7 @@ func RunDeepAgent(
|
||||
}
|
||||
}
|
||||
peRoot, perr := NewPlanExecuteRoot(ctx, &PlanExecuteRootArgs{
|
||||
MainToolCallingModel: mainModel,
|
||||
MainToolCallingModel: peMainModel,
|
||||
ExecModel: execModel,
|
||||
OrchInstruction: orchInstruction,
|
||||
ToolsCfg: mainToolsCfg,
|
||||
|
||||
@@ -806,10 +806,12 @@ func isClaudeProvider(cfg *config.OpenAIConfig) bool {
|
||||
// Eino HTTP Client Bridge
|
||||
// ============================================================
|
||||
|
||||
// NewEinoHTTPClient 为 einoopenai.ChatModelConfig 返回一个 http.Client,包含两层 transport 包装:
|
||||
// 1. 当 cfg.Provider 为 claude 时,最内层套 claudeRoundTripper,把 OpenAI /chat/completions 透明
|
||||
// NewEinoHTTPClient 为 einoopenai.ChatModelConfig 返回一个 http.Client,包含多层 transport 包装:
|
||||
// 1. 当 cfg.Provider 为 claude 时,套 claudeRoundTripper,把 OpenAI /chat/completions 透明
|
||||
// 桥接为 Anthropic /v1/messages(并把 Claude SSE 翻译回 OpenAI SSE 格式)。
|
||||
// 2. 最外层无条件套 einoSSESanitizingRoundTripper,吞掉中转站发的 SSE 心跳/注释/控制行
|
||||
// 2. reasoningToolChoiceCompatRoundTripper:tool_choice=required/object 时剥离 thinking 字段,避免
|
||||
// plan_execute replanner 等强制工具调用与推理模式冲突(部分网关返回 400)。
|
||||
// 3. 最外层无条件套 einoSSESanitizingRoundTripper,吞掉中转站发的 SSE 心跳/注释/控制行
|
||||
// (": keepalive" / "event: ping" / "retry: 3000" 等),避免 Eino 用的 meguminnnnnnnnn/go-openai
|
||||
// SDK 在累计超过 300 个非 "data:" 行后抛 "stream has sent too many empty messages"。
|
||||
//
|
||||
@@ -825,6 +827,7 @@ func NewEinoHTTPClient(cfg *config.OpenAIConfig, base *http.Client) *http.Client
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
transport = &reasoningToolChoiceCompatRoundTripper{base: transport}
|
||||
if isClaudeProvider(cfg) {
|
||||
transport = &claudeRoundTripper{
|
||||
base: transport,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// reasoningPayloadKeys are OpenAI-compatible root fields that enable "thinking" /
|
||||
// extended-reasoning modes on gateways such as DashScope/Qwen and MiniMax.
|
||||
var reasoningPayloadKeys = []string{
|
||||
"thinking",
|
||||
"reasoning_effort",
|
||||
"output_config",
|
||||
"reasoning",
|
||||
}
|
||||
|
||||
// StripReasoningFromChatCompletionBody removes thinking / reasoning fields from a
|
||||
// chat-completions JSON body.
|
||||
func StripReasoningFromChatCompletionBody(rawBody []byte) ([]byte, error) {
|
||||
var payload map[string]any
|
||||
if err := sonic.Unmarshal(rawBody, &payload); err != nil {
|
||||
return rawBody, nil
|
||||
}
|
||||
if !stripReasoningFields(payload) {
|
||||
return rawBody, nil
|
||||
}
|
||||
out, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
return rawBody, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// StripReasoningIfForcedToolChoice removes thinking / reasoning fields when the
|
||||
// request sets tool_choice to "required" or an object. Several providers reject
|
||||
// that combination (e.g. DashScope: "tool_choice does not support being set to
|
||||
// required or object in thinking mode").
|
||||
func StripReasoningIfForcedToolChoice(rawBody []byte) ([]byte, error) {
|
||||
var payload map[string]any
|
||||
if err := sonic.Unmarshal(rawBody, &payload); err != nil {
|
||||
return rawBody, nil
|
||||
}
|
||||
if !forcedToolChoiceIncompatibleWithThinking(payload) {
|
||||
return rawBody, nil
|
||||
}
|
||||
if !stripReasoningFields(payload) {
|
||||
return rawBody, nil
|
||||
}
|
||||
out, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
return rawBody, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func stripReasoningFields(payload map[string]any) bool {
|
||||
changed := false
|
||||
for _, key := range reasoningPayloadKeys {
|
||||
if _, ok := payload[key]; ok {
|
||||
delete(payload, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func forcedToolChoiceIncompatibleWithThinking(payload map[string]any) bool {
|
||||
tc, ok := payload["tool_choice"]
|
||||
if !ok || tc == nil {
|
||||
return false
|
||||
}
|
||||
switch v := tc.(type) {
|
||||
case string:
|
||||
return v == "required"
|
||||
case map[string]any:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStripReasoningFromChatCompletionBody(t *testing.T) {
|
||||
in := []byte(`{"model":"deepseek-chat","messages":[],"thinking":{"type":"enabled"},"reasoning_effort":"high"}`)
|
||||
out, err := StripReasoningFromChatCompletionBody(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(out)
|
||||
if strings.Contains(s, "thinking") || strings.Contains(s, "reasoning_effort") {
|
||||
t.Fatalf("expected reasoning fields stripped, got %s", s)
|
||||
}
|
||||
if !strings.Contains(s, `"model":"deepseek-chat"`) {
|
||||
t.Fatalf("expected model preserved, got %s", s)
|
||||
}
|
||||
|
||||
plain := []byte(`{"model":"gpt-4o","messages":[]}`)
|
||||
out2, err := StripReasoningFromChatCompletionBody(plain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out2) != string(plain) {
|
||||
t.Fatalf("expected unchanged payload, got %s", out2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripReasoningIfForcedToolChoice(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
strip bool
|
||||
contain string
|
||||
}{
|
||||
{
|
||||
name: "required strips thinking",
|
||||
in: `{"model":"minimax","messages":[],"thinking":{"type":"enabled"},"tool_choice":"required","tools":[]}`,
|
||||
strip: true,
|
||||
},
|
||||
{
|
||||
name: "object tool_choice strips thinking",
|
||||
in: `{"model":"qwen","messages":[],"thinking":{"type":"enabled"},"tool_choice":{"type":"function","function":{"name":"respond"}}}`,
|
||||
strip: true,
|
||||
},
|
||||
{
|
||||
name: "auto keeps thinking",
|
||||
in: `{"model":"qwen","messages":[],"thinking":{"type":"enabled"},"tool_choice":"auto"}`,
|
||||
strip: false,
|
||||
contain: "thinking",
|
||||
},
|
||||
{
|
||||
name: "no tool_choice keeps thinking",
|
||||
in: `{"model":"qwen","messages":[],"thinking":{"type":"enabled"}}`,
|
||||
strip: false,
|
||||
contain: "thinking",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, err := StripReasoningIfForcedToolChoice([]byte(tc.in))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(out)
|
||||
hasThinking := strings.Contains(s, "thinking")
|
||||
if tc.strip && hasThinking {
|
||||
t.Fatalf("expected thinking stripped, got %s", s)
|
||||
}
|
||||
if !tc.strip && tc.contain != "" && !strings.Contains(s, tc.contain) {
|
||||
t.Fatalf("expected %q in %s", tc.contain, s)
|
||||
}
|
||||
if !tc.strip && string(out) != tc.in {
|
||||
t.Fatalf("expected unchanged payload, got %s", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReasoningToolChoiceCompatRoundTripper(t *testing.T) {
|
||||
var gotBody string
|
||||
rt := &reasoningToolChoiceCompatRoundTripper{
|
||||
base: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
b, _ := io.ReadAll(req.Body)
|
||||
gotBody = string(b)
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(`{"choices":[{"message":{"content":"ok"}}]}`)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com/v1/chat/completions", strings.NewReader(
|
||||
`{"model":"m","thinking":{"type":"enabled"},"tool_choice":"required","messages":[]}`,
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(gotBody, "thinking") {
|
||||
t.Fatalf("expected thinking stripped in transit, got %s", gotBody)
|
||||
}
|
||||
if !strings.Contains(gotBody, `"tool_choice":"required"`) {
|
||||
t.Fatalf("expected tool_choice preserved, got %s", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// reasoningToolChoiceCompatRoundTripper strips thinking/reasoning fields from
|
||||
// chat/completions requests that force tool_choice, which some gateways reject
|
||||
// when thinking mode is enabled on the same request.
|
||||
type reasoningToolChoiceCompatRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt *reasoningToolChoiceCompatRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if rt == nil || rt.base == nil || req == nil || req.Body == nil {
|
||||
if rt != nil && rt.base != nil {
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
if req.Method != http.MethodPost || !strings.HasSuffix(req.URL.Path, "/chat/completions") {
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
_ = req.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patched, perr := StripReasoningIfForcedToolChoice(body)
|
||||
if perr != nil {
|
||||
patched = body
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(patched))
|
||||
req.ContentLength = int64(len(patched))
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(patched)))
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
@@ -26,6 +26,35 @@ const (
|
||||
wireOutputConfig
|
||||
)
|
||||
|
||||
// ApplyPlanExecutePlannerModelConfig configures the plan_execute planner/replanner
|
||||
// ChatModel. Those Eino agents call WithToolChoice(Forced); several gateways reject
|
||||
// thinking / reasoning fields on the same request (tool_choice required/object).
|
||||
// Executor should keep the normal ApplyToEinoChatModelConfig path.
|
||||
func ApplyPlanExecutePlannerModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.OpenAIConfig) {
|
||||
if cfg == nil || oa == nil {
|
||||
return
|
||||
}
|
||||
offOA := *oa
|
||||
offReasoning := oa.Reasoning
|
||||
offReasoning.Mode = "off"
|
||||
offOA.Reasoning = offReasoning
|
||||
ApplyToEinoChatModelConfig(cfg, &offOA, nil)
|
||||
clearReasoningFromChatModelConfig(cfg)
|
||||
}
|
||||
|
||||
func clearReasoningFromChatModelConfig(cfg *einoopenai.ChatModelConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.ReasoningEffort = ""
|
||||
if cfg.ExtraFields != nil {
|
||||
for _, key := range []string{"thinking", "reasoning_effort", "output_config", "reasoning"} {
|
||||
delete(cfg.ExtraFields, key)
|
||||
}
|
||||
}
|
||||
applyThinkingDisabled(cfg)
|
||||
}
|
||||
|
||||
// ApplyToEinoChatModelConfig merges reasoning-related options into cfg.
|
||||
// Precondition: cfg already has APIKey, BaseURL, Model, HTTPClient set.
|
||||
func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.OpenAIConfig, client *ClientIntent) {
|
||||
|
||||
@@ -49,6 +49,30 @@ func TestApplyOpenAICompat_xhighExtraField(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPlanExecutePlannerModelConfig_stripsReasoningWhenGlobalOn(t *testing.T) {
|
||||
cfg := &einoopenai.ChatModelConfig{}
|
||||
oa := &config.OpenAIConfig{
|
||||
BaseURL: "https://antchat.example.com/v1",
|
||||
Model: "minimax-m3",
|
||||
Reasoning: config.OpenAIReasoningConfig{
|
||||
Profile: "openai_compat",
|
||||
Mode: "on",
|
||||
Effort: "high",
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyReasoningOff_disablesThinking(t *testing.T) {
|
||||
cfg := &einoopenai.ChatModelConfig{}
|
||||
oa := &config.OpenAIConfig{
|
||||
|
||||
Reference in New Issue
Block a user