mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
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 约束限定取值,每次启动先删后建保持幂等。
101 lines
3.4 KiB
Go
101 lines
3.4 KiB
Go
package db
|
|
|
|
import "testing"
|
|
|
|
// TestProfileMaxTokensRoundTrip pins the output-cap columns through the whole
|
|
// read/write surface: both column lists (list vs keyed loads) must carry them,
|
|
// and an update must not drop them. A miscounted column list silently shifts
|
|
// every later Scan target, so this is the test that catches it.
|
|
func TestProfileMaxTokensRoundTrip(t *testing.T) {
|
|
d, err := Open(testDSN(t))
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
// Close via Cleanup, registered FIRST so it runs LAST: cleanups are LIFO, and a
|
|
// plain `defer d.Close()` would fire before them — the row-deleting cleanups
|
|
// would then run against a closed pool and silently leave test rows behind.
|
|
t.Cleanup(func() { d.Close() })
|
|
|
|
id, err := d.SaveProfile(&LLMProfile{
|
|
Name: "t-maxtok", Format: "openai", Model: "m", APIKey: "k",
|
|
MaxTokens: 8192, MaxTokensField: "max_completion_tokens",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) })
|
|
|
|
// Keyed single-row load (profileColsKey).
|
|
p, err := d.ProfileByID(id)
|
|
if err != nil || p == nil {
|
|
t.Fatalf("ProfileByID: %v, p=%v", err, p)
|
|
}
|
|
if p.MaxTokens != 8192 || p.MaxTokensField != "max_completion_tokens" {
|
|
t.Fatalf("keyed load: max_tokens=%d field=%q", p.MaxTokens, p.MaxTokensField)
|
|
}
|
|
|
|
// List load (profileCols — the hint variant, a separate column list).
|
|
ps, err := d.ListProfiles()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var found *LLMProfile
|
|
for _, x := range ps {
|
|
if x.ID == id {
|
|
found = x
|
|
}
|
|
}
|
|
if found == nil {
|
|
t.Fatal("profile missing from ListProfiles")
|
|
}
|
|
if found.MaxTokens != 8192 || found.MaxTokensField != "max_completion_tokens" {
|
|
t.Fatalf("list load: max_tokens=%d field=%q", found.MaxTokens, found.MaxTokensField)
|
|
}
|
|
|
|
// Update with a blank key takes the "keep existing key" UPDATE branch, which
|
|
// has its own column list and is the easiest one to forget.
|
|
p.APIKey = ""
|
|
p.MaxTokens = 4096
|
|
p.MaxTokensField = ""
|
|
if _, err := d.SaveProfile(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := d.ProfileByID(id)
|
|
if err != nil || after == nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if after.MaxTokens != 4096 || after.MaxTokensField != "" {
|
|
t.Fatalf("after update: max_tokens=%d field=%q", after.MaxTokens, after.MaxTokensField)
|
|
}
|
|
if after.APIKey != "k" {
|
|
t.Fatalf("blank key on update must keep the stored one, got %q", after.APIKey)
|
|
}
|
|
}
|
|
|
|
// A profile saved without touching the new fields must read back as "no cap,
|
|
// classic field name" — the pre-feature behaviour old rows also get.
|
|
func TestProfileMaxTokensDefaults(t *testing.T) {
|
|
d, err := Open(testDSN(t))
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
// Close via Cleanup, registered FIRST so it runs LAST: cleanups are LIFO, and a
|
|
// plain `defer d.Close()` would fire before them — the row-deleting cleanups
|
|
// would then run against a closed pool and silently leave test rows behind.
|
|
t.Cleanup(func() { d.Close() })
|
|
|
|
id, err := d.SaveProfile(&LLMProfile{Name: "t-maxtok-default", Format: "openai", Model: "m", APIKey: "k"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) })
|
|
|
|
p, err := d.ProfileByID(id)
|
|
if err != nil || p == nil {
|
|
t.Fatalf("ProfileByID: %v", err)
|
|
}
|
|
if p.MaxTokens != 0 || p.MaxTokensField != "" {
|
|
t.Fatalf("defaults: max_tokens=%d field=%q, want 0 and \"\"", p.MaxTokens, p.MaxTokensField)
|
|
}
|
|
}
|