diff --git a/main.go b/main.go index 0fc31cb..e90dafa 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,7 @@ var llmTransport = &http.Transport{ DialContext: (&net.Dialer{Timeout: 300 * time.Second}).DialContext, } -var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto", 200000, nil} +var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto", 262144, nil} func atoiD(s string, d int) int { if v, e := strconv.Atoi(strings.TrimSpace(s)); e == nil { @@ -115,7 +115,7 @@ var cwCache = map[string]int{} func fetchContextWindow(cfg *Cfg) int { // Only values discovered from the /models endpoint are cached, keyed by - // endpoint+model. The context_window-override and 200000 default are derived + // endpoint+model. The context_window-override and 262144 default are derived // per call from cfg so they never shadow each other across configs. key := cfg.Endpoint + "\x00" + cfg.Model cwCacheMu.Lock() @@ -131,9 +131,9 @@ func fetchContextWindow(cfg *Cfg) int { return cw } if v, ok := cfg.Raw["context_window"]; ok { - return atoiD(v, 200000) + return atoiD(v, 262144) } - return 200000 + return 262144 } func getCfg(path string) Cfg { @@ -705,7 +705,7 @@ func estTokens(msgs []Message) int { } func contextPct(u Usage, cw int) float64 { - if cw <= 0 { cw = 200000 } + if cw <= 0 { cw = 262144 } return float64(u.PromptTokens) * 100.0 / float64(cw) } @@ -1087,7 +1087,17 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message, sp string, depth int) ([] } if m.Content != nil { fmt.Println(renderMD(*m.Content)) } } - if len(m.ToolCalls) == 0 { done = true; break } + if len(m.ToolCalls) == 0 { + // If the model returned only a reasoning block with no non-reasoning + // tokens or tool calls, nudge it to continue rather than ending the turn. + if m.ReasoningContent != "" && (m.Content == nil || strings.TrimSpace(*m.Content) == "") { + fmt.Println(c("[auto continue: response was reasoning-only]", 33)) + msgs = append(msgs, Message{Role: "user", Content: strp("continue")}) + continue + } + done = true + break + } for _, tc := range m.ToolCalls { if err := ctx.Err(); err != nil { return msgs, turnUsage, err } fn, astr := tc.Function.Name, filterText(tc.Function.Arguments) diff --git a/main_test.go b/main_test.go index fadb39d..66a44ae 100644 --- a/main_test.go +++ b/main_test.go @@ -1124,6 +1124,49 @@ func TestALToolLoop(t *testing.T) { } } +func TestALReasoningOnlyAutoContinue(t *testing.T) { + // First response is reasoning-only (no content, no tool calls); the agent + // must auto-append a "continue" user message and keep looping until a real + // answer arrives. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Messages []Message `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Errorf("decode request: %v", err) + } + for _, m := range req.Messages { + if m.Role == "user" && m.Content != nil && strings.TrimSpace(*m.Content) == "continue" { + w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"final answer"}}]}`)) + return + } + } + w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":null,"reasoning_content":"thinking hard"}}]}`)) + })) + defer srv.Close() + + cfg := defCfg + cfg.Endpoint = srv.URL + cfg.Stream = false + cfg.APIKey = "-" + msgs, _, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("start")}}, "sys", 0) + if err != nil { + t.Fatalf("AL: %v", err) + } + if got := last(msgs); got != "final answer" { + t.Errorf("last = %q, want %q", got, "final answer") + } + var continues int + for _, m := range msgs { + if m.Role == "user" && m.Content != nil && strings.TrimSpace(*m.Content) == "continue" { + continues++ + } + } + if continues != 1 { + t.Errorf("expected exactly 1 auto continue message, got %d", continues) + } +} + func TestALRunSubagent(t *testing.T) { var n int var mu sync.Mutex @@ -1842,12 +1885,12 @@ func TestFetchContextWindowFallbackConfig(t *testing.T) { t.Errorf("expected fallback to raw context_window 65536, got %d", cw) } - // Case 2: Config does not specify context_window -> default 200000 + // Case 2: Config does not specify context_window -> default 262144 cfg2 := defCfg cfg2.Endpoint = srv.URL cfg2.Raw = map[string]string{} - if cw := fetchContextWindow(&cfg2); cw != 200000 { - t.Errorf("expected default 200000, got %d", cw) + if cw := fetchContextWindow(&cfg2); cw != 262144 { + t.Errorf("expected default 262144, got %d", cw) } }