Merge pull request 'Let length continuations and Anthropic output run unbounded' (#7) from feature/stream-display-overhaul into main
ci / vet-and-test (push) Failing after 2s
ci / docker-checkout-hostgateway (push) Successful in 2s

This commit was merged in pull request #7.
This commit is contained in:
2026-10-02 05:04:53 +00:00
3 changed files with 53 additions and 4 deletions
+3 -1
View File
@@ -258,8 +258,10 @@ func BuildBody(protocol Protocol, model string, messages []Message, tools []Tool
"max_tokens": opts.MaxTokens,
"stream": true,
}
// Anthropic 要求 max_tokens。0 表示调用方不限制:用足够大的默认值,
// 避免 4096 把长回复截断。真正的上限由调用方 StreamOptions.MaxTokens 决定。
if body["max_tokens"] == 0 {
body["max_tokens"] = 4096
body["max_tokens"] = 128000
}
if sys != "" {
body["system"] = sys
+10 -3
View File
@@ -84,9 +84,12 @@ type Config struct {
// RepeatGuard 为真时,同名且参数按键排序后相同的调用连续 3 次,注入一条提醒。
// 每个 run 最多 3 条,不中断循环。
RepeatGuard bool
// ContinueOnLength 为真时,FinishReason=length 且没有工具调用就续写,最多 3 次。
// 续写提示不写进 Session。第 4 次截断返回错误。
// ContinueOnLength 为真时,FinishReason=length 且没有工具调用就续写。
// 续写提示不写进 Session。次数由 MaxLengthContinuations 决定。
ContinueOnLength bool
// MaxLengthContinuations 是 length 续写的次数上限。
// 0 表示默认 3 次;负数表示不限制。
MaxLengthContinuations int
}
// StopInfo 是 Stop 钩子看到的本 run 进度。
@@ -219,7 +222,11 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
}
emit(cfg, Event{Type: "turn_end", Message: last})
if cfg.ContinueOnLength && end.FinishReason == "length" && len(last.ToolCalls) == 0 {
if lengthContinues >= maxLengthContinuations {
lengthCap := cfg.MaxLengthContinuations
if lengthCap == 0 {
lengthCap = maxLengthContinuations
}
if lengthCap > 0 && lengthContinues >= lengthCap {
errCap := errString("length continuation exceeded")
cfg.logger().Error("runtime length continuation exceeded", errCap)
return Result{Text: last.Content, Messages: messages, StopReason: llm.StopError, Usage: usage, Err: errCap}, nil
+40
View File
@@ -248,6 +248,46 @@ func TestLoopContinueOnLengthCaps(t *testing.T) {
}
}
func TestLoopContinueOnLengthUnlimited(t *testing.T) {
var n int
stream := func(ctx context.Context, model string, messages []llm.Message, decls []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
n++
ch := make(chan llm.Event, 1)
finish := "length"
text := "part"
if n >= 5 {
finish = "stop"
text = "done"
}
ch <- llm.Event{
Kind: llm.KindMessageEnd,
FinishReason: finish,
Message: llm.Message{Role: llm.RoleAssistant, Content: text, StopReason: llm.StopEnd},
}
close(ch)
return ch
}
res, err := Run(context.Background(), Config{
Model: "fake",
MaxTurns: 8,
ContinueOnLength: true,
MaxLengthContinuations: -1,
Stream: stream,
}, llm.Message{Role: llm.RoleUser, Content: "go"})
if err != nil {
t.Fatal(err)
}
if res.Err != nil {
t.Fatalf("err = %v", res.Err)
}
if n != 5 {
t.Fatalf("model calls = %d, want 5", n)
}
if res.Text != "done" {
t.Fatalf("text = %q", res.Text)
}
}
func TestLoopHistorySkipsGeneratedSystem(t *testing.T) {
var first string
_, err := Run(context.Background(), Config{