diff --git a/llmrec/llmrec.go b/llmrec/llmrec.go index d4c3d43..1bbd55c 100644 --- a/llmrec/llmrec.go +++ b/llmrec/llmrec.go @@ -41,16 +41,25 @@ func TaskIDFrom(ctx context.Context) string { type Recorder struct { inner llm.Provider pg *db.DB - model string // model name (from config, not in CompletionRequest) - prof string // LLM profile name (from llm_profiles) - enabled func() bool // reports whether recording is currently on; nil = always record + model string // model name (from config, not in CompletionRequest) + prof string // LLM profile name (from llm_profiles) + // thinkingType / reasoningEffort 是配置级思考参数(思考开关 / 思考强度)。它们在 + // norma 的 buildBody() 里从 provider 配置注入真正的 HTTP body,不出现在 + // CompletionRequest 上,故 Recorder 需在此单独带一份,序列化时写进录制。 + thinkingType string + reasoningEffort string + enabled func() bool // reports whether recording is currently on; nil = always record } // Wrap returns a Provider that records calls to pg when enabled() reports true. -// profName is the LLM profile name (e.g. "default"); may be empty. A nil enabled -// predicate records unconditionally (backward-compatible default). -func Wrap(inner llm.Provider, pg *db.DB, model, profName string, enabled func() bool) *Recorder { - return &Recorder{inner: inner, pg: pg, model: model, prof: profName, enabled: enabled} +// profName is the LLM profile name (e.g. "default"); may be empty. thinkingType / +// reasoningEffort are the config-level thinking params actually sent to the API +// (empty = not sent). A nil enabled predicate records unconditionally. +func Wrap(inner llm.Provider, pg *db.DB, model, profName, thinkingType, reasoningEffort string, enabled func() bool) *Recorder { + return &Recorder{ + inner: inner, pg: pg, model: model, prof: profName, + thinkingType: thinkingType, reasoningEffort: reasoningEffort, enabled: enabled, + } } // parseSession extracts task id and worker role from session strings like @@ -103,7 +112,7 @@ func (r *Recorder) Stream(ctx context.Context, req llm.CompletionRequest) iter.S // Serialize the request only when storing bodies (this is the expensive part). reqBody := "" if recordBodies { - reqBody = serializeRequest(req) + reqBody = r.serializeRequest(req) } return func(yield func(llm.StreamEvent, error) bool) { @@ -255,12 +264,22 @@ func (r *Recorder) record(req llm.CompletionRequest, session, taskID, worker, re } // serializeRequest builds a JSON representation of the completion request. -func serializeRequest(req llm.CompletionRequest) string { +func (r *Recorder) serializeRequest(req llm.CompletionRequest) string { m := map[string]any{ "system": req.System, "messages": req.Messages, "max_tokens": req.MaxTokens, } + // 记录本次调用实际发出的思考参数。type 采用「有效值」:每请求覆盖 req.Thinking + // 优先于配置级 thinkingType(与 norma buildBody 的判定一致,如 compaction 摘要会 + // 强制 disabled);effort 无每请求覆盖,直接取配置值。两者皆空则不写 thinking 字段。 + effType := r.thinkingType + if req.Thinking != "" { + effType = req.Thinking + } + if effType != "" || r.reasoningEffort != "" { + m["thinking"] = map[string]string{"type": effType, "effort": r.reasoningEffort} + } if len(req.Tools) > 0 { // Store tool names only (full schemas are huge). names := make([]string, len(req.Tools)) diff --git a/server/server.go b/server/server.go index 36895b6..fefd45e 100644 --- a/server/server.go +++ b/server/server.go @@ -410,7 +410,7 @@ func (s *Server) applyLLM(cfg agent.Config) error { s.cfgMu.Lock() profName := s.llmProf s.cfgMu.Unlock() - prov = llmrec.Wrap(prov, s.m.PG(), cfg.Model, profName, s.m.LLMRecordEnabled) + prov = llmrec.Wrap(prov, s.m.PG(), cfg.Model, profName, cfg.ThinkingType, cfg.ReasoningEffort, s.m.LLMRecordEnabled) } // LLM 轮询(默认关):把激活配置包进故障转移链,当前配置不可用时自动切下一个。 // 只影响「走全局激活配置」的这条路径——agent 绑定 / 任务 pin 的走 providerForProfile, @@ -504,7 +504,7 @@ func (s *Server) providerForProfile(id int64) (llm.Provider, agent.Config, bool) } // Wrap with recorder, tagged with this profile's name. if p, _ := s.m.pg.ProfileByID(id); p != nil { - prov = llmrec.Wrap(prov, s.m.PG(), cfg.Model, p.Name, s.m.LLMRecordEnabled) + prov = llmrec.Wrap(prov, s.m.PG(), cfg.Model, p.Name, cfg.ThinkingType, cfg.ReasoningEffort, s.m.LLMRecordEnabled) } s.provCacheMu.Lock() if generation != s.provCacheGen {