From 196cba71752797699e702d12d93d86a235d29f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E5=8F=8A?= <522caiji@gmail.com> Date: Thu, 10 Sep 2026 13:34:21 +0800 Subject: [PATCH] fix: remove WorkBuddy non-stream chat timeout --- CHANGELOG.md | 10 ++++ internal/providers/workbuddy/client.go | 9 ++- internal/providers/workbuddy/client_test.go | 61 +++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d440693..835af8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`. ### English +- Remove the fixed 120-second timeout from WorkBuddy non-streaming chat aggregation and preserve upstream read errors instead of reporting a missing [DONE] marker + +### 中文 + +- 移除 WorkBuddy 非流式聊天聚合的固定 120 秒超时,并保留上游读取错误,避免误报缺少 [DONE] 标记 + +## 0.4.2 - 2026-09-09 + +### English + - Return cached provider models immediately while refreshing expired catalogs in the background, and deduplicate concurrent catalog loads ### 中文 diff --git a/internal/providers/workbuddy/client.go b/internal/providers/workbuddy/client.go index f8703ab..45232e8 100644 --- a/internal/providers/workbuddy/client.go +++ b/internal/providers/workbuddy/client.go @@ -405,12 +405,17 @@ func (c *Client) ChatNonStream(ctx context.Context, accountID string, req transl if err != nil { return providers.ChatOutcome{}, err } - resp, err := c.http.Do(httpReq) + client := *c.http + client.Timeout = 0 + resp, err := client.Do(httpReq) if err != nil { return providers.ChatOutcome{}, err } defer resp.Body.Close() - body, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + if readErr != nil { + return providers.ChatOutcome{}, fmt.Errorf("read workbuddy stream: %w", readErr) + } if resp.StatusCode >= 300 { return providers.ChatOutcome{}, classifiedError(resp.StatusCode, body) } diff --git a/internal/providers/workbuddy/client_test.go b/internal/providers/workbuddy/client_test.go index fbcdf6a..973bef5 100644 --- a/internal/providers/workbuddy/client_test.go +++ b/internal/providers/workbuddy/client_test.go @@ -193,6 +193,67 @@ func TestChatNonStreamAggregatesToolsAndReasoning(t *testing.T) { } } +func TestChatNonStreamReadLifecycle(t *testing.T) { + for _, test := range []struct { + name string + cancel bool + truncate bool + }{ + {name: "exceeds shared client timeout"}, + {name: "preserves context cancellation", cancel: true}, + {name: "preserves truncated body error", truncate: true}, + } { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + client, store := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if test.truncate { + w.Header().Set("Content-Length", "100000") + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = io.WriteString(w, "data: {}\n\n") + w.(http.Flusher).Flush() + if test.cancel { + cancel() + return + } + if test.truncate { + return + } + select { + case <-time.After(60 * time.Millisecond): + _, _ = io.WriteString(w, chatSSE) + case <-r.Context().Done(): + } + })) + payload, _ := (Credential{AccessToken: "at", UID: "u1", Domain: "codebuddy.cn", ExpiresAt: 4102444800}).Encode() + store.items = map[string][]byte{"acc1": payload} + client.rememberCatalog([]providers.ModelInfo{{NativeModel: "glm-5.2", Capabilities: providers.ModelCapabilities{ReasoningOptions: []string{"low", "high"}}}}) + client.http.Timeout = 10 * time.Millisecond + out, err := client.ChatNonStream(ctx, "acc1", translate.ChatRequest{ + Model: "glm-5.2", Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}}, + }) + switch { + case test.cancel: + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected cancellation, got %v", err) + } + case test.truncate: + if !errors.Is(err, io.ErrUnexpectedEOF) { + t.Fatalf("expected body read error, got %v", err) + } + default: + if err != nil || out.Content != "OK" { + t.Fatalf("outcome=%+v err=%v", out, err) + } + } + if client.http.Timeout != 10*time.Millisecond { + t.Fatal("shared client timeout was changed") + } + }) + } +} + func TestModelsFiltersCliAgentAndDisabled(t *testing.T) { payload, _ := Credential{AccessToken: "at", UID: "u1", Domain: "codebuddy.cn", ExpiresAt: 4102444800}.Encode() store := &memStore{items: map[string][]byte{"acc1": payload}}