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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

All notable changes to the **OpenCode Go BYOK Provider** extension are documented here.

## [0.7.5] — 2026-09-08

### Fixed

- **`[Gateway]` `x-opencode-session` is now sent on every OpenCode request, not just chat.** OpenCode's Go docs (updated 2026-09-07) require a stable session id on all requests, and requests missing it may start erroring from 2026-09-06. Four auxiliary fetch sites hit the gateway without the header: `GET /models`, inline completions, `GET /usage` server sync, and the Manage-Provider test connection. All four now send a persisted per-installation session id; the main chat path keeps its per-conversation id. See `docs/issues/99-20260908-issue218-223-triage-dropdown-and-session-header.md`.

- **`[Models]` `GET /models` is now served from cache instead of refetching on every provider poll (#222).** `ModelListFetcher.fetch()` performed a live upstream fetch on every `provideLanguageModelChatInformation` poll (VS Code polls every few hundred ms) because `MODEL_LIST_CACHE_TTL_MS` was only consulted on the failure path. The fresh snapshot is now checked before the network call; a new `invalidate()` hooks into `Refresh Models` so manual refreshes still hit the gateway. The `Models registered` log line is now emitted only when its signature changes, ending the identical-line spam. Documented in `docs/issues/94-20260908-issue222-uncached-models-poll.md`.

- **`[Provider]` One shared output channel instead of one per request (#220).** The god-file split left a `createOutputChannel("OpenCode")` inside `prepareChatRequest()`, which runs per request — `createOutputChannel` registers a new Output-tab entry every call, so long sessions accumulated dozens of duplicate channels. `chatPrep` no longer creates channels; transports receive the provider's lazy singleton (disposed via `context.subscriptions`). Documented in `docs/issues/95-20260908-issue220-duplicate-output-channels.md`.

- **`[Responses]` function_call / function_call_output items are now strictly paired (#216).** The gateway rejects the whole request with `No tool output found for function call <id>` when a `function_call` lost its output (history trim, lost `tool_call_id`) — and the converter even fabricated a `tool-<timestamp>` call_id that could never match. Fabricated ids are gone, and `pairResponsesFunctionCallItems()` drops orphaned calls/outputs before sending. 5 regression tests. Documented in `docs/issues/96-20260908-issue216-no-tool-output-for-function-call.md`.

- **`[Responses]` Nested event payloads parse; zero-part failures now carry their own diagnostics (#217).** gpt-5.6-luna streams could end with zero extractable parts while the gateway billed completion tokens, triggering Copilot Chat's empty-response loop guard (verified: neither error string exists in our 0.7.4 VSIX). `response.output_text.delta` and reasoning events now unwrap nested payload objects (`delta.text`, `delta.content`, `text.value`, …), and after retries are exhausted a dedicated error names the token count, event stats, and the `[diag-sse-event-*]` path. The `[diag-empty-response]` dump no longer false-positives on healthy tool-call-only turns (tool calls flush in the transport's `finally`, after the diagnostic runs) — it now only fires when no healthy `finish_reason` was extracted. Documented in `docs/issues/97-20260908-issue217-luna-zero-parts-empty-response-loop.md`.

- **`[Retry]` 429 responses honoring `Retry-After` are retried once transparently (#221).** Upstream Console Go rate limits are provider-side, but when the upstream names a short wait the extension now waits (capped at 30 s via `RATE_LIMIT_MAX_RETRY_AFTER_WAIT_MS`) and retries before anything is streamed — no duplicated content, no hard failure. Longer waits keep the existing actionable error. Documented in `docs/issues/98-20260908-issue221-429-retry-after.md`.

### Triage

- **#218 / #223 closed as triaged.** #218: VS Code's `chat.utilityModel` / `inlineChat.defaultModel` dropdowns only list natively integrated providers; BYOK vendors can't opt in — users should use **OpenCode: Configure Utility Models** (doc 32). #223: the reported `MissingSessionID` stack trace belongs to `vizards.deepseek-v4-for-copilot`, not this extension; we have always sent `x-opencode-session`. Full analysis in `docs/issues/99-20260908-issue218-223-triage-dropdown-and-session-header.md`.

## [0.7.4] — 2026-09-03

### Fixed
Expand Down
41 changes: 40 additions & 1 deletion docs/devlog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
# 🧠 OPENCODE COPILOT CHAT DEVLOG

**Branch:** `main` | **Updated:** 2026-09-03 Asia/Jakarta | **Current Phase:** v0.7.4 batch — 6 community issue fixes (#204/#206/#207/#208/#213/#214) on branch `fix/issues-204-214-batch`, pending merge. Open: PR #161 (restore API key command).
**Branch:** `fix/issues-216-223-batch` | **Updated:** 2026-09-08 Asia/Jakarta | **Current Phase:** v0.7.5 batch verified — 5 fixes + 2 triages (#216/#217/#220/#221/#222 + #218/#223), manual + automated verification passed, pending PR.

---

## ✅ Manual + automated verification (2026-09-08)

User ran the manual checklist against a live build; results all green:

| Check | Result |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| #222 A/B/C — no log spam, zero `GET /models` during session, Refresh Models still fetches + filters | ✅ |
| #220 — after 8 requests the Output dropdown still shows exactly one `OpenCode` channel | ✅ |
| #216/#217 — 9 chained luna tool-call turns (multi-parallel `read_file`/`runSubagent`), all 200, no `No tool output found`, no empty-response loop, usage/cache recorded | ✅ |
| Regression — per-model thinking resolves (`thinkingSource=modelConfiguration`), Go usage recording + status bar update (`entries=1..9`) | ✅ |

Follow-up commits after verification:

- `ac7a9a1` — suppress `[diag-empty-response]` false positive on healthy tool-call-only turns (tool calls flush after the diagnostic runs; now gated on a healthy `finish_reason`).
- `b99f449` / `25f0301` — regression suites automating the checklist: history-trim × pairing invariants, the real luna event shapes (flat + nested `output_text.delta`, tool-call sequence), and header-capture tests pinning `x-opencode-session` on `GET /models`, inline completions, and `GET /usage`.
- `418c4c4` — after the gateway enforcement tip (docs/go updated 2026-09-07): all four auxiliary fetch sites now send `x-opencode-session` (persisted per-installation id); chat path keeps the per-conversation id.

Suite: 462/462 unit tests, full lint gate pass.

---

## ✅ Batch v0.7.5 — Seven Issues (#216 #217 #218 #220 #221 #222 #223) — 2026-09-08

**Scope:** one branch (`fix/issues-216-223-batch`), atomic commits in triage-priority order, `closes #N` in each message. Deep-dive first: issue bodies, docs/issues 88–93, git history, published 0.7.4 VSIX extraction, and local session history.

| Issue | Root cause (verified in code/artifact) | Fix | Commit |
| ----- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------- |
| #222 | `MODEL_LIST_CACHE_TTL_MS` only consulted on failure path → live `GET /models` every UI poll | Cache-first `fetch()`, `invalidate()` for Refresh Models, log-on-change registration | `9f0eba9` |
| #220 | `createOutputChannel("OpenCode")` inside per-request `prepareChatRequest()` (refactor fallout) | Shared lazy-singleton channel from provider; field removed from chatPrep | `5d2b525` |
| #216 | Fabricated `tool-<ts>` call_id + unpaired function_call/output items after trim → gateway 400 | No fabricated ids; `pairResponsesFunctionCallItems()` 1:1 pairing pass + 5 tests | `ed53ecb` |
| #217 | Nested Responses event payloads unrecognized → zero parts, Copilot loop guard (VSIX-verified) | Nested delta unwrapping in routing normalizer + dedicated zero-part error | `573f757` |
| #221 | Upstream Console Go 429 hardened into failure despite Retry-After | `parseRetryAfterMs` + single transparent retry (≤30 s, pre-stream) | `8d8e9f0` |
| #218 | VS Code default-model dropdowns whitelist native providers only (doc 32) | Triage doc; Configure Utility Models remains the user path | docs |
| #223 | Stack trace belongs to `vizards.deepseek-v4-for-copilot`, not this extension | Triage doc; our requests always carry `x-opencode-session` | docs |

Docs: `docs/issues/94`–`99`, CHANGELOG 0.7.5, version bump 0.7.4 → 0.7.5.

---

Expand Down
57 changes: 57 additions & 0 deletions docs/issues/94-20260908-issue222-uncached-models-poll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Issue #222 — Uncached `GET /models` on every `provideLanguageModelChatInformation` poll

**Status:** ✅ Solved (branch `fix/issues-216-223-batch`, commit `9f0eba9`)
**Topic:** models / caching / provider-poll
**Updated:** 2026-09-08
**Tags:** #models #cache #polling #logs
**GitHub Issue:** [ltmoerdani/opencode-copilot-chat#222](https://github.com/ltmoerdani/opencode-copilot-chat/issues/222)
**Related:** issue doc [35 (model list fetch resilience / #78)](35-20260720-issue78-model-list-fetch-resilience.md)

---

## Problem

Every UI refresh triggered a real upstream `GET /models` fetch plus a repeated
`Models registered` log line. The Output channel became noise; upstream saw
unbounded request volume from poll-happy clients.

## Analysis

`ModelListFetcher.fetch()` (`src/provider/modelList.ts`) performed the live
fetch unconditionally. `MODEL_LIST_CACHE_TTL_MS` existed but was only consulted
on the **failure** path (`loadCached()` inside `fallback()` and after the retry
loop). A successful fetch cached the snapshot, but the next `fetch()` never
looked at it. VS Code polls `provideLanguageModelChatInformation` every few
hundred ms, so each poll = live fetch + `replaceLiveModelMetadata` rebuild +
re-registration.

## Fix

- Cache-first reorder: `fetch()` consults the fresh snapshot
(in-memory, then `globalState`, both TTL-guarded) **before** the network
call; stale snapshots fall through to the existing fetch + retry path.
- `ModelListFetcher.invalidate()` clears both cache layers;
`refreshMetadataAndModels()` (the `Refresh Models` command) calls it so a
manual refresh still performs a real upstream fetch.
- `Models registered` summary in `src/provider/modelInfo.ts` is now logged
only when its signature (count/first/last/variant) changes.

## Files Changed

| File | Change |
| ---------------------------------- | --------------------------------------------------------- |
| `src/provider/modelList.ts` | cache-first `fetch()`, new `invalidate()` |
| `src/provider/modelInfo.ts` | log-on-change via `LAST_REGISTRATION_LOG_SIGNATURE` |
| `src/provider/OpenCodeProvider.ts` | `refreshMetadataAndModels()` calls `fetcher.invalidate()` |

## Verification

- `npx tsc --noEmit` clean; 454/454 unit tests pass; staged-lint gate pass.
- Follow-up (manual): confirm with the Output channel that repeated picker
refreshes produce no new `Models registered` lines and no `/models` traffic
within the TTL window.

## Lessons Learned

A TTL constant that only guards the failure path is not a cache — the
cache check has to sit before the work it is supposed to prevent.
51 changes: 51 additions & 0 deletions docs/issues/95-20260908-issue220-duplicate-output-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Issue #220 — Dozens of duplicate "OpenCode" output channels accumulate per session

**Status:** ✅ Solved (branch `fix/issues-216-223-batch`, commit `5d2b525`)
**Topic:** output-channel / lifecycle
**Updated:** 2026-09-08
**Tags:** #output-channel #regression #refactor-fallout
**GitHub Issue:** [ltmoerdani/opencode-copilot-chat#220](https://github.com/ltmoerdani/opencode-copilot-chat/issues/220)
**Related:** PR #155 god-file split (doc 67), PR #138 central config refactor (doc 66)

---

## Problem

Every chat request registered a brand-new `OpenCode` entry in the Output-tab
dropdown. Long sessions accumulated dozens of channels and the machine slowed
down; disabling the extension stopped the growth (reporter's A/B test).

## Analysis

`vscode.window.createOutputChannel()` registers a NEW channel every call —
there is no dedup by name. `OpenCodeProvider.getOutputChannel()` already used
the correct lazy-singleton pattern, but the god-file split
(`03029a6` / `e8fb6b2`) copied a `createOutputChannel("OpenCode")` call into
`prepareChatRequest()` (`src/provider/chatPrep.ts`), which runs **per request**.
Each request therefore leaked one more channel.

## Fix

- Removed the per-request channel creation (and the `outputChannel` field)
from `chatPrep.ts`.
- `OpenCodeProvider.provideLanguageModelChatResponse()` now passes its shared
lazy-singleton channel (already pushed to `context.subscriptions`, so it is
disposed with the extension) to all four transports.

## Files Changed

| File | Change |
| ---------------------------------- | -------------------------------------------------- |
| `src/provider/chatPrep.ts` | drop `createOutputChannel` + `outputChannel` field |
| `src/provider/OpenCodeProvider.ts` | use `this.getOutputChannel()` for transports |

## Verification

- `npx tsc --noEmit` clean; 454/454 unit tests pass; staged-lint gate pass.
- Follow-up (manual): keep a session open through multiple requests and verify
exactly one `OpenCode` entry exists in the Output dropdown.

## Lessons Learned

`createOutputChannel` is a register operation, not a lookup — any call that
isn't behind a singleton guard will multiply UI entries.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Issue #216 — "No tool output found for function call call_…" (HTTP 400, gpt-5.6-luna)

**Status:** ✅ Solved (branch `fix/issues-216-223-batch`, commit `ed53ecb`)
**Topic:** responses-api / tool-call-pairing
**Updated:** 2026-09-08
**Tags:** #responses #tool-calls #pairing #luna
**GitHub Issue:** [ltmoerdani/opencode-copilot-chat#216](https://github.com/ltmoerdani/opencode-copilot-chat/issues/216)
**Related:** issue doc [90 (#206 fc_ item id normalization)](90-20260903-issue206-luna-responses-fc-id-mismatch.md), issue doc [68 (history trim)](68-20260820-history-trim-context-overflow.md)

---

## Problem

The request after a tool call failed with `Upstream request failed:
[invalid_request_error] No tool output found for function call call_RITWx…`
(HTTP 400) — the whole turn died even though VS Code had delivered the tool
result.

## Analysis

The #206 fix (commit `40be420`) normalized the `function_call` **item id** to
the `fc_` namespace while keeping `call_id` verbatim so it can pair with
`function_call_output.call_id`. Two residual gaps broke that pairing:

1. `responsesInputItemsFromMessage()` fabricated
`call_id: \`tool-${Date.now()}\``for tool messages whose`tool_call_id`
was lost — an id that can never match its call, so the gateway rejected
the request.
2. History replay could deliver a `function_call` without its output (or vice
versa) — e.g. after `trimOldMessagesToFitContext` dropped half a group, or
when VS Code replayed tool messages in unusual shapes. The gateway rejects
the entire request for a single unpaired item.

## Fix

- Never fabricate a `call_id`: a tool message without `tool_call_id` produces
no `function_call_output` item.
- New pure `pairResponsesFunctionCallItems()` enforces strict 1:1 pairing over
the assembled `input` list: orphaned outputs are dropped, calls without an
output are dropped, duplicate outputs keep only the first. Wired into
`buildResponsesRequestBody()`.
- Unit tests cover matched pairs, orphaned output, trimmed call, duplicate
output, and the missing-`tool_call_id` case (`src/test/responsesRequest.test.ts`).

## Files Changed

| File | Change |
| ----------------------------------- | ---------------------------------------------------------- |
| `src/responsesRequest.ts` | no fabricated call_id + `pairResponsesFunctionCallItems()` |
| `src/request/openai.ts` | pairing pass in `buildResponsesRequestBody()` |
| `src/test/responsesRequest.test.ts` | 5 regression tests |

## Verification

- 454/454 unit tests pass (5 new); `npx tsc --noEmit` clean; staged-lint gate pass.
- Follow-up (manual): run a tool-calling agent turn on gpt-5.6-luna via
`/v1/responses` and confirm the follow-up request no longer 400s.

## Lessons Learned

Fixing an id-format rejection (#206) without also enforcing the pairing
invariant left the adjacent failure mode one step away. Pairing should be a
post-condition of request assembly, not an emergent property of history.
Loading
Loading