diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb4054..fb10efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Reset the generate transport's idle timeout on every received chunk, allowing active reasoning streams to exceed the timeout overall while still aborting stalled streams (#87). + - Batch consecutive tool-result images after all tool results on the generate transport, preventing interleaved user messages from breaking multi-tool turns with "Tool result is missing". - Add display pricing for DeepSeek V4.1 Flash, Qwen 3.8 Max 0902, Gemini 3.8 Flash, Muse Spark 1.3 variants, LongCat 2.0 free, and Ling 3.0 Flash Sante free. Verify against the September 15 pricing page and live 69-model catalog; correct DeepSeek V4 Flash and Vision Exp off-peak prices to $0.15/$0.60 with $0.003 cache reads per million tokens. diff --git a/src/core.ts b/src/core.ts index 0d16e70..cc6fe04 100644 --- a/src/core.ts +++ b/src/core.ts @@ -708,6 +708,7 @@ export function createStreamCommandCode(deps: CoreDependencies) { if (controller.signal.aborted) throw abortError("Aborted") const { done, value } = await raceAbort(reader.read(), attemptController.signal) if (done) { + clearAttemptTimeout() if (buffer.trim()) handleEvent(parseStreamEventLine(buffer)) if (!finished) { throw new Error( @@ -716,6 +717,13 @@ export function createStreamCommandCode(deps: CoreDependencies) { } break } + if (timeoutMs !== undefined) { + clearAttemptTimeout() + attemptTimeoutId = setTimeout(() => { + attemptTimedOut = true + attemptController.abort() + }, timeoutMs) + } if (controller.signal.aborted) throw abortError("Aborted") buffer += decoder.decode(value, { stream: true }) diff --git a/tests/test-retry.ts b/tests/test-retry.ts index e0366de..ba27ea0 100644 --- a/tests/test-retry.ts +++ b/tests/test-retry.ts @@ -322,6 +322,40 @@ describe("streamCommandCode — timeout", () => { if (error?.type !== "error") throw new Error("expected error") assert.match(error.error.errorMessage ?? "", /timed out after 50ms/) }) + + // https://github.com/patlux/pi-commandcode-provider/issues/87 + it("does not abort actively streaming responses that exceed timeoutMs overall", async () => { + server.mockResponse({ + type: "success", + events: [ + JSON.stringify({ type: "text-delta", text: "chunk 1 " }), + JSON.stringify({ type: "text-delta", text: "chunk 2 " }), + JSON.stringify({ type: "text-delta", text: "chunk 3 " }), + JSON.stringify({ type: "finish", finishReason: "stop" }), + ], + delays: [0, 200, 200, 200], + }) + const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() }) + + const events = await collectEvents( + streamCommandCode(makeModel(), makeContext(), { + apiKey: TEST_API_KEY, + timeoutMs: 500, + }), + 5_000, + ) + + assert.equal(server.requestCount(), 1) + assert.deepEqual(eventTypes(events), [ + "start", + "text_start", + "text_delta", + "text_delta", + "text_delta", + "text_end", + "done", + ]) + }) }) describe("streamCommandCode — abort cancels retry loop", () => {