Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente

### Fixed

- **`[Streaming]` Muse Spark no longer throws "stream ended before completion" after delivering content (#178 regression).** The truncated-stream detector from #178 flags streams that end without `[DONE]` or `finish_reason`. Muse Spark on the Responses API delivers content (text, tool calls) but closes the connection without either signal β€” a gateway quirk, not a failure. The engine now logs a warning and returns successfully when content was already delivered, instead of throwing an error popup on an otherwise complete response. Documented in `docs/issues/79-20260822-issue-muse-spark-stream-completion.md`.

- **`[Models]` Deprecated filter no longer hides live models (#182).** `models.dev` `status: deprecated` was hiding models still served by the gateway (e.g. `laguna-s-2.1-free`). The filter now cross-checks against the live gateway response β€” only hides when `models.dev` says deprecated AND the gateway confirms the model is absent. Note: `deepseek-v4-flash-free` is listed by the gateway but actually broken upstream ("Model is unavailable") β€” this is an upstream issue, not solvable from the extension side. Documented in `docs/issues/78-20260822-issue182-deprecated-model-gateway-crosscheck.md`.

## [0.7.0] β€” 2026-08-22
Expand Down
77 changes: 77 additions & 0 deletions docs/issues/79-20260822-issue-muse-spark-stream-completion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
**Status:** βœ… Solved

# Muse Spark stream completion without [DONE] / finish_reason

**Topic:** streaming / transport / responses-api / muse-spark
**Updated:** 2026-08-22
**Tags:** #streaming #transport #responses-api #muse-spark #regression
**Supersedes:** β€”

---

## Overview

Muse Spark on the Responses API delivers content successfully but closes the connection without sending `data: [DONE]` or a `response.completed` event with `stop_reason`. The truncated-stream detector from #178 was throwing an error **after** content was already delivered to VS Code, creating a confusing error popup on an otherwise successful response.

---

## Problem

User reports after installing v0.7.0:

```text
OpenCode Zen stopped sending data before the response was complete (the
connection closed unexpectedly). Your message may be cut off β€” try sending
it again; a single resend usually succeeds.
```

The error fires **after** the model has answered the query β€” the content is visible in the chat, then the error popup appears.

### Root Cause

PR #178 added `isStreamTruncated()` to detect streams that end without `data: [DONE]` or `finish_reason`. The check correctly identifies abnormal termination, but treats ALL missing-signal streams the same:

- `extractedPartCount === 0` β†’ retry (safe, no content to duplicate) βœ…
- `extractedPartCount > 0` β†’ throw error ❌ (confusing: content was delivered)

Muse Spark on the Responses API sends 12 events / 212 KB of content, then closes the connection without `[DONE]` or `finish_reason`. This is a **gateway quirk**, not a truncation. The stream was complete from the user's perspective.

### Why it only affects Muse Spark

Other Responses API models (GPT-5 family) always send `data: [DONE]` and/or `response.completed` with `stop_reason`. Muse Spark's gateway does not, even for successful responses.

---

## Solution

When `isStreamTruncated()` returns `true` AND `extractedPartCount > 0`, the engine logs a `[warn]` line and returns successfully instead of throwing. The user received their content β€” the missing termination signals are logged for diagnostics but don't surface as an error.

```text
[warn] stream ended without [DONE] / finish_reason but 12 parts were
delivered (212970 bytes / 12 events)
```

### Files Changed

| File | Change |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `src/transports/engine.ts` | Added `extractedPartCount > 0` branch in truncation check: log warning + emit summary + return (instead of throwing) |

---

## Verification

```bash
npm run compile # passes
npm run lint # passes (7 checks)
npm test # passes (305+ tests)
```

Manual testing: Muse Spark delivers content without error popup.

---

## Notes

- The `isStreamTruncated()` function itself is unchanged β€” it still correctly identifies streams without `[DONE]`/`finish_reason`. The fix is in how the engine _handles_ the result.
- This is a regression from #178, which was designed for the case where content was NOT delivered. The Muse Spark case (content delivered, no termination signals) was not anticipated.
14 changes: 14 additions & 0 deletions src/transports/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,20 @@ export async function streamOpenCodeResponse(options: StreamOpenCodeResponseOpti
await streamOpenCodeResponse({ ...options, streamFailureRetryAttempt: nextAttempt });
return;
}
// Content was already delivered to VS Code β€” the response is usable even
// though the stream lacked [DONE] / finish_reason (e.g. Muse Spark on
// the Responses API). Log the anomaly but don't throw, since the user
// already received their content and throwing after delivery creates a
// confusing error popup on an otherwise successful response.
if (extractedPartCount > 0) {
options.output?.appendLine(
`[warn] stream ended without [DONE] / finish_reason but ${String(extractedPartCount)} parts were delivered (${String(totalBytes)} bytes / ${String(totalEvents)} events)`,
);
emitSummary(totalBytes, totalEvents, {
rateLimitSummary,
});
return;
}
const requestError = new OpenCodeRequestError(
`${options.providerDisplayName} response stream ended before completion (no [DONE] or finish_reason after ${String(totalBytes)} bytes / ${String(totalEvents)} events${localRequestId ? `, request ${localRequestId}` : ""}).`,
`${options.providerDisplayName} stopped sending data before the response was complete (the connection closed unexpectedly). Your message may be cut off β€” try sending it again; a single resend usually succeeds. If this keeps happening, check your connection, VPN, or firewall.`,
Expand Down
Loading