Skip to content

Commit 8025cc7

Browse files
fix(opencode-go): distinguish synthetic stream preambles
1 parent 56e8121 commit 8025cc7

2 files changed

Lines changed: 115 additions & 4 deletions

File tree

extensions/opencode-go/stream-termination.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,70 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
288288
expect(received.some((event) => event.type === "done")).toBe(true);
289289
});
290290

291+
it("keeps the first-event window after synthetic block-start events until a provider delta", async () => {
292+
const { stream: baseStream, controller } = createFakeBaseStream();
293+
let abortCalled = false;
294+
295+
const underlying = vi.fn((_model, _context, options) => {
296+
if (options?.signal) {
297+
options.signal.addEventListener("abort", () => {
298+
abortCalled = true;
299+
});
300+
}
301+
return baseStream;
302+
});
303+
304+
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
305+
provider: "opencode-go",
306+
idleTimeoutMs: 5_000,
307+
firstEventTimeoutMs: 10_000,
308+
});
309+
310+
const downstream = await Promise.resolve(
311+
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
312+
);
313+
expect(downstream).toBeDefined();
314+
if (!downstream) {
315+
return;
316+
}
317+
318+
const received: AnyEvent[] = [];
319+
const consumer = (async () => {
320+
for await (const event of downstream) {
321+
received.push(event);
322+
}
323+
})();
324+
325+
const partial = {
326+
role: "assistant",
327+
content: [{ type: "text", text: "" }],
328+
stopReason: undefined,
329+
};
330+
controller.emit({ type: "start", partial } as any);
331+
controller.emit({ type: "text_start", contentIndex: 0, partial } as any);
332+
333+
await vi.advanceTimersByTimeAsync(6_000);
334+
expect(abortCalled).toBe(false);
335+
336+
const message = {
337+
...partial,
338+
content: [{ type: "text", text: "hello" }],
339+
stopReason: "stop",
340+
};
341+
controller.emit({
342+
type: "text_delta",
343+
contentIndex: 0,
344+
delta: "hello",
345+
partial: message,
346+
} as any);
347+
controller.emit({ type: "done", reason: "stop", message } as any);
348+
await consumer;
349+
350+
expect(abortCalled).toBe(false);
351+
expect(received.some((event) => event.type === "text_delta")).toBe(true);
352+
expect(received.some((event) => event.type === "done")).toBe(true);
353+
});
354+
291355
it("honors explicit opencode-go provider request timeout above the wrapper idle default", async () => {
292356
const { stream: baseStream, controller } = createFakeBaseStream();
293357
let abortCalled = false;
@@ -340,6 +404,48 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
340404
await consumer;
341405
});
342406

407+
it("honors explicit opencode-go provider request timeout below wrapper defaults", async () => {
408+
const { stream: baseStream } = createFakeBaseStream();
409+
let abortCalled = false;
410+
411+
const underlying = vi.fn((_model, _context, options) => {
412+
if (options?.signal) {
413+
options.signal.addEventListener("abort", () => {
414+
abortCalled = true;
415+
});
416+
}
417+
return baseStream;
418+
});
419+
420+
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
421+
provider: "opencode-go",
422+
idleTimeoutMs: 5_000,
423+
firstEventTimeoutMs: 10_000,
424+
});
425+
426+
const downstream = await Promise.resolve(
427+
wrapper(
428+
{ provider: "opencode-go", id: "deepseek-v4-flash", requestTimeoutMs: 2_000 } as any,
429+
{} as any,
430+
{} as any,
431+
),
432+
);
433+
expect(downstream).toBeDefined();
434+
if (!downstream) {
435+
return;
436+
}
437+
438+
const consumer = (async () => {
439+
for await (const event of downstream) {
440+
void event;
441+
}
442+
})();
443+
444+
await vi.advanceTimersByTimeAsync(2_500);
445+
expect(abortCalled).toBe(true);
446+
await consumer;
447+
});
448+
343449
it("aborts and releases the underlying stream when no first event arrives", async () => {
344450
const { stream: baseStream, getReturnCalls } = createFakeBaseStream();
345451
let abortCalled = false;

extensions/opencode-go/stream-termination.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,14 @@ function validTimeoutMs(value: unknown): number | undefined {
4848
}
4949

5050
function resolveTimeoutMs(model: unknown, fallbackMs: number): number {
51-
return Math.max(
52-
validTimeoutMs((model as { requestTimeoutMs?: unknown })?.requestTimeoutMs) ?? 0,
53-
fallbackMs,
51+
return validTimeoutMs((model as { requestTimeoutMs?: unknown })?.requestTimeoutMs) ?? fallbackMs;
52+
}
53+
54+
function isProviderProgressEvent(event: AssistantMessageEvent): boolean {
55+
return (
56+
event.type === "text_delta" ||
57+
event.type === "thinking_delta" ||
58+
event.type === "toolcall_delta"
5459
);
5560
}
5661

@@ -348,7 +353,7 @@ export function createOpencodeGoStalledStreamWrapper(
348353
}
349354
trackPartial(event);
350355
output.push(event);
351-
if (event.type !== "start") {
356+
if (isProviderProgressEvent(event)) {
352357
armIdleTimer();
353358
}
354359
}

0 commit comments

Comments
 (0)