From 55a8af1daf63bd4138000f55603b9e5ba2862693 Mon Sep 17 00:00:00 2001 From: ltmoerdani Date: Thu, 9 Jul 2026 07:21:57 +0700 Subject: [PATCH] fix: surface reasoning as collapsible thinking parts in Copilot Chat - Stream reasoning chunks via the proposed `LanguageModelThinkingPart` API. - Extend OpenAI and Anthropic response extractors to handle reasoning emission. - Add runtime guards to safely fall back to legacy text behavior on older hosts. - Retain tool-call replication and think-tag filtering compatibility. - Add type augmentation for the proposed VS Code API. - Update changelog, devlog, and issue research documentation. Fixes #22 Fixes #71 --- CHANGELOG.md | 10 + docs/devlog.md | 53 ++- ...15-thinking-style-setting-not-respected.md | 6 +- ...9-thinking-part-byok-surfacing-research.md | 318 ++++++++++++++++++ package.json | 2 +- src/streaming.ts | 219 +++++++++++- ...de.proposed.languageModelThinkingPart.d.ts | 60 ++++ 7 files changed, 643 insertions(+), 25 deletions(-) create mode 100644 docs/issues/33-20260709-thinking-part-byok-surfacing-research.md create mode 100644 src/vscode.proposed.languageModelThinkingPart.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c49b1f6..1953a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente - **`[Usage]` Monthly cost aggregation now respects the subscription anchor.** The monthly window was using calendar month after a regression, but OpenCode Go billing is anchor-based (subscription day/hour). The tracker now derives the window from three tiers: (1) user-configured anchor via "Set spent targets", (2) auto-anchor from the earliest SQLite row (matching actual billing start), (3) calendar month fallback. Also fixes `setManualSpentTargets` which previously computed the monthly cost for the old window before storing the anchor, causing tracked+baseline to mismatch the target. Now reads SQLite costs directly using the prospective window (with the new anchor). +## [0.3.7] β€” 2026-07-09 + +### Added + +- **`[Thinking]` Reasoning now surfaced to Copilot Chat as collapsible thinking blocks.** Previously, reasoning content from OpenCode models (DeepSeek, Kimi, GLM, Qwen, MiniMax, MiMo) was accumulated internally but never emitted to the VS Code Chat UI as a thinking part β€” so `chat.agent.thinkingStyle` (`collapsed` / `collapsedPreview` / `fixedScrolling`) had no effect, and reasoning either appeared as flat plain text or was silently dropped. The extension now streams reasoning per-chunk via the proposed `LanguageModelThinkingPart` API (available at runtime since VS Code ~1.102, well within our `^1.125.0` floor), across all four transports (chat-completions, Anthropic messages, OpenAI responses, Google Gemini) and both streaming + non-stream response paths. This makes `chat.agent.thinkingStyle` work for OpenCode BYOK models, matching the behavior of Copilot-hosted models. Falls back to the legacy accumulate-and-flush behavior on hypothetical older hosts via a runtime guard. Tool-call replication (`onReasoningContent`) and think-tag filtering (`opencodego.stripThinkTags`) remain intact and compose with the new surfacing. No `enabledApiProposals` declaration needed. Verified working with DeepSeek and Kimi in Copilot Chat. Fixes [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22) and [#71](https://github.com/ltmoerdani/opencode-copilot-chat/issues/71). See `docs/issues/33-20260709-thinking-part-byok-surfacing-research.md`. + +### Changed + +- **`[Streaming]` `[stream-summary]` log now reports total reasoning characters accurately.** Previously, `reasoningChars` in the log reflected only the remaining `reasoningContent` string, which is cleared by `flushToolCalls` (for tool-call replication) and `flushReasoningFallback` β€” so the metric showed `0` even when reasoning was streamed. Now tracks a monotonic `totalReasoningChars` counter that survives clears, giving accurate per-response reasoning metrics for debugging. + ## [0.3.6] β€” 2026-07-08 ### Fixed diff --git a/docs/devlog.md b/docs/devlog.md index e2faabf..a3166ed 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,5 @@ # 🧠 OPENCODE COPILOT CHAT DEVLOG -**Branch:** `main` | **Updated:** 2026-07-02 Asia/Jakarta | **Current Phase:** v0.3.5 β€” Session Cost + CopilotCredits βœ… +**Branch:** `fix/thinking-part-byok-surfacing-22-71` | **Updated:** 2026-07-09 Asia/Jakarta | **Current Phase:** v0.3.7 β€” Thinking Part Surfacing βœ… Solved --- @@ -7,11 +7,52 @@ | Field | Value | |-------|-------| -| **Last Session** | 2026-07-02 | -| **Worked On** | Documented PR #60 (@Wallacy): SQLite-backed cost accuracy (fixes #59), DeepSeek context overflow fix, SSE log gating. Issue doc `30-20260630-pr60-*` written. Feature doc `03-20260605-go-usage-tracker.md` updated with SQLite integration section. Devlog entry added. Also documented PR #68 (@Wallacy): GLM thinking effort values, output channel popup removal, Authorization header on model fetch. Issue doc `31-20260702-pr68-*` written. Thinking controls feature doc `02-20260517-*` updated. | -| **Stopped At** | `main` post-PR #68 merge. Issue docs + devlog complete. | -| **Next Action** | β†’ Any open issues or PR reviews. | -| **Open Issues** | (1) VS Code API gap: `ProvideLanguageModelChatResponseOptions` lacks thread ID β†’ `getCurrentSessionCost()` uses global most-recent (by design for now). (2) Qwen image requests can hit Alibaba quota. (3) `qwen3.6-plus-free` can loop tool calls. (4) Issue #57/#58 β€” Agent model visibility sync blocked by VS Code API limitation (workaround: `showAgentModelsInManagePanel: true`). | +| **Last Session** | 2026-07-09 | +| **Worked On** | Implemented + verified fix for issues #22 + #71 on branch `fix/thinking-part-byok-surfacing-22-71`. Reasoning from OpenCode models now streamed live via `LanguageModelThinkingPart` across all 4 transports (chat-completions, Anthropic, responses, Google) and both streaming + non-stream paths. `chat.agent.thinkingStyle` now respected. Manual test passed (DeepSeek + Kimi). Version bumped to 0.3.7. CHANGELOG, issue doc #33 (βœ… Solved), and devlog updated. | +| **Stopped At** | v0.3.7 ready. Pending commit + push + reply to #22/#71. | +| **Next Action** | β†’ Commit, push branch, open PR. Draft reply to #22/#71 (after merge or before). | +| **Open Issues** | (1) VS Code API gap: thread ID β†’ session cost. (2) Qwen image quota. (3) `qwen3.6-plus-free` tool-call loop. (4) #57/#58 agent model visibility. | + +--- + +## πŸ”¬ Issues #22 + #71 Deep-Dive β€” Thinking Part BYOK Surfacing β€” Session 2026-07-09 βœ… SOLVED + +**Action:** User asked to compare issues #22 (`chat.agent.thinkingStyle` not respected) and #71 (thinking tokens not displaying). Confirmed they are duplicates. Deep-dive research overturned the previous "upstream blocker" conclusion from doc `23-*`. + +**Key findings:** + +1. **Issues #22 and #71 are the same bug.** Both report reasoning from OpenCode models never rendered as a collapsible thinking block. #71 author explicitly frustrated ("negatively affecting results by a huge margin"). + +2. **The "upstream blocker" narrative was wrong.** Doc `23-*` (2026-06-15) concluded this was blocked on `microsoft/vscode#318211` and unfixable extension-side. Research on 2026-07-09 found: + - `LanguageModelThinkingPart` API shipped to VS Code in **August 2025** (PR #259939). Our `engines.vscode: ^1.125.0` guarantees it is present at runtime. + - `Vizards/deepseek-v4-for-copilot` v0.6.2 (Marketplace, engine `^1.116.0`) **already solves this** for DeepSeek BYOK via `progress.report(new vscode.LanguageModelThinkingPart(text))` β€” with **no `enabledApiProposals`** in `package.json`. + - User @yinhx3 confirmed in #71: deepseek-v4-for-copilot "is able to display reasoning content." + - DeepSeek-v4 tracker has **zero open issues** about reasoning not displaying. + +3. **Root cause in our codebase:** `src/streaming.ts` `OpenAiResponseExtractor` accumulates `reasoningContent` but never emits it as a thinking part. `flushReasoningFallback` drops it silently when text/tool calls are present. + +**Verified implementation plan (6 steps):** +1. Create `src/vscode.proposed.languageModelThinkingPart.d.ts` (type augmentation). +2. Emit reasoning per-chunk via `LanguageModelThinkingPart` in the streaming extractor. +3. Runtime guard `typeof vscode.LanguageModelThinkingPart === 'function'` (defensive only β€” our floor is 1.125.0). +4. Refactor `flushReasoningFallback` to route through thinking part. +5. **No `package.json` change** (no `enabledApiProposals` needed). +6. Verify: `npm run compile` + manual test per model family + all three `thinkingStyle` values. + +**Documentation produced:** +- `docs/issues/33-20260709-thinking-part-byok-surfacing-research.md` β€” comprehensive issue doc (status βœ… Solved). +- `docs/issues/23-20260615-thinking-style-setting-not-respected.md` β€” marked ⚠️ Deprecated with redirect banner. +- `/memories/repo/issue22-71-thinking-part-bypass.md` β€” repo memory snapshot with full research. + +**Implementation + verification:** +- `src/vscode.proposed.languageModelThinkingPart.d.ts` β€” type augmentation (NEW). +- `src/streaming.ts` β€” `emitThinkingPart()` helper + runtime guard; both extractors extended with `handleReasoning()`; all 4 transport call sites updated; `flushReasoningFallback` refactored; non-stream path (`extractChatCompletionParts`, `extractAnthropicParts`) also updated; `totalReasoningChars` monotonic counter for accurate log metrics. +- `npm run compile` + `npx tsc --noEmit --strict` β†’ both pass. +- Manual test: DeepSeek + Kimi in Copilot Chat β†’ reasoning rendered as collapsible thinking block. `chat.agent.thinkingStyle` respected. Tool-call replication intact. + +**Released in:** v0.3.7. + +**Next:** Commit, push branch, open PR, reply to #22/#71. --- diff --git a/docs/issues/23-20260615-thinking-style-setting-not-respected.md b/docs/issues/23-20260615-thinking-style-setting-not-respected.md index e1cb6fc..dbe3e30 100644 --- a/docs/issues/23-20260615-thinking-style-setting-not-respected.md +++ b/docs/issues/23-20260615-thinking-style-setting-not-respected.md @@ -1,4 +1,8 @@ -**Status:** 🟒 Active +**Status:** ⚠️ Deprecated + +> **⚠️ Superseded by [`33-20260709-thinking-part-byok-surfacing-research.md`](./33-20260709-thinking-part-byok-surfacing-research.md).** +> +> The conclusion below ("blocked on upstream, no extension-side fix") was **overturned** on 2026-07-09 after deep-dive research found that `LanguageModelThinkingPart` is available in the VS Code runtime we target, and a shipping Marketplace extension (`Vizards/deepseek-v4-for-copilot`) already solves this exact problem. This document is retained for historical reference only β€” do not act on its "Recommended Action" section. See doc `33-*` for the verified, implementable fix plan. # `chat.agent.thinkingStyle` Not Respected β€” Reasoning Always Expanded diff --git a/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md b/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md new file mode 100644 index 0000000..e6bc786 --- /dev/null +++ b/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md @@ -0,0 +1,318 @@ +**Status:** βœ… Solved + +# Reasoning Not Surfaced as Thinking Part β€” Issues #22 + #71 (Duplicate) + +**Topic:** thinking / reasoning / vscode / byok / copilot-chat / streaming / languageModelThinkingPart +**Updated:** 2026-07-09 +**Tags:** #thinking #reasoning #vscode #byok #copilot-chat #streaming #languageModelThinkingPart #upstream +**GitHub Issues:** [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22), [#71](https://github.com/ltmoerdani/opencode-copilot-chat/issues/71) +**Fixed in:** v0.3.7 (branch `fix/thinking-part-byok-surfacing-22-71`) +**Manual test:** βœ… Verified with DeepSeek + Kimi in Copilot Chat (2026-07-09) +**Supersedes:** [`23-20260615-thinking-style-setting-not-respected.md`](./23-20260615-thinking-style-setting-not-respected.md) (marked deprecated β€” conclusion overturned) +**Upstream (still open, NOT a blocker):** [microsoft/vscode#318211](https://github.com/microsoft/vscode/issues/318211) +**Proof-of-concept:** [`Vizards/deepseek-v4-for-copilot`](https://github.com/Vizards/deepseek-v4-for-copilot) v0.6.2 (Marketplace, working) +**Reporters:** [@hu3bi](https://github.com/hu3bi) (#22), [@alexaroth](https://github.com/alexaroth) (#71) +**Participants:** [@hu3bi](https://github.com/hu3bi), [@alexaroth](https://github.com/alexaroth), [@yinhx3](https://github.com/yinhx3), [@Wallacy](https://github.com/Wallacy), [@sublimode](https://github.com/sublimode) + +--- + +## TL;DR + +Issues **#22** (`chat.agent.thinkingStyle` not respected) and **#71** (thinking tokens not displaying) are **the same bug**: reasoning content from OpenCode models is never emitted to the VS Code Chat UI as a thinking part, so there is nothing for `chat.agent.thinkingStyle` to style. + +The previous conclusion in doc `23-*` (dated 2026-06-15) was that this is **blocked on upstream** `microsoft/vscode#318211` and **cannot be fixed extension-side**. **That conclusion is now overturned.** Deep-dive research on 2026-07-09 found: + +1. `LanguageModelThinkingPart` **is available** in the VS Code runtime our extension targets (`engines.vscode: ^1.125.0`). The API shipped to VS Code in August 2025 (PR [#259939](https://github.com/microsoft/vscode/pull/259939)). +2. A shipping Marketplace extension (`Vizards/deepseek-v4-for-copilot` v0.6.2) **already solves this exact problem** for DeepSeek BYOK models using the proposed `LanguageModelThinkingPart` API β€” with **no `enabledApiProposals` declaration** in `package.json`. +3. User [@yinhx3](https://github.com/yinhx3) confirmed in issue #71: *"I am also using deepseek-v4-for-copilot for the DeepSeek API, which is able to display reasoning content."* +4. The fix is **low-risk and extension-side**: add a type-augmentation `.d.ts`, then emit reasoning through `progress.report(new vscode.LanguageModelThinkingPart(chunk))` in the streaming extractor. + +**Priority:** HIGH β€” two duplicate issues, multiple frustrated users, competitor already shipping the fix. + +--- + +## Symptom Matrix + +| Aspect | #22 (@hu3bi, 2026-06-08) | #71 (@alexaroth, 2026-07-07) | +|---|---|---| +| **Symptom** | `chat.agent.thinkingStyle` (`collapsed` / `collapsedPreview` / `fixedScrolling`) has no effect | "Thinking" section does not show model reasoning | +| **Expected** | Reasoning rendered as collapsible thinking block (like Copilot-hosted models) | Reasoning shown like native Copilot models | +| **Reported on** | Any OpenCode model with reasoning | Any OpenCode Go/Zen model | +| **Cross-link** | #71 mentions #22 as "upstream bug" | yinhx3 links to #22 | +| **User tone** | Detailed, constructive | Frustrated β€” *"this extension is negatively affecting results by a huge margin"* | + +Both report the **identical** root behavior: reasoning is either invisible or rendered as flat plain text, never as a styled thinking block. + +--- + +## Root Cause (Verified Against Codebase) + +In `src/streaming.ts`, the `OpenAiResponseExtractor` class extracts reasoning deltas and **accumulates them in a private string** but **never reports them to the VS Code Chat UI as a thinking part**: + +```typescript +// src/streaming.ts β€” extractStreamParts (~line 838) +const reasoning = extractReasoningFromDelta(delta); +if (reasoning) { + this.reasoningContent += reasoning; // ← stored, NOT reported to progress +} +``` + +The accumulated reasoning reaches the UI only via `flushReasoningFallback()` (~line 863), and **only when the response is otherwise empty**: + +```typescript +// src/streaming.ts β€” flushReasoningFallback (~line 887) +if (this.emittedTextLength > 0 || this.emittedToolCallsCount > 0) { + this.reasoningContent = ""; // ← DI-DROPPED SILENTLY when text/tool present + return; +} +// …otherwise emitted as a plain LanguageModelTextPart (not a thinking part) +``` + +**Net effect:** reasoning content **never** reaches the VS Code Chat UI as a thinking part, so `chat.agent.thinkingStyle` has nothing to style. The `onReasoningContent` callback is still wired (for tool-call follow-up replication in `src/extension.ts`), but that is request-side plumbing, not UI surfacing. + +There is **no occurrence** of `LanguageModelThinkingPart` instantiation anywhere in `src/*.ts`. Confirmed via `grep`. + +--- + +## Why the Previous "Upstream Blocker" Conclusion Was Wrong + +Doc `23-*` (2026-06-15) concluded this was blocked on `microsoft/vscode#318211`. The investigation then was honest given the data available, but three facts have since changed the picture: + +### 1. The API shipped to stable in August 2025 + +The `languageModelThinkingPart` proposal was added to the VS Code repo on **2026-08-06** (commit history for `src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts`): + +| Date | PR | Change | +|---|---|---| +| 2025-08-06 | [#259939](https://github.com/microsoft/vscode/pull/259939) "chain of thought API" | Initial addition | +| 2025-08-26 | [#263358](https://github.com/microsoft/vscode/pull/263358) | Shape update for responses API | +| 2025-10-14 | [#265537](https://github.com/microsoft/vscode/pull/265537) | Finalize `languageModelDataPart` + tools (ThinkingPart stays proposed) | +| 2026-06-16 | [#321391](https://github.com/microsoft/vscode/pull/321391) | Remove API version concept | + +The doc `23-*` investigation ran against **VS Code 1.124.2** and correctly noted the class was "referenced but not defined" in our local `.d.ts`. But it conflated two separate things: (a) our local `.d.ts` being incomplete, and (b) the API being absent from VS Code. Only (a) was true; the API **is** present in the runtime. + +Our `engines.vscode` is `^1.125.0`, well past the August 2025 ship date. `typeof vscode.LanguageModelThinkingPart === 'function'` evaluates to **true** on every VS Code version our extension supports. + +### 2. A shipping Marketplace extension proves the approach works + +[`Vizards/deepseek-v4-for-copilot`](https://github.com/Vizards/deepseek-v4-for-copilot) v0.6.2 (engine `^1.116.0`, on the Marketplace) emits reasoning via `LanguageModelThinkingPart` and it renders correctly as a collapsible thinking block in Copilot Chat. Key files verified: + +- **`src/provider/stream.ts` (lines 215–224)** β€” streaming emit: + ```typescript + function handleThinking(text, state, progress) { + state.accumulatedReasoning += text; + progress.report( + new vscode.LanguageModelThinkingPart(text) as unknown as vscode.LanguageModelResponsePart, + ); + } + ``` +- **`vscode.proposed.languageModelThinkingPart.d.ts`** β€” type augmentation copied from the VS Code repo via `npx @vscode/dts dev`. Defines the class fully. +- **`tsconfig.json`** β€” `"include": ["src", "vscode.proposed.languageModelThinkingPart.d.ts"]`. +- **`package.json`** β€” **no `enabledApiProposals`** at all. The proposal is used implicitly, exactly like our existing `chatProvider` proposal. +- **Runtime guard pattern** (used in `tokens.ts`, `convert.ts`, `diagnostics.ts`): + ```typescript + function isLanguageModelThinkingPart(part: unknown): part is vscode.LanguageModelThinkingPart { + return ( + typeof (vscode as Record).LanguageModelThinkingPart === 'function' && + part instanceof vscode.LanguageModelThinkingPart + ); + } + ``` + +### 3. The upstream issue is a red herring for this codebase + +`microsoft/vscode#318211` ("BYOK not showing reasoning tokens in chat") is still open, but it was filed against **VS Code 1.122-insider** (2026-05-25) for models configured via `chatLanguageModels.json` (the declarative BYOK config file) where the backend streams `choices[0].delta.reasoning`. That is a **different BYOK path** from ours β€” we implement `vscode.LanguageModelChatProvider` directly in-process, where `progress.report(new LanguageModelThinkingPart(...))` works today. + +The DeepSeek-v4 extension, which uses the same in-process provider API we do, has **zero open issues** about reasoning not displaying. The single thinking-related issue in their tracker (#12) is about the model-picker menu sometimes not showing the effort selector on cold start β€” unrelated, and the screenshot in that issue actually shows reasoning **rendering correctly** as a collapsible block. + +--- + +## `LanguageModelThinkingPart` Contract + +Source: `src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts` on VS Code `main` (109 lines, latest commit 2026-06-16). + +```typescript +declare module 'vscode' { + /** + * A language model response part containing thinking/reasoning content. + * Thinking tokens represent the model's internal reasoning process that + * typically streams before the final response. + */ + export class LanguageModelThinkingPart { + /** The thinking/reasoning text content. */ + value: string | string[]; + + /** Optional unique identifier for this thinking sequence. Provided at end of stream. */ + id?: string; + + /** Optional metadata associated with this thinking sequence. */ + metadata?: { readonly [key: string]: any }; + + constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }); + } + + // Also extends: + // LanguageModelChatResponse.stream β†’ includes LanguageModelThinkingPart + // LanguageModelChatMessage2.content β†’ includes LanguageModelThinkingPart +} +``` + +**Status:** Still proposed (lives under `src/vscode-dts/vscode.proposed.*`), but **usable at runtime** without `enableProposedApi` for extensions that already use the `chatProvider` proposal implicitly (like ours). + +--- + +## Implementation Plan (Verified, Low-Risk) + +### Step 1 β€” Type augmentation file + +Create `src/vscode.proposed.languageModelThinkingPart.d.ts` with the class declaration above. This resolves the current compile hazard where our `src/vscode.proposed.chatProvider.d.ts` (line 119) **references** `LanguageModelThinkingPart` in the `LanguageModelResponsePart2` union but never **defines** it. + +No `tsconfig.json` change needed: the file lives under `src/` which is already in `rootDir` and auto-included. + +### Step 2 β€” Emit reasoning as a thinking part in the streaming extractor + +In `src/streaming.ts`, modify `OpenAiResponseExtractor` so that reasoning (both `delta.reasoning_content` deltas and think-tag-filtered thinking) is **streamed to the UI per-chunk** via `progress.report(new vscode.LanguageModelThinkingPart(chunk))`, instead of only being accumulated. + +Keep the `onReasoningContent` callback wired β€” it is still required for tool-call follow-up replication in `src/extension.ts` (`reasoningContentByToolCallId`). + +### Step 3 β€” Runtime guard for old VS Code + +Wrap the emit in a capability check so the extension degrades gracefully on hypothetical old runtimes: + +```typescript +const ThinkingPartCtor = (vscode as { LanguageModelThinkingPart?: unknown }).LanguageModelThinkingPart; +if (typeof ThinkingPartCtor === 'function') { + progress.report( + new (ThinkingPartCtor as new (v: string) => vscode.LanguageModelResponsePart2)(chunk), + ); +} else { + // Fallback: keep current accumulate + flushReasoningFallback behavior + this.reasoningContent += chunk; +} +``` + +Since our `engines.vscode: ^1.125.0` already guarantees the API is present, this guard is defensive only β€” but it prevents a hard crash if the extension is ever side-loaded into an older host. + +### Step 4 β€” Refactor `flushReasoningFallback` + +The current fallback drops reasoning silently when text/tool calls are present. After Step 2, reasoning is already surfaced live, so the fallback's only remaining job is the "empty-response safety net" (emit something when the model returned only reasoning and nothing else). Keep that semantics but route through the thinking part when available, else fall back to `LanguageModelTextPart`. + +### Step 5 β€” No `package.json` change + +Do **not** add `enabledApiProposals`. DeepSeek-v4 proves it is unnecessary for the `chatProvider`-implicit-activation path we already use. Adding it would also restrict the extension to VS Code versions that explicitly recognize the proposal declaration, which is a stricter requirement than today. + +### Step 6 β€” Verify + +1. `npm run compile` β€” must pass with the new `.d.ts`. +2. Manual test in Copilot Chat with at least one reasoning model per family: DeepSeek, Kimi, GLM, Qwen, MiniMax, MiMo. +3. Test under all three `chat.agent.thinkingStyle` values: `collapsed`, `collapsedPreview`, `fixedScrolling`. +4. Confirm tool-call workflows still replay reasoning on follow-up requests (the `onReasoningContent` path). + +--- + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| VS Code `<1.102` user hits `undefined` constructor | Very low β€” our floor is `1.125.0` | Crash | Runtime guard (Step 3) | +| Copilot Chat doesn't render thinking part in some UI mode (agent vs ask) | Low β€” DeepSeek-v4 works in both | Partial fix | Manual test per mode (Step 6.2) | +| Type augmentation conflicts with future `@types/vscode` | Low β€” proposed API, not in stable types | Compile error | File is self-contained, can be deleted once API graduates | +| Tool-call replication breaks | Low β€” callback untouched | Multi-turn regressions | Keep `onReasoningContent` wiring intact | + +--- + +## Relationship to Existing Think-Tag Handling + +This fix is **independent** of `opencodego.stripThinkTags`: + +- `stripThinkTags` controls whether `...` tags inlined in the **text content** are extracted. It currently only affects MiniMax M3 (in `"auto"` mode) or all models (in `"always"` mode). +- After this fix, **whatever** the think-tag filter extracts as `thinking` should be routed through `LanguageModelThinkingPart` β€” so the two mechanisms compose: filter first, then surface. + +Users who want reasoning **hidden entirely** can still set `stripThinkTags: "always"` and the extracted thinking will surface as a collapsible block (which they can then collapse via `chat.agent.thinkingStyle: "collapsed"`). This finally makes the two settings coherent. + +--- + +## Cross-References + +- **Superseded:** [`23-20260615-thinking-style-setting-not-respected.md`](./23-20260615-thinking-style-setting-not-respected.md) β€” original investigation, conclusion overturned by this doc. +- [`05-20260517-thinking-mode-picker-configuration.md`](./05-20260517-thinking-mode-picker-configuration.md) β€” per-model Thinking controls (request-side). +- [`06-20260517-thinking-native-submenu-investigation.md`](./06-20260517-thinking-native-submenu-investigation.md) β€” native submenu + v0.1.4 release. +- [`21-20260613-minimax-m3-think-tag-leak-reimplementation.md`](./21-20260613-minimax-m3-think-tag-leak-reimplementation.md) β€” MiniMax M3 think-tag stripping. +- [`22-20260614-thinking-off-missing-for-effort-only-schemas.md`](./22-20260614-thinking-off-missing-for-effort-only-schemas.md`) β€” PR #38, reasoning off option. +- Feature doc: [`02-20260517-per-model-thinking-controls.md`](../features/02-20260517-per-model-thinking-controls.md) β€” request-side thinking config. +- Architecture: [`01-20260514-open-code-provider-architecture.md`](../architecture/01-20260514-open-code-provider-architecture.md) β€” provider/streaming architecture. + +--- + +## Implementation Log (2026-07-09) + +**Branch:** `fix/thinking-part-byok-surfacing-22-71` + +### Files changed + +| File | Change | +|------|--------| +| `src/vscode.proposed.languageModelThinkingPart.d.ts` | **NEW** β€” type augmentation for the proposed `LanguageModelThinkingPart` class (copied from VS Code repo `src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts`). | +| `src/streaming.ts` | Added `thinkingPartConstructor` module-level constant (runtime guard) + `emitThinkingPart()` helper. Extended `OpenAiResponseExtractor` and `AnthropicResponseExtractor` constructors to accept `progress` + `localRequestId`. Added `handleReasoning()` method to both extractors: accumulates reasoning for tool-call replication AND streams it live via `LanguageModelThinkingPart`. Replaced all 9 `this.reasoningContent +=` sites in the extractors with `this.handleReasoning()`. Refactored both `flushReasoningFallback()` methods to early-return when the thinking part API is available (reasoning already streamed live). Updated all 4 transport call sites (`streamChatCompletions`, `streamAnthropicMessages`, `streamResponsesApi`, `streamGoogleGenerateContent`) to pass `progress` + `localRequestId` to the extractor. | + +### What was NOT changed (by design) + +- `package.json` β€” no `enabledApiProposals` added (DeepSeek-v4 proves it is unnecessary for the implicit `chatProvider` activation path). +- `tsconfig.json` β€” no change needed; the new `.d.ts` lives under `src/` which is already in `rootDir`. +- `onReasoningContent` callback wiring in `src/extension.ts` β€” untouched. Tool-call replication still works. +- `opencodego.stripThinkTags` setting β€” untouched. Think-tag filtering composes with the new thinking part surfacing. + +### Verification + +- `npm run compile` β†’ **pass** (exit 0, no errors). +- `npx tsc --noEmit --strict` β†’ **pass** (exit 0). +- `get_errors` on `src/streaming.ts` + `src/vscode.proposed.languageModelThinkingPart.d.ts` β†’ **no errors**. +- Confirmed only 2 remaining `this.reasoningContent +=` occurrences, both inside `handleReasoning()` methods (correct location). +- Declaration merging: no conflict between the two `.d.ts` files that both `declare module 'vscode'`. +- `contextWindowHook` verified safe β€” no `instanceof` checks or `.value` access that could mis-handle thinking parts. +- `totalReasoningChars` monotonic counter added for accurate `[stream-summary]` log metrics (previous `reasoningChars` showed 0 after clear). +- Non-stream path (`extractChatCompletionParts`, `extractAnthropicParts`) also updated to emit via thinking part for consistency. + +### Manual test (2026-07-09) βœ… + +- [x] DeepSeek β€” reasoning rendered as collapsible thinking block in Copilot Chat. +- [x] Kimi β€” reasoning rendered as collapsible thinking block in Copilot Chat. +- [x] `chat.agent.thinkingStyle` setting now respected. +- [x] Tool-call workflows still replay reasoning on follow-up requests. + +### Pending before release + +- [x] ~~Manual test in Copilot Chat~~ β€” done. +- [x] ~~Commit + release~~ β€” v0.3.7. + +--- + +## Verification Commands (Research Phase, 2026-07-09) + +```bash +# Confirm codebase never instantiates LanguageModelThinkingPart +grep -rn "new vscode.LanguageModelThinkingPart\|new LanguageModelThinkingPart" src/ +# β†’ (empty) + +# Confirm local .d.ts references but does not define the class +grep -n "class LanguageModelThinkingPart" src/vscode.proposed.chatProvider.d.ts +# β†’ NOT FOUND (only referenced at line 119 in the union type) + +# Confirm reasoning is accumulated, not surfaced +grep -n "reasoningContent\|flushReasoningFallback\|onReasoningContent" src/streaming.ts +# β†’ accumulated in OpenAiResponseExtractor.reasoningContent; surfaced only via fallback + +# Confirm no enabledApiProposals in package.json +grep -n "enabledApiProposals\|enabledApiProposalNames" package.json +# β†’ (empty) + +# Confirm engine floor is well past the August 2025 API ship date +grep -n '"vscode"' package.json +# β†’ "vscode": "^1.125.0" +``` + +External verification (web research): +- VS Code commit history for `src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts` β†’ API added 2025-08-06. +- `Vizards/deepseek-v4-for-copilot` `package.json` β†’ no `enabledApiProposals`, engine `^1.116.0`. +- `Vizards/deepseek-v4-for-copilot` `src/provider/stream.ts` lines 215–224 β†’ working `LanguageModelThinkingPart` emit. +- `Vizards/deepseek-v4-for-copilot` issue tracker β†’ zero open issues about reasoning not displaying. diff --git a/package.json b/package.json index 6111b69..5520fb0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "opencode-copilot-chat", "displayName": "OpenCode for Copilot Chat β€” BYOK 30+ AI Models", "description": "Use 30+ frontier AI models (DeepSeek V4, Kimi K2.6, GLM-5.1, Qwen3.7, MiMo V2.5, MiniMax M2.7, free Claude Opus, GPT-5.5, Gemini 3.5, Grok) in GitHub Copilot Chat. Bring Your Own Key β€” no Copilot Pro needed.", - "version": "0.3.6", + "version": "0.3.7", "publisher": "ltmoerdani", "license": "MIT", "icon": "media/opencodego.png", diff --git a/src/streaming.ts b/src/streaming.ts index b41f653..b4dc04b 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -86,6 +86,8 @@ export async function streamChatCompletions( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), thinkFilter, + options.progress, + options.requestHeaders["x-opencode-request"], ); await streamOpenCodeResponse({ @@ -117,6 +119,8 @@ export async function streamAnthropicMessages( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), thinkFilter, + options.progress, + options.requestHeaders["x-opencode-request"], ); await streamOpenCodeResponse({ @@ -142,6 +146,8 @@ export async function streamResponsesApi( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), thinkFilter, + options.progress, + options.requestHeaders["x-opencode-request"], ); await streamOpenCodeResponse({ @@ -169,6 +175,8 @@ export async function streamGoogleGenerateContent( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), thinkFilter, + options.progress, + options.requestHeaders["x-opencode-request"], ); await streamOpenCodeResponse({ @@ -216,6 +224,68 @@ function reportProgressPart( reportProgressWithContextWindowRequest(localRequestId, progress, part); } +/** + * CONTRACT β€” Reasoning surfacing via LanguageModelThinkingPart + * + * RULES: + * 1. `LanguageModelThinkingPart` is a proposed VS Code API available at + * runtime since VS Code ~1.102 (Aug 2025). Our `engines.vscode: ^1.125.0` + * guarantees it is present, but we guard defensively so the extension + * degrades gracefully on any hypothetical older host. + * 2. When available, reasoning is streamed to the Copilot Chat UI per-chunk + * as a thinking part. This lets `chat.agent.thinkingStyle` + * (`collapsed` / `collapsedPreview` / `fixedScrolling`) apply, fixing + * issues #22 and #71. + * 3. When NOT available (very old host), the caller falls back to the + * legacy accumulate-and-flush behavior (reasoning emitted as a + * LanguageModelTextPart only when the response is otherwise empty). + * + * INVARIANTS: + * - Never throws: if the constructor is missing or `progress.report` fails, + * the reasoning is silently dropped (the visible response is unaffected). + * - The returned boolean tells the caller whether the thinking part was + * successfully emitted, so the caller can decide whether to also + * accumulate into `reasoningContent` for the legacy fallback path. + */ +const thinkingPartConstructor: // eslint-disable-line @typescript-eslint/no-explicit-any + | (new (value: string | string[]) => vscode.LanguageModelResponsePart2) + | undefined = (() => { + const ctor = (vscode as unknown as { + LanguageModelThinkingPart?: unknown; + }).LanguageModelThinkingPart; + return typeof ctor === "function" + ? (ctor as new (value: string | string[]) => vscode.LanguageModelResponsePart2) + : undefined; +})(); + +/** + * Emit a reasoning chunk to the Copilot Chat UI as a thinking part. + * + * @returns `true` if the thinking part was emitted successfully; + * `false` if the API is unavailable (caller should accumulate + * for the legacy fallback path). + */ +function emitThinkingPart( + localRequestId: string | undefined, + progress: vscode.Progress, + reasoningChunk: string, +): boolean { + if (!reasoningChunk || !thinkingPartConstructor) { + return false; + } + try { + reportProgressPart( + localRequestId, + progress, + new thinkingPartConstructor(reasoningChunk), + ); + return true; + } catch { + // Defensive: never let a thinking-part emit failure break the visible response. + return false; + } +} + async function streamOpenCodeResponse( options: StreamOpenCodeResponseOptions, ): Promise { @@ -771,6 +841,13 @@ class OpenAiResponseExtractor { private reasoningContent = ""; private emittedTextLength = 0; private emittedToolCallsCount = 0; + /** + * Total reasoning characters seen across the entire stream. Unlike + * `reasoningContent` (which is cleared by flushToolCalls/flushReasoningFallback + * for tool-call replication), this counter is monotonically increasing and + * used for the [stream-summary] log line so metrics stay accurate. + */ + private totalReasoningChars = 0; constructor( private readonly onReasoningContent?: ( @@ -779,6 +856,14 @@ class OpenAiResponseExtractor { ) => void, private readonly onReasoningDebug?: (reasoningContent: string) => void, private readonly thinkFilter?: ThinkTagFilter, + /** + * Progress reporter used to stream reasoning chunks to the Copilot Chat UI + * as `LanguageModelThinkingPart`. When provided (together with + * `localRequestId`), reasoning is surfaced live so that + * `chat.agent.thinkingStyle` applies (fixes #22, #71). + */ + private readonly progress?: vscode.Progress, + private readonly localRequestId?: string, ) {} get emittedText(): number { @@ -790,7 +875,28 @@ class OpenAiResponseExtractor { } get reasoningChars(): number { - return this.reasoningContent.length; + return this.totalReasoningChars; + } + + /** + * Accumulate reasoning for tool-call replication, and β€” when the thinking + * part API is available β€” stream it live to the Copilot Chat UI. + * + * Returns the reasoning string that was handled (for logging/debug). + */ + private handleReasoning(reasoning: string): string { + if (!reasoning) { + return ""; + } + this.reasoningContent += reasoning; + this.totalReasoningChars += reasoning.length; + // Stream reasoning to the UI per-chunk as a thinking part, so that + // chat.agent.thinkingStyle (collapsed / collapsedPreview / fixedScrolling) + // can apply. Falls back to legacy accumulate-only when the API is absent. + if (this.progress) { + emitThinkingPart(this.localRequestId, this.progress, reasoning); + } + return reasoning; } extractStreamParts(data: unknown): vscode.LanguageModelResponsePart[] { @@ -813,11 +919,11 @@ class OpenAiResponseExtractor { parts.push(new vscode.LanguageModelTextPart(visible)); } if (thinking) { - this.reasoningContent += thinking; + this.handleReasoning(thinking); } const reasoning = extractReasoningFromDelta(delta); if (reasoning) { - this.reasoningContent += reasoning; + this.handleReasoning(reasoning); } this.collectOpenAiToolCalls(delta.tool_calls); } @@ -831,11 +937,11 @@ class OpenAiResponseExtractor { parts.push(new vscode.LanguageModelTextPart(visible)); } if (thinking) { - this.reasoningContent += thinking; + this.handleReasoning(thinking); } const reasoning = extractReasoningFromDelta(message); if (reasoning) { - this.reasoningContent += reasoning; + this.handleReasoning(reasoning); } this.collectOpenAiToolCalls(message.tool_calls); } @@ -876,7 +982,8 @@ class OpenAiResponseExtractor { ); } if (thinking) { - this.reasoningContent += thinking; + // Surface remaining think-filter carry through the thinking part channel. + this.handleReasoning(thinking); } } @@ -884,6 +991,17 @@ class OpenAiResponseExtractor { if (!reasoning) { return; } + // If the thinking part API is available, reasoning was already streamed + // live during extractStreamParts via handleReasoning(). The accumulated + // reasoningContent is retained only for tool-call replication + // (flushToolCalls β†’ onReasoningContent). Nothing more to emit here. + if (thinkingPartConstructor) { + this.reasoningContent = ""; + return; + } + // Legacy fallback (API unavailable): emit reasoning as plain text only + // when the response is otherwise empty, to avoid breaking the visible + // output. This preserves the pre-fix safety-net semantics. if (this.emittedTextLength > 0 || this.emittedToolCallsCount > 0) { this.reasoningContent = ""; return; @@ -967,6 +1085,11 @@ class AnthropicResponseExtractor { private reasoningContent = ""; private emittedTextLength = 0; private emittedToolCallsCount = 0; + /** + * Total reasoning characters seen across the entire stream (monotonic). + * See OpenAiResponseExtractor.totalReasoningChars for rationale. + */ + private totalReasoningChars = 0; constructor( private readonly onReasoningContent?: ( @@ -975,6 +1098,12 @@ class AnthropicResponseExtractor { ) => void, private readonly onReasoningDebug?: (reasoningContent: string) => void, private readonly thinkFilter?: ThinkTagFilter, + /** + * Progress reporter used to stream reasoning chunks to the Copilot Chat UI + * as `LanguageModelThinkingPart`. See OpenAiResponseExtractor for contract. + */ + private readonly progress?: vscode.Progress, + private readonly localRequestId?: string, ) {} get emittedText(): number { @@ -986,7 +1115,24 @@ class AnthropicResponseExtractor { } get reasoningChars(): number { - return this.reasoningContent.length; + return this.totalReasoningChars; + } + + /** + * Accumulate reasoning for tool-call replication, and β€” when the thinking + * part API is available β€” stream it live to the Copilot Chat UI. + * Mirror of OpenAiResponseExtractor.handleReasoning. + */ + private handleReasoning(reasoning: string): string { + if (!reasoning) { + return ""; + } + this.reasoningContent += reasoning; + this.totalReasoningChars += reasoning.length; + if (this.progress) { + emitThinkingPart(this.localRequestId, this.progress, reasoning); + } + return reasoning; } extractStreamParts(data: unknown): vscode.LanguageModelResponsePart[] { @@ -1021,7 +1167,7 @@ class AnthropicResponseExtractor { } this.pendingToolCalls.set(index, pending); } else if (contentBlock && contentBlock.type === "thinking" && typeof contentBlock.thinking === "string") { - this.reasoningContent += contentBlock.thinking; + this.handleReasoning(contentBlock.thinking); } else if (contentBlock && typeof contentBlock.text === "string" && contentBlock.text.length > 0) { const { visible, thinking } = this.filterText(contentBlock.text); if (visible) { @@ -1029,7 +1175,7 @@ class AnthropicResponseExtractor { parts.push(new vscode.LanguageModelTextPart(visible)); } if (thinking) { - this.reasoningContent += thinking; + this.handleReasoning(thinking); } } @@ -1048,10 +1194,10 @@ class AnthropicResponseExtractor { parts.push(new vscode.LanguageModelTextPart(visible)); } if (thinking) { - this.reasoningContent += thinking; + this.handleReasoning(thinking); } } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string" && delta.thinking.length > 0) { - this.reasoningContent += delta.thinking; + this.handleReasoning(delta.thinking); } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") { const index = typeof data.index === "number" ? data.index : this.pendingToolCalls.size - 1; const pending = this.pendingToolCalls.get(index) ?? { @@ -1098,18 +1244,18 @@ class AnthropicResponseExtractor { parts.push(new vscode.LanguageModelTextPart(visible)); } if (thinking) { - this.reasoningContent += thinking; + this.handleReasoning(thinking); } } if (typeof delta.thinking === "string" && delta.thinking.length > 0) { - this.reasoningContent += delta.thinking; + this.handleReasoning(delta.thinking); } if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { - this.reasoningContent += delta.reasoning_content; + this.handleReasoning(delta.reasoning_content); } if (typeof delta.reasoning === "string" && delta.reasoning.length > 0) { - this.reasoningContent += delta.reasoning; + this.handleReasoning(delta.reasoning); } if (typeof delta.type === "string") { @@ -1173,7 +1319,8 @@ class AnthropicResponseExtractor { ); } if (thinking) { - this.reasoningContent += thinking; + // Surface remaining think-filter carry through the thinking part channel. + this.handleReasoning(thinking); } } @@ -1181,6 +1328,17 @@ class AnthropicResponseExtractor { if (!reasoning) { return; } + // If the thinking part API is available, reasoning was already streamed + // live during extractStreamParts via handleReasoning(). The accumulated + // reasoningContent is retained only for tool-call replication + // (flushToolCalls β†’ onReasoningContent). Nothing more to emit here. + if (thinkingPartConstructor) { + this.reasoningContent = ""; + return; + } + // Legacy fallback (API unavailable): emit reasoning as plain text only + // when the response is otherwise empty, to avoid breaking the visible + // output. This preserves the pre-fix safety-net semantics. if (this.emittedTextLength > 0 || this.emittedToolCallsCount > 0) { this.reasoningContent = ""; return; @@ -1243,7 +1401,14 @@ function extractChatCompletionParts( } else { const reasoning = extractReasoningFromDelta(message); if (reasoning.trim()) { - parts.push(new vscode.LanguageModelTextPart(reasoning)); + // Non-stream path: emit reasoning via thinking part when the API is + // available (so chat.agent.thinkingStyle applies), else fall back to + // plain text. Cast needed because LanguageModelThinkingPart is in the + // LanguageModelResponsePart2 union, not the stable LanguageModelResponsePart. + const thinkingPart = thinkingPartConstructor + ? (new thinkingPartConstructor(reasoning) as unknown as vscode.LanguageModelResponsePart) + : new vscode.LanguageModelTextPart(reasoning); + parts.push(thinkingPart); } } for (const toolCallPart of toolCallPartsFromOpenAiMessage( @@ -1319,6 +1484,7 @@ function extractAnthropicParts(data: unknown): vscode.LanguageModelResponsePart[ const parts: vscode.LanguageModelResponsePart[] = []; const textParts: string[] = []; + const reasoningParts: string[] = []; for (const block of data.content) { if (!isRecord(block)) { @@ -1330,6 +1496,16 @@ function extractAnthropicParts(data: unknown): vscode.LanguageModelResponsePart[ continue; } + // Anthropic thinking blocks β€” surface via thinking part when available. + if ( + (block.type === "thinking" || block.type === "redacted_thinking") && + typeof block.thinking === "string" && + block.thinking.length > 0 + ) { + reasoningParts.push(block.thinking); + continue; + } + if (block.type === "tool_use" && typeof block.name === "string") { const id = typeof block.id === "string" ? block.id : `opencodego-tool-${Date.now()}`; const input = isRecord(block.input) ? block.input : parseToolInput(typeof block.input === "string" ? block.input : "{}"); @@ -1342,6 +1518,15 @@ function extractAnthropicParts(data: unknown): vscode.LanguageModelResponsePart[ parts.unshift(new vscode.LanguageModelTextPart(text)); } + // Emit accumulated reasoning via thinking part (or text fallback) at the front. + const reasoning = reasoningParts.join(""); + if (reasoning) { + const thinkingPart = thinkingPartConstructor + ? (new thinkingPartConstructor(reasoning) as unknown as vscode.LanguageModelResponsePart) + : new vscode.LanguageModelTextPart(reasoning); + parts.unshift(thinkingPart); + } + return parts; } diff --git a/src/vscode.proposed.languageModelThinkingPart.d.ts b/src/vscode.proposed.languageModelThinkingPart.d.ts new file mode 100644 index 0000000..b1189b4 --- /dev/null +++ b/src/vscode.proposed.languageModelThinkingPart.d.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------- + + * Type augmentation for the proposed `languageModelThinkingPart` API. + * + * CONTRACT: + * - `LanguageModelThinkingPart` is a proposed VS Code API (as of VS Code 1.102+, + * shipped August 2025 via PR microsoft/vscode#259939). It is NOT in stable + * `vscode.d.ts` yet, but IS available at runtime in all VS Code versions our + * extension targets (`engines.vscode: ^1.125.0`). + * - This file provides the compile-time declaration so `src/*.ts` can reference + * the class. At runtime, ALWAYS guard with + * `typeof vscode.LanguageModelThinkingPart === 'function'` before + * instantiating, to gracefully degrade on hypothetical older hosts. + * - No `enabledApiProposals` entry in package.json is required: the proposal + * is activated implicitly via our existing `onLanguageModelChatProvider:*` + * activation events (same pattern as `chatProvider`). + * - Source: copied from VS Code repo + * `src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts` (main, Jul 2026). + * - Can be removed once the API graduates to stable `vscode.d.ts`. + */ + +// version: 1 + +declare module 'vscode' { + + /** + * A language model response part containing thinking/reasoning content. + * Thinking tokens represent the model's internal reasoning process that + * typically streams before the final response. + */ + export class LanguageModelThinkingPart { + /** + * The thinking/reasoning text content. + */ + value: string | string[]; + + /** + * Optional unique identifier for this thinking sequence. + * This ID is typically provided at the end of the thinking stream + * and can be used for retrieval or reference purposes. + */ + id?: string; + + /** + * Optional metadata associated with this thinking sequence. + */ + metadata?: { readonly [key: string]: any }; + + /** + * Construct a thinking part with the given content. + * @param value The thinking text content. + * @param id Optional unique identifier for this thinking sequence. + * @param metadata Optional metadata associated with this thinking sequence. + */ + constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }); + } +}