feat(llm): 模型配置新增输出上限,支持 max_completion_tokens 字段名
llm_profiles 加两列:max_tokens(单次回复输出上限,0=不发送该字段,由服务端 默认值决定)与 max_tokens_field(上限发哪个键)。后者仅对 format=openai 生效: 空(默认)= max_tokens;'max_completion_tokens' = 新键。OpenAI 推理模型 (o 系列/GPT-5)拒收 max_tokens、只认新键,而多数兼容网关反过来只认旧键, 故做成显式开关而非自动推断。anthropic 与 openai-responses 自带字段名, 存下来只会误导,保存时一并清空。 顺带补上一处从未接通的链路:planner/worker/chat/mainagent/goals 五个 agentcore.Options 都没设过 MaxTokens,恒为 0——openai 系因 omitempty 压根 不发上限,anthropic 走 norma 的 8192 兜底。光加配置项而不接这条线,用户在 后台填了数字也不会生效,故本次一并接通,取值与 streaming 一样按 profile 每轮解析,故障转移切换后下一轮即生效。 task_llm.go 里 nonStreaming() 的来源优先级(agent 绑定 → 任务链激活 profile → 全局)抽成共用的 activeCfg(),避免 maxTokens() 再抄一份同样的三级回退。 norma 升到 v0.3.3(该版本提供 Config.MaxTokensField)。go.mod 只动 norma 这 一行:仓库此前未 tidy 过,跑 go mod tidy 会顺带升 x/crypto、x/net 等一批无关 依赖,不混进本次改动。 迁移:新列走 ADD COLUMN IF NOT EXISTS + 默认 0/'',旧库升级后行为完全不变; CHECK 约束限定取值,每次启动先删后建保持幂等。
This commit is contained in:
1 parent
338e77c064
commit
7cc467c159
20 files changed
+364
-33
No files matched your search
+20
-3
@@ -312,6 +312,7 @@ func (s *Server) loadLLMConfig() (agent.Config, bool) {
|
||||
cfg.ThinkingType = p.ThinkingType
|
||||
cfg.ReasoningEffort = p.ReasoningEffort
|
||||
cfg.Stream = p.Streaming
|
||||
cfg.MaxTokens, cfg.MaxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
if cfg.APIKey == "" {
|
||||
return cfg, false
|
||||
}
|
||||
@@ -327,16 +328,19 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error {
|
||||
// (anthropic / openai / openai-responses), matching the DB CHECK constraint.
|
||||
format := cfg.Provider()
|
||||
var id int64
|
||||
// agent.Config carries no failover fields, so carry the stored ones forward —
|
||||
// otherwise this legacy endpoint would silently reset the profile's priority,
|
||||
// pool_exclude, and streaming to zero values on every save.
|
||||
// 这个 legacy 端点的请求体不含轮询/收发/输出上限参数,故把库里已存的值原样带回 —
|
||||
// 否则每次保存都会把 profile 的 priority、pool_exclude、streaming 以及输出上限
|
||||
// (max_tokens / max_tokens_field)悄悄重置成零值。
|
||||
var priority int
|
||||
var poolExclude bool
|
||||
streaming := true // 旧库/新建默认流式
|
||||
var maxTokens int
|
||||
var maxTokensField string
|
||||
if profs, _ := s.m.pg.ListProfiles(); profs != nil {
|
||||
for _, p := range profs {
|
||||
if p.Name == "default" {
|
||||
id, priority, poolExclude, streaming = p.ID, p.Priority, p.PoolExclude, p.Streaming
|
||||
maxTokens, maxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -346,6 +350,7 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error {
|
||||
APIKey: cfg.APIKey, RatePerSecond: cfg.RatePerSecond, RatePerMinute: cfg.RatePerMinute,
|
||||
ContextWindowK: cfg.ContextWindowK, ThinkingType: cfg.ThinkingType, ReasoningEffort: cfg.ReasoningEffort, IsDefault: true,
|
||||
Priority: priority, PoolExclude: poolExclude, Streaming: streaming,
|
||||
MaxTokens: maxTokens, MaxTokensField: maxTokensField,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -396,6 +401,7 @@ func (s *Server) buildPlannerWorker(pinID *int64, gProv llm.Provider, gCfg agent
|
||||
wk.SetWebSearch(s.webSearchFor("worker"))
|
||||
wk.SetConstraintInject(s.constraintInjectWorker) // 操作约束注入 worker(可配置,默认开;每轮读)
|
||||
wk.SetNonStreaming(nonStreamingResolver(wCfg)) // 该 profile 选非流式时走 Provider.Complete
|
||||
wk.SetMaxTokens(maxTokensResolver(wCfg)) // 单次回复输出上限(0 = 不发)
|
||||
pProv, pCfg := s.providerForAgent("planner", pinID, gProv, gCfg)
|
||||
pl := agent.NewPlanner(pProv, pCfg.Model, s.m.dir, tx, pCfg.CompactionWindow(), s.agentMaxTurns("planner"))
|
||||
pl.SetKillWork(s.engine.KillWork) // planner kill_work → terminate a running work
|
||||
@@ -404,6 +410,7 @@ func (s *Server) buildPlannerWorker(pinID *int64, gProv llm.Provider, gCfg agent
|
||||
pl.SetWebSearch(s.webSearchFor("planner"))
|
||||
pl.SetConstraintInject(s.constraintInjectPlanner) // 操作约束注入 planner(可配置,默认开;每轮读)
|
||||
pl.SetNonStreaming(nonStreamingResolver(pCfg)) // 该 profile 选非流式时走 Provider.Complete
|
||||
pl.SetMaxTokens(maxTokensResolver(pCfg)) // 单次回复输出上限(0 = 不发)
|
||||
return pl, wk
|
||||
}
|
||||
|
||||
@@ -415,6 +422,12 @@ func nonStreamingResolver(cfg agent.Config) func() bool {
|
||||
return func() bool { return nonStreaming }
|
||||
}
|
||||
|
||||
// maxTokensResolver mirrors nonStreamingResolver for the per-reply output cap.
|
||||
func maxTokensResolver(cfg agent.Config) func() int {
|
||||
maxTokens := cfg.MaxTokens
|
||||
return func() int { return maxTokens }
|
||||
}
|
||||
|
||||
// applyLLM (re)builds the planner/worker/main-agent from cfg and installs them on
|
||||
// the running engine as the GLOBAL active pair. Safe to call at runtime (UI configures LLM).
|
||||
func (s *Server) applyLLM(cfg agent.Config) error {
|
||||
@@ -459,6 +472,7 @@ func (s *Server) applyLLM(cfg agent.Config) error {
|
||||
s.mainAgent.SetWebSearch(s.webSearchFor("mainagent"))
|
||||
s.mainAgent.SetSteerWork(s.engine.SteerWork) // steer_work:人对运行中 work 实时纠偏
|
||||
s.mainAgent.SetNonStreaming(nonStreamingResolver(mCfg))
|
||||
s.mainAgent.SetMaxTokens(maxTokensResolver(mCfg))
|
||||
// chat agent serves MANY custom agents by key → it holds the GLOBAL opts
|
||||
// (backend/key) and gates Enabled per-conversation-agent at Chat time. 对话始终用激活配置。
|
||||
s.chatAgent = agent.NewChatAgent(prov, cfg.Model, s.m.dir, tx, win) // chat page runner
|
||||
@@ -466,6 +480,7 @@ func (s *Server) applyLLM(cfg agent.Config) error {
|
||||
s.chatAgent.SetWebSearch(s.m.WebSearchOpts())
|
||||
s.chatAgent.SetGuard(s.chatGuard())
|
||||
s.chatAgent.SetNonStreaming(nonStreamingResolver(cfg))
|
||||
s.chatAgent.SetMaxTokens(maxTokensResolver(cfg))
|
||||
s.llmProv = prov
|
||||
s.llmCfg = cfg
|
||||
s.llmOn = true
|
||||
@@ -492,6 +507,7 @@ func (s *Server) loadProfileConfig(id int64) (agent.Config, bool) {
|
||||
cfg.ThinkingType = p.ThinkingType
|
||||
cfg.ReasoningEffort = p.ReasoningEffort
|
||||
cfg.Stream = p.Streaming
|
||||
cfg.MaxTokens, cfg.MaxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
if cfg.APIKey == "" {
|
||||
return cfg, false
|
||||
}
|
||||
@@ -644,6 +660,7 @@ func (s *Server) chatAgentForProfile(id int64) *agent.ChatAgent {
|
||||
ca.SetWebSearch(s.m.WebSearchOpts())
|
||||
ca.SetGuard(s.chatGuard())
|
||||
ca.SetNonStreaming(nonStreamingResolver(cfg))
|
||||
ca.SetMaxTokens(maxTokensResolver(cfg))
|
||||
s.profMu.Lock()
|
||||
if ex := s.profChatAgents[id]; ex != nil { // lost the race → keep the winner
|
||||
ca = ex
|
||||
|
||||
Reference in new issue
Block a user