mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-11-13 15:57:22 +01:00
Fix audit model thinking compatibility
This commit is contained in:
1 parent
5affb4df1b
commit
bc0b288d40
8 files changed
+172
-2
No files matched your search
@@ -10,6 +10,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 修复审计 Agent 向不支持 thinking 字段的兼容网关发起请求失败的问题;支持独立兼容 profile,并按模型和地址识别默认行为。
|
||||
|
||||
- 修复已知支持 `none` 的官方 OpenAI 模型在关闭推理后仍使用服务端默认推理的问题(#366):按提供商、端点和模型发送并保留显式关闭参数,覆盖 Planner 和强制工具选择;保留 DeepSeek 与第三方兼容网关行为,并明确提示需要 Responses 的工具调用限制。
|
||||
|
||||
- 防止内置管理员失权:拒绝停用内置 `admin` 或移除其系统管理员角色,失败时保留原资料、权限和会话;界面同步禁用对应操作。
|
||||
|
||||
@@ -201,6 +201,8 @@ hitl:
|
||||
base_url: "" # openai 后端留空跟随主模型;typesafe 后端留空使用 https://api.typesafe.ai
|
||||
api_key: "" # openai 后端留空跟随主模型;typesafe 后端填写 TypeSafe API Key
|
||||
model: "" # openai 后端建议填小模型;typesafe 后端留空使用 jev-latest
|
||||
reasoning:
|
||||
profile: "" # auto / deepseek_compat / openai_compat;仅控制审计请求的 thinking 字段
|
||||
# 已决策审计日志保留天数(与 MCP 监控一致;省略默认 90;0 表示不自动清理)
|
||||
retention_days: 90
|
||||
# 按你环境里的真实工具名增删(与侧栏一致、小写不敏感);不需要全局免审批可改为 []
|
||||
|
||||
@@ -1230,6 +1230,12 @@ func (h HitlConfig) TypeSafeConfigEffective() (baseURL, apiKey, model string) {
|
||||
func (h HitlConfig) AuditModelEffective(main OpenAIConfig) OpenAIConfig {
|
||||
out := main
|
||||
am := h.AuditModel
|
||||
if strings.TrimSpace(am.Provider) != "" || strings.TrimSpace(am.BaseURL) != "" || strings.TrimSpace(am.Model) != "" {
|
||||
out.Reasoning.Profile = ""
|
||||
}
|
||||
if strings.TrimSpace(am.Reasoning.Profile) != "" {
|
||||
out.Reasoning.Profile = am.Reasoning.Profile
|
||||
}
|
||||
if strings.TrimSpace(am.Provider) != "" {
|
||||
out.Provider = strings.TrimSpace(am.Provider)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAuditModelProfileInheritance(t *testing.T) {
|
||||
main := OpenAIConfig{Reasoning: OpenAIReasoningConfig{Profile: "deepseek_compat"}}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
audit OpenAIConfig
|
||||
want string
|
||||
}{
|
||||
{"inherit", OpenAIConfig{}, "deepseek_compat"},
|
||||
{"key alone inherits", OpenAIConfig{APIKey: "other"}, "deepseek_compat"},
|
||||
{"model isolates", OpenAIConfig{Model: "glm"}, ""},
|
||||
{"url isolates", OpenAIConfig{BaseURL: "https://example.com"}, ""},
|
||||
{"provider isolates", OpenAIConfig{Provider: "openai"}, ""},
|
||||
{"explicit overrides", OpenAIConfig{Reasoning: OpenAIReasoningConfig{Profile: "openai_compat"}}, "openai_compat"},
|
||||
{"explicit auto overrides", OpenAIConfig{Reasoning: OpenAIReasoningConfig{Profile: "auto"}}, "auto"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := (HitlConfig{AuditModel: tc.audit}).AuditModelEffective(main).Reasoning.Profile
|
||||
if got != tc.want {
|
||||
t.Fatalf("profile=%q want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2359,6 +2359,7 @@ func updateHitlConfig(doc *yaml.Node, cfg config.HitlConfig) {
|
||||
setStringInMap(auditModelNode, "base_url", cfg.AuditModel.BaseURL)
|
||||
setStringInMap(auditModelNode, "api_key", cfg.AuditModel.APIKey)
|
||||
setStringInMap(auditModelNode, "model", cfg.AuditModel.Model)
|
||||
setStringInMap(ensureMap(auditModelNode, "reasoning"), "profile", cfg.AuditModel.Reasoning.Profile)
|
||||
// flow 样式 [a, b, c] 单行展示,工具多时比块序列省行数
|
||||
setFlowStringSliceInMap(hitlNode, "tool_whitelist", cfg.ToolWhitelist)
|
||||
setStringInMap(hitlNode, "default_mode", cfg.EffectiveDefaultMode())
|
||||
|
||||
@@ -51,8 +51,9 @@ func (h *AgentHandler) auditAgentReview(ctx context.Context, hitlMode, toolName
|
||||
},
|
||||
"temperature": 0.1,
|
||||
"max_completion_tokens": 1024,
|
||||
// 审计裁决需要结构化 JSON;关闭 thinking 避免 Qwen 等把正文放进 reasoning_content 导致解析失败。
|
||||
"thinking": map[string]interface{}{"type": "disabled"},
|
||||
}
|
||||
if auditDisableThinking(llmCfg) {
|
||||
requestBody["thinking"] = map[string]interface{}{"type": "disabled"}
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
)
|
||||
|
||||
// auditDisableThinking selects only the audit request's provider-specific field.
|
||||
func auditDisableThinking(cfg config.OpenAIConfig) bool {
|
||||
switch cfg.Reasoning.ProfileEffective() {
|
||||
case "deepseek", "deepseek_compat":
|
||||
return true
|
||||
case "auto":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
identity := strings.ToLower(cfg.Model + " " + cfg.BaseURL)
|
||||
if strings.Contains(identity, "glm") {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(identity, "deepseek") || strings.Contains(identity, "qwen")
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestAuditAgentRequestCompatibility(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, model, profile, mainProfile string
|
||||
wantThinking, fail bool
|
||||
}{
|
||||
{name: "glm", model: "tencent:glm-5.3"},
|
||||
{name: "deepseek", model: "deepseek-chat", wantThinking: true},
|
||||
{name: "qwen", model: "qwen-plus", wantThinking: true},
|
||||
{name: "unknown", model: "other"},
|
||||
{name: "explicit deepseek", model: "glm-5", profile: "deepseek_compat", wantThinking: true},
|
||||
{name: "explicit openai", model: "deepseek-chat", profile: "openai_compat"},
|
||||
{name: "isolated main profile", model: "glm-5", mainProfile: "deepseek_compat"},
|
||||
{name: "failure is rejected", model: "glm-5", fail: true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
calls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
thinking, present := body["thinking"]
|
||||
if present != tc.wantThinking {
|
||||
t.Errorf("thinking present=%v, want %v", present, tc.wantThinking)
|
||||
}
|
||||
if present {
|
||||
v, ok := thinking.(map[string]any)
|
||||
if !ok || v["type"] != "disabled" {
|
||||
t.Errorf("thinking=%v", thinking)
|
||||
}
|
||||
}
|
||||
if body["max_completion_tokens"] != float64(1024) || body["temperature"] != 0.1 {
|
||||
t.Errorf("unexpected request limits: %v", body)
|
||||
}
|
||||
if tc.fail {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"{\"decision\":\"approve\"}"}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
h := &AgentHandler{logger: zap.NewNop(), config: &config.Config{OpenAI: config.OpenAIConfig{APIKey: "test", Model: "main", Reasoning: config.OpenAIReasoningConfig{Profile: tc.mainProfile}}, Hitl: config.HitlConfig{AuditModel: config.OpenAIConfig{BaseURL: server.URL, Model: tc.model, Reasoning: config.OpenAIReasoningConfig{Profile: tc.profile}}}}}
|
||||
decision := h.auditAgentReview(context.Background(), "approval", "test", nil)
|
||||
want := "approve"
|
||||
if tc.fail {
|
||||
want = "reject"
|
||||
}
|
||||
if decision.Decision != want || calls != 1 {
|
||||
t.Fatalf("decision=%+v calls=%d", decision, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditThinkingDetectionAddressAndPrecedence(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
model, url, profile string
|
||||
want bool
|
||||
}{
|
||||
{"other", "https://deepseek.example/v1", "", true},
|
||||
{"other", "https://qwen.example/v1", "", true},
|
||||
{"glm-5", "https://deepseek.example/v1", "", false},
|
||||
{"deepseek", "https://example/v1", "openai_compat", false},
|
||||
} {
|
||||
got := auditDisableThinking(config.OpenAIConfig{Model: tc.model, BaseURL: tc.url, Reasoning: config.OpenAIReasoningConfig{Profile: tc.profile}})
|
||||
if got != tc.want {
|
||||
t.Errorf("model=%s url=%s profile=%s got %v", tc.model, tc.url, tc.profile, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditProfileConfigPersistence(t *testing.T) {
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal([]byte("hitl:\n audit_model:\n reasoning:\n profile: openai_compat\n"), &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updateHitlConfig(&doc, config.HitlConfig{AuditModel: config.OpenAIConfig{Reasoning: config.OpenAIReasoningConfig{Profile: "deepseek_compat"}}})
|
||||
raw, err := yaml.Marshal(&doc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded config.Config
|
||||
if err := yaml.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := decoded.Hitl.AuditModel.Reasoning.Profile; got != "deepseek_compat" {
|
||||
t.Fatalf("profile=%q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user