diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ad2b9fa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,10 @@
+# Normalize every text file to LF in the index and on checkout. This keeps
+# `npm run lint` (prettier + shellcheck, which require LF per .editorconfig)
+# green on Windows, where core.autocrlf=true would otherwise check files out
+# as CRLF. Binary files are untouched (text=auto detects them).
+* text=auto eol=lf
+
+# Shell scripts must stay LF: husky hooks (checked by shellcheck via
+# `npm run lint`) and any POSIX scripts break with CRLF on Windows checkouts.
+.husky/* text eol=lf
+*.sh text eol=lf
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2b81b9..d1270d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,28 @@
All notable changes to the **OpenCode Go BYOK Provider** extension are documented here.
+## [Unreleased]
+
+### Changed
+
+- **`[Internal]` API keys are configured through the BYOK panel only.** The `OpenCode Go: Set API Key` / `OpenCode Zen: Set API Key` commands and the "Set / Clear API Key" menu items inside `Manage Provider` are removed — keys are entered once via **Chat: Manage Language Models → "+ Add Models"** (the native BYOK flow). `SecretStorage` is no longer a user-facing entry point; it stays as an internal per-vendor mirror (`opencodego.apiKey` / `opencodezen.apiKey`) that the BYOK resolution writes so agent-host variants and cold-start requests inherit the group key. Splitting the secret per vendor also fixes a latent collision where Go and Zen shared a single `opencodego.apiKey` and overwrote each other's key. `Refresh Models` / `Test Connection` now point at the BYOK flow when no key is configured instead of prompting for one.
+
+- **`[Internal]` Per-provider Thinking strategy classes + single config authority.** The thinking/reasoning system is refactored from one monolithic builder into a per-provider strategy (`src/thinking/`): an interface + factory (`provider.ts`), a shared base class, and one class per model family (`deepseek`, `glm`, `kimi`, `minimax`, `openai`, `qwen`, `mimo`, `fallback`). Each provider now owns its reasoning picker schema, its request-payload mapping, and whether its `reasoning_content` is surfaced as chat content. Configuration resolves from a **single authority** — the VS Code per-model configuration (model picker / Manage), with workspace settings and per-family defaults as fallbacks — instead of competing sources (workspace + modelConfiguration + a `globalState` shadow copy + defaults). The shadow copy is removed, so a thinking effort chosen for one model can no longer silently leak onto another model or override an explicit "Off". Model IDs are normalized to `effectiveModelId` (the `::sk-***` fp suffix is gone), which also stops the per-model settings group from being recreated on every pick. Request builders are split out of `extension.ts` into per-endpoint modules (`src/request/{types,schema,shared,openai,anthropic,google}.ts`). Windows tooling fixes: `scripts/lint.ts` runs npm `.cmd` shims through the shell and a new `.gitattributes` enforces LF normalization, so `npm run lint` (prettier + shellcheck) is green on Windows; `scripts/staged-lint.ts` and `isCwdInWorkspace` get the same treatment.
+
+- **`[Internal]` God files split into domain modules (no behavior change).** The three monolithic files — `src/extension.ts` (4653 lines), `src/streaming.ts` (1620) and `src/goUsageTracker.ts` (1510) — are split into domain folders:
+ - `src/usage/` — Go usage domain: `tracker.ts`, `history.ts` (OpenCode CLI SQLite read/aggregation), `pricing.ts`, `formatting.ts`, `dashboard.ts` (status bar + usage webview incl. the HTML template + tooltip SVG), plus the moved `usage.ts` / `usageProfile.ts` / `goUsageSync.ts`.
+ - `src/transports/` — one file per transport (`chatCompletions`, `responses`, `anthropic`, `google`) plus the shared streaming `engine`, pure `sse` parser, `extractors`, `extract` helpers and `thinkTags` filter; the transport contract types live in `src/core/transport.ts` (routing in `src/core/routing.ts`).
+ - `src/provider/` — `OpenCodeProvider` class, `definitions` (PROVIDERS table + model types), `messages`/`tokens` (message conversion + token estimation), `settings` (schema/getSettings/limits/capabilities), `visionProxy`.
+ - `src/models/` — `metadata`, `modelLimits`, `modelCapabilities`, `modelNames`, `pricing`, `metadataFetcher` (models.dev cache).
+ - `src/commands/` — provider, agent-window, diagnostics and thinking-picker command handlers; `src/request/headers.ts` for the OpenCode request headers.
+ - `extension.ts` is now a thin entry (~400 lines) that only wires activation + command registration. The two compat barrels (`streaming.ts`, `goUsageTracker.ts`) are removed and every importer references canonical paths. All behavior-preserving — verified by `npm run compile` + 291 unit tests + mock-server retry E2E (`npm run test-retry`) + `npm run lint`.
+
+- **`[Internal]` Data-driven model registry (`src/core/registry.ts`).** The transport router and the thinking-family detector previously each owned a hardcoded model-prefix table. Both now read ONE data-driven table: `MODEL_REGISTRY` rows map model-family patterns → `{ endpointKind, sdkPackage, thinkingFamily, vendors? }`. `resolveModelRouting()` honors per-vendor restrictions (e.g. MiniMax `m2.x` → Messages on Go, Gemini → Google on Zen); `thinkingFamily()` reads the same table vendor-agnostically. Adding a new model family = adding one row (+ optionally a thinking strategy class). Context limits / capabilities stay metadata-driven (live models.dev) rather than duplicated in a static table. Behavior-preserving — verified by 14 new registry tests (305 total).
+
+### Fixed
+
+- **DeepSeek / Mimo thinking content no longer leaks into the chat transcript.** `treatReasoningAsContent` was mis-detecting native-reasoning families as "no reasoning in body" and echoing their `reasoning_content` as plain chat text. The decision now comes from the provider strategy (always `false` for DeepSeek and Mimo), so chain-of-thought stays in the thinking panel.
+
## [0.6.0] — 2026-08-13
### Added
diff --git a/README.md b/README.md
index 486b8a3..acae08a 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@
| 🎯 **Smart routing** | Each model family auto-routes to its native transport (`/responses`, `/messages`, `streamGenerateContent`, `/chat/completions`) |
| 🖼️ **Vision + PDF + Audio** | Multimodal models pass through image, PDF, audio, and video inputs. Oversized images auto-resize to 2000×2000 / 5MB to match the gateway contract. |
| 📐 **Context-size picker** | Kimi K3 and other tiered-context models expose `256K` vs full-window selection in the per-model configuration, with the cheaper tier selected by default. |
-| 🔒 **Your key, your control** | API key stored in VS Code SecretStorage — never leaves your machine |
+| 🔒 **Your key, your control** | API key entered once in Language Models → **Add Models…** — stored by VS Code, never leaves your machine |
---
@@ -69,7 +69,7 @@
5. **Click the model picker** (current model name) → **Add Models…**
6. **Select** **OpenCode Go** or **OpenCode Zen**.
7. **Press Enter** to accept the default group name.
-8. **Paste your API key** when prompted (stored securely in VS Code SecretStorage).
+8. **Paste your API key** when prompted (stored by VS Code in your language-models configuration).
9. **Pick the models** you want enabled.
10. **Select any OpenCode model** from the picker and start chatting. 🚀
@@ -421,11 +421,10 @@ The easiest way to manage your key is **Settings → Language Models** (gear ⚙
| Command | Description |
| --------------------------------------------------------- | --------------------------------------------------------------- |
-| `OpenCode Go: Manage Provider` | Manage legacy API key, refresh models, test connection |
-| `OpenCode Go: Set API Key` | Store/update legacy OpenCode Go API key |
+| `OpenCode Go: Manage Provider` | Test connection, refresh models, configure utility models |
| `OpenCode Go: Refresh Models` | Force a fresh model-list fetch (bypasses the Manage menu) |
| `OpenCode Go: Diagnostics` | Report of Go models + request history |
-| `OpenCode Zen: Manage Provider` | Manage Zen API key, refresh models, test connection |
+| `OpenCode Zen: Manage Provider` | Test connection, refresh models, configure utility models |
| `OpenCode Zen: Refresh Models` | Force a fresh Zen model-list fetch (bypasses the Manage menu) |
| `OpenCode Zen: Diagnostics` | Report of Zen models + request history |
| `OpenCode: Model Picker Diagnostics` | All registered models (Go + Zen + Copilot) side-by-side |
@@ -474,7 +473,7 @@ Inline suggestions, next-edit suggestions, semantic search, and embedding-backed
Where is my API key stored?
-In VS Code's **SecretStorage** — the same encrypted store used by GitHub auth. It never leaves your machine and is never sent anywhere except directly to `opencode.ai`.
+In your VS Code **language-models configuration** — add it via **Chat: Manage Language Models → Add Models… → OpenCode Go / OpenCode Zen**. VS Code stores the key in its encrypted language-models storage, it never leaves your machine, and it is only sent to `opencode.ai`.
diff --git a/docs/architecture/01-20260514-open-code-provider-architecture.md b/docs/architecture/01-20260514-open-code-provider-architecture.md
index f77b14b..19c6877 100644
--- a/docs/architecture/01-20260514-open-code-provider-architecture.md
+++ b/docs/architecture/01-20260514-open-code-provider-architecture.md
@@ -97,8 +97,7 @@ The native provider configuration schema is declared in `package.json` under `co
| Command | Purpose |
| --------------------------------------------------------- | -------------------------------------------------------------------------- |
-| `OpenCode Go: Manage Provider` | Legacy fallback key management, refresh, and connection test |
-| `OpenCode Go: Set API Key` | Legacy fallback key storage |
+| `OpenCode Go: Manage Provider` | Refresh models, test connection, configure utility models |
| `OpenCode Go: Remove/Re-add Provider in Language Models` | Toggle `opencodego.enabled` (remove/re-add the provider, requires reload) |
| `OpenCode Zen: Remove/Re-add Provider in Language Models` | Toggle `opencodezen.enabled` (remove/re-add the provider, requires reload) |
| `OpenCode Go: Diagnostics` | Go model and transport diagnostics |
@@ -106,7 +105,7 @@ The native provider configuration schema is declared in `package.json` under `co
| `OpenCode: Model Picker Diagnostics` | Cross-provider model metadata comparison |
| `OpenCode: Set Thinking Effort...` | Global thinking-mode helper for supported families |
-The recommended setup path is still VS Code's native **Language Models** UI. The legacy commands remain for diagnostics and fallback compatibility.
+The recommended — and only — setup path is VS Code's native **Language Models** UI ("+ Add Models"). The `Set API Key` command and the legacy key-management menu items were removed (the old single `opencodego.apiKey` secret could not represent both Go and Zen keys); the remaining manage commands cover refresh, connection testing, and diagnostics.
---
@@ -119,14 +118,14 @@ For model discovery (`provideLanguageModelChatInformation`), the extension resol
1. Read `options.configuration.apiKey` (the native BYOK value) if VS Code supplied one.
2. If step 1 produced nothing, fall back to `SecretStorage` unconditionally.
-The unconditional fallback (since 0.5.0, [#86](https://github.com/ltmoerdani/opencode-copilot-chat/issues/86)) covers users who stored the key via the extension command `OpenCode Go: Set API Key` instead of the native BYOK flow. It mirrors Copilot's own `AbstractLanguageModelChatProvider`, which always falls back to its own storage when `configuration.apiKey` is absent. A per-vendor flag (`hasConfiguredByokGroup`) suppresses the groupless call once a native BYOK group exists, so models are not listed twice ([#106](https://github.com/ltmoerdani/opencode-copilot-chat/issues/106)). A group call whose `configuration` is present but carries no API key is treated as a **per-model configuration group** (only `settings`, created when the user picks e.g. `reasoningEffort` in the model picker) and returns no models, so the groupless call remains the single source; per-model settings still apply at request time via `modelConfiguration` ([#131](https://github.com/ltmoerdani/opencode-copilot-chat/issues/131)).
+The fallback is an internal mirror of Copilot's own `AbstractLanguageModelChatProvider`, which always falls back to its own storage when `configuration.apiKey` is absent. Since the `Set API Key` command was removed, the only writer is the BYOK group resolution itself: when a non-agent provider resolves a key it persists it into its **per-vendor** secret (`opencodego.apiKey` / `opencodezen.apiKey`, resolved via `secretKeyFor()` in `src/config.ts`), so agent-host variants and cold-start requests inherit it. A per-vendor flag (`hasConfiguredByokGroup`) suppresses the groupless call once a native BYOK group exists, so models are not listed twice ([#106](https://github.com/ltmoerdani/opencode-copilot-chat/issues/106)). A group call whose `configuration` is present but carries no API key is treated as a **per-model configuration group** (only `settings`, created when the user picks e.g. `reasoningEffort` in the model picker) and returns no models, so the groupless call remains the single source; per-model settings still apply at request time via `modelConfiguration` ([#131](https://github.com/ltmoerdani/opencode-copilot-chat/issues/131)).
Security rules:
- Real API keys are never written to repository files.
- Documentation must use placeholders only.
-- API keys should be entered through VS Code's native secret-backed provider configuration.
-- Legacy `SecretStorage` support remains only as a fallback path.
+- API keys are entered through VS Code's native secret-backed provider configuration ("+ Add Models"); there is no command-palette key entry.
+- `SecretStorage` remains only as an internal fallback (per-vendor, mirroring the BYOK group key for agent variants and cold-start requests).
Safe placeholder example:
diff --git a/docs/architecture/02-20260809-provider-adapter-architecture.md b/docs/architecture/02-20260809-provider-adapter-architecture.md
index a2fa17a..f3215b9 100644
--- a/docs/architecture/02-20260809-provider-adapter-architecture.md
+++ b/docs/architecture/02-20260809-provider-adapter-architecture.md
@@ -226,9 +226,11 @@ Each phase gate: `npm run compile` must pass + a targeted test with at least 1 m
## Timeline
-| Date | Status | Change |
-| ---------- | --------- | ----------------------------------------------------------------------------- |
-| 2026-08-09 | 🟢 Active | Initial research + analysis document created. Proposal only; no code changed. |
+| Date | Status | Change |
+| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 2026-08-09 | 🟢 Active | Initial research + analysis document created. Proposal only; no code changed. |
+| 2026-08-14 | ✅ Done | God-file split executed on `refactor/split-god-files` (usage/ · transports/ · provider/ · models/ · core/ · commands/); `extension.ts` 4653 → ~414 lines. See CHANGELOG `[Unreleased]`. |
+| 2026-08-14 | ✅ Done | **Data-driven registry implemented** (`src/core/registry.ts`). `resolveModelRouting()` (transport) and `thinkingFamily()` both read the `MODEL_REGISTRY` table; `ModelEndpointKind` type moved to the registry. Scope note: context limits/capabilities stay metadata-driven (live models.dev) — not duplicated as a static table. The full `ModelTransport` port interface remains future work. |
---
diff --git a/docs/devlog.md b/docs/devlog.md
index 6650062..5440b52 100644
--- a/docs/devlog.md
+++ b/docs/devlog.md
@@ -1,6 +1,62 @@
# 🧠 OPENCODE COPILOT CHAT DEVLOG
-**Branch:** `main` | **Updated:** 2026-08-13 Asia/Jakarta | **Current Phase:** Autocomplete (#49) + central-config/usage refactor (#138) merged; PR #133/#135/#136/#138 all landed. `main` HEAD `616d6f6`.
+**Branch:** `refactor/split-god-files` | **Updated:** 2026-08-14 Asia/Jakarta | **Current Phase:** data-driven model registry done (13 commits on `refactor/split-god-files`); prior: god-file split complete.
+
+---
+
+## ✅ Data-driven model registry — 2026-08-14
+
+**Action:** Implemented `src/core/registry.ts` — the single data-driven source of truth for per-model wiring (architecture doc 02, item 3).
+
+**What:**
+
+- `MODEL_REGISTRY`: family rows → `{ patterns, endpointKind, sdkPackage, thinkingFamily, vendors? }` (first match wins; vendor restrictions honored).
+- `core/routing.ts` `resolveModelRouting()` now reads the registry (was an if-chain); `thinking/provider.ts` `thinkingFamily()` reads the same table vendor-agnostically (was a second hardcoded prefix table). `ModelEndpointKind` type moved to the registry, re-exported by `provider/definitions.ts`.
+- Adding a model family = one row (+ optionally a thinking strategy class). Context limits/capabilities stay metadata-driven (live models.dev).
+- New `src/test/registry.test.ts` (14 tests): transport routing × vendors, thinking family, lookup mechanics. Total **305 tests**.
+
+**Verification:** compile + 305 tests + `npm run test-retry` 7/7 + lint green. CHANGELOG `[Unreleased]` + architecture doc 02 timeline updated.
+
+---
+
+## ✅ God-file split — usage/ + transports/ + provider/ + models/ + commands/ — 2026-08-14
+
+**Branch:** `refactor/split-god-files` (12 commits ahead of `main`, PR pending)
+
+**Action:** Split the three god files into domain modules per `docs/architecture/02-20260809-provider-adapter-architecture.md`, pure mechanical (cut-paste, zero behavior change). Phase-gated: each commit passes `npm run compile` + `npm test` (291) + `npm run lint`.
+
+**What:**
+
+1. **`src/usage/`** (from `goUsageTracker.ts` 1510 → split): `tracker.ts` (class + types + time helpers), `history.ts` (OpenCode CLI SQLite read/aggregation, pure), `pricing.ts` (bundled cost snapshot + `estimateCost`, pure), `formatting.ts` (status-bar/quick-pick), `dashboard.ts` (usage state + status bar + webview incl. the 583-line HTML template + tooltip SVG). Moved `usage.ts` / `usageProfile.ts` / `goUsageSync.ts` in.
+2. **`src/transports/`** (from `streaming.ts` 1620 → split): one entry per transport (`chatCompletions` / `responses` / `anthropic` / `google`) + shared `engine` (HTTP/SSE + retry/backoff), `sse` (pure parser), `extractors` (Base/OpenAi/Anthropic), `extract` (non-stream + pure helpers), `streamParts`, `thinkTags` (pure). Contract types → `src/core/transport.ts`.
+3. **`src/models/` + `src/core/`**: moved `metadata`/`modelLimits`/`modelCapabilities`/`modelNames` → `models/`, `routing.ts` → `core/routing.ts`, new `models/metadataFetcher.ts` (models.dev cache) + `models/pricing.ts`.
+4. **`src/provider/`** (from `extension.ts`): `OpenCodeProvider` class (moved whole, one cohesive concern), `definitions` (PROVIDERS + model types + user-agent/transient-fetch helpers), `messages`/`tokens` (convertMessage + token estimation), `settings` (schema/getSettings/limits/capabilities/rawModelId/visionProxyEnabled), `visionProxy`.
+5. **`src/commands/`**: `providers.ts`, `agentsWindow.ts`, `diagnostics.ts`, `thinkingPicker.ts` — command handlers moved out of `extension.ts`.
+6. **Thin entry**: `extension.ts` is ~414 lines (was 4653) — only `activate` wiring + `deactivate`. Both compat barrels (`streaming.ts`, `goUsageTracker.ts`) deleted; all importers reference canonical paths.
+
+**Verification:** `npm run compile` clean; `npm test` 291/291; `npm run test-retry` 7/7 (mock server); `npm run lint` fully green on Windows. CHANGELOG `[Unreleased]` updated.
+
+**Notes:** env quirks hit during the run — user's global gitignore ignores `.vscode`, so lint-staged fails whenever `.vscode/settings.json` is staged (commit with targeted `git add`, never `git add -A`); PowerShell `WriteAllLines` writes CRLF, so always `prettier --write` after PowerShell line-range edits.
+
+---
+
+## ✅ Thinking refactor + per-request modules + Windows lint fixes — 2026-08-13
+
+**Branch:** `refactor/thinking-request-modules` (6 commits ahead of `main`, PR drafted)
+
+**Action:** Refactored the thinking/reasoning system and split the monolithic `extension.ts` request path, per user request to align with VS Code extension standards.
+
+**What:**
+
+1. **Per-provider Thinking strategies** (`src/thinking/`, commit `400861c`). One strategy class per model family (`deepseek` / `glm` / `kimi` / `minimax` / `openai` / `qwen` / `mimo` / `fallback`) behind a shared interface + factory. Each owns its picker schema, request-payload mapping, and `treatReasoningAsContent`. Pure modules (no `vscode` import) keep unit-testability in plain Node.
+2. **Single config authority** (`resolve.ts`). VS Code per-model configuration wins (as VS Code itself designs), then workspace settings, then per-family defaults. Removed the `globalState` shadow copy of thinking overrides — root cause of (a) "Max" being treated as "Off" (fp-suffixed model IDs never matched the per-model config group) and (b) "Off" being silently overridden by a shadow "max". Model IDs normalized to `effectiveModelId` (no `::sk-***` fp suffix), which also stops the per-model settings group from being recreated on every pick (related to #131 / PR #135).
+3. **CoT leak fix.** `treatReasoningAsContent` now comes from the provider strategy — always `false` for DeepSeek and Mimo — so native-reasoning models keep chain-of-thought in the thinking panel instead of echoing it into the chat transcript. Upstream gateway bug (#37635) deliberately not worked around for Mimo (user decision).
+4. **Request module split** (commits `ad1df51` + `e63b757`). Body builders + message/tool conversions moved out of `extension.ts` (~640 lines) into `src/request/{types,schema,shared,openai,anthropic,google}.ts`.
+5. **Windows fixes** (commits `679a851`, `21e8393`, `ec5b27f`). `staged-lint.ts` and `lint.ts` run npm `.cmd` shims through the shell (fixes ENOENT on Windows); `isCwdInWorkspace` matches both path separators; new `.gitattributes` enforces LF normalization so prettier + shellcheck pass on Windows.
+
+**Verification:** `npm run compile` clean; `npm test` **290/290**; `npm run lint` fully green on Windows (was silently ENOENT-failing before); `npm run package` produced `opencode-copilot-chat-0.6.0.vsix`.
+
+**Docs:** CHANGELOG `[Unreleased]` updated. Live testing with a real API key not done; `npm run test-retry` (mock server) is available.
---
@@ -451,13 +507,13 @@
## ⚡ Session Handoff
-| Field | Value |
-| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Last Session** | 2026-07-23 (Session 4 — stabilization) |
-| **Worked On** | Final stabilization of #36 fix. After iteration 3 (suffix-repetition detection), user reported: (a) `treatReasoningAsContent` was leaking thinking to visible text, (b) `contentAfterReasoning` guard was suppressing ALL models, (c) thinking/reasoning was being "cut off" and stopping without warning. Through 4 additional iterations: (1) removed `treatReasoningAsContent` to fix leak → thinking went back to thinking panel ✅, (2) reverted `contentAfterReasoning` and `shouldSuppressTextEmit` which were false-positiving on DeepSeek/GLM/Kimi (they legitimately use `reasoning_content` then `content`), (3) re-added `treatReasoningAsContent` with correct condition: only when Go gateway + NO `reasoning_effort` in body. Key insight from web research: upstream issue #37635 is confirmed (gateway bug) and PR #37558 merged `reasoning_content` parsing — but the gateway bug itself persists. |
-| **Stopped At** | All fixes verified working. Documentation updated. Ready to push. |
-| **Next Action** | → Commit all changes → push `fix/mimo-thinking-budget` branch → open PR. |
-| **Open Issues** | (1)-(9) same as before. (10) Log spam from agent-host provider still high (#36 debugging showed it). (11) Upstream #37635 still open, workaround can be removed when fixed server-side. (12) #98 premature tool-call flush regression — fix implemented, pending compile/test. |
+| Field | Value |
+| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Last Session** | 2026-08-13 (thinking refactor + request split, branch `refactor/thinking-request-modules`) |
+| **Worked On** | Diagnosed the "Max thinking treated as Off" bug: four competing config sources (workspace settings, VS Code per-model config, a `globalState` shadow copy, defaults) plus fp-suffixed model IDs meant the per-model config never reached the request path, and `treatReasoningAsContent` mis-echoed CoT for native-reasoning families. Refactored per user direction into per-provider Thinking strategy classes (`src/thinking/`), made VS Code per-model config the single authority, removed the shadow state and `fpEffectiveModelId`, and made the CoT-surfacing decision per provider (DeepSeek/Mimo always `false`). Split the request path out of `extension.ts` into `src/request/` (openai/anthropic/google builders). Fixed `npm run lint` on Windows: `lint.ts`/`staged-lint.ts` shell-shim fix + `.gitattributes` LF normalization (prettier/shellcheck were failing on CRLF checkouts). |
+| **Stopped At** | 6 commits on the branch, all green: compile ✅, 290/290 tests ✅, full `npm run lint` ✅, VSIX packages ✅. CHANGELOG `[Unreleased]` + devlog updated. PR body drafted, branch not yet pushed. |
+| **Next Action** | → Push branch → open PR (template applied) → optionally run `npm run test-retry` live mock E2E. Remaining architecture candidates (user-approved direction): `src/request/headers.ts`, `OpenCodeProvider` class → `src/providers/`, usage webview HTML → own file, `commands.ts`, metadata cache module. |
+| **Open Issues** | (1) #131 duplicate-model group / PR #135 — our `effectiveModelId` normalization is related; confirm interaction. (2) Upstream #37635 gateway CoT bug still open (Mimo intentionally not worked around). (3) #98/#36 remain closed/verified. (4) Live API validation (`npm run validate-models`) not run — requires `OPENCODE_API_KEY`. (5) VSIX currently includes local `AGENTS.md` + `.codegraph/` (not in `.vscodeignore`); minor, follow-up. |
---
@@ -1850,41 +1906,42 @@ rg -n "sk-[A-Za-z0-9]|apiKey.*[A-Za-z0-9]{20,}|Authorization: Bearer [A-Za-z0-9]
## 📋 Completed History
-| Date | Version | Summary |
-| ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 2026-06-13 | docs | Deep audit — all 4 🟢 Active docs verified against codebase + git history + CHANGELOG. All marked ✅ Solved: issue #19 (PR #15 merged), references #01 (research complete), architecture #01 (living ref complete), issue #01 (all code fixed v0.1.9/v0.1.10, remaining tool-call loop is model behavior not code bug). 0 Active docs remain. |
-| 2026-06-13 | docs | Rewrote devlog into work-context format and flagged unrelated `WORK-CONTEXT.md` content |
-| 2026-06-13 | docs | Backdated and consolidated the 2026-05-15 Qwen 3.6 Plus Free tool-call loop investigation |
-| 2026-06-12 | docs | Added provider architecture reference for Go/Zen BYOK setup and later routing/metadata/usage evolution |
-| 2026-06-12 | v0.2.7 | Temperature support fix, Kimi thinking format correction |
-| 2026-06-11 | research | Agents window model visibility — GitHub Issue #11 deep-dive. Investigated VS Code source (`targetChatSessionType`, `chatSessions`, `chatSessionsProvider` proposed API). Concluded Option A (duplicate models with `targetChatSessionType: 'copilotcli'`) is only marketplace-compatible path. Custom "OpenCode" tab blocked by `vsce` proposed API policy. Doc: `docs/references/01-20260611-agents-window-model-visibility.md` |
-| 2026-06-10 | v0.2.6 | Removed message trimming + gzip |
-| 2026-06-10 | v0.2.5 | Removed gzip HTTP 500 path |
-| 2026-06-10 | v0.2.4 | Context size selector, dynamic reasoning, thinking controls, strip think tags |
-| 2026-06-09 | cleanup | Project cleanup — full codebase review (20+ improvements across 5 categories), fixed 4 immediate bugs: redundant activationEvents, stale user-agent version, duplicate CHANGELOG, .vsix gitignore. Doc: `docs/issues/18-20260609-project-cleanup-immediate-bugfixes.md` |
-| 2026-06-09 | v0.2.3 | Output channel cleanup — removed all verbose debug/informational logs from "OpenCode" channel, fixed `Buffer` TS error with `TextDecoder` Web API, refreshed extension icon (commit `c8383735`), version bumped to 0.2.3. Doc: `docs/issues/16-20260609-output-channel-cleanup-textdecoder-fix.md` |
-| 2026-06-08 | v0.2.2 | Strip think tags from model output |
-| 2026-06-08 | icon | Extension icon redesign — replaced generic `>` bracket logo with creative OpenCode Mark design (gradients, glow, grid pattern, sparkle accents). Researched brand assets from `anomalyco/opencode` source. Doc: `docs/features/04-20260608-extension-icon-redesign.md` |
-| 2026-06-06 | v0.2.1 | Removed unused Go usage panel/command |
-| 2026-06-05 | usage-debug | Go Usage Tracker status bar not updating — REST API exhaustive search (all 404), CLI dependency removal, session.percent bug fix, debug output channel, temporary v0.2.1 test VSIX. Doc: `docs/issues/14-20260605-go-usage-status-bar-not-updating.md` |
-| 2026-06-05 | v0.2.0 | Go Usage Tracker feature implementation — GitHub user request → OpenCode pricing research → status bar + Quick Pick design → `goUsageTracker.ts` + `extension.ts` → VSIX build. Doc: `docs/features/03-20260605-go-usage-tracker.md` |
-| 2026-06-05 | v0.1.10 | Qwen routing reverted to Anthropic Messages API; Anthropic SSE tool call parsing; Qwen thinking payload |
-| 2026-06-04 | v0.1.8 | PR #7 review/merge/release — languageModelPricing API, models.dev cost data, 4-tier priceCategory, modality detection, type consolidation, experimental config cleanup. Doc: `docs/issues/11-20260604-pr7-pricing-api-review-merge-release.md` |
-| 2026-06-04 | v0.1.9 | Qwen tool calling fixed (routed to chat-completions); context window for Qwen |
-| 2026-06-04 | v0.1.8 | languageModelPricing, modality detection, cost metadata, capabilities alignment |
-| 2026-05-27 | v0.1.7 | Transport diagnostics + Context Window usage integration — added native `usage` DataPart reporting, kept OpenCode custom usage telemetry, restored richer token counting, integrated PR #6, packaged and installed `0.1.7`, and merged `develop` back to `main`. Doc: `docs/issues/10-20260527-context-window-usage-pr6-integration.md` |
-| 2026-05-24 | v0.1.6 | PR #4 review/merge/release — native Zen routing, models.dev cache, modular split, 5 unit tests, vision fixes preserved, marketplace VSIX packaged. Doc: `docs/issues/09-20260524-pr4-review-merge-release.md` |
-| 2026-05-21 | v0.1.6 | models.dev metadata cache, Zen GPT/Gemini routing, timeouts |
-| 2026-05-20 | v0.1.5 | Vision image request fixes and release consolidation — replaced stack-overflow-prone image byte encoding, diagnosed provider-side Alibaba `429 insufficient_quota`, omitted Qwen `thinking_budget` for image requests when Thinking is `auto`, audited OpenCode attachment metadata, removed incorrect `Vision` capability from GLM/MiniMax/MiMo Pro rows, restored the `0.1.5` changelog entry, compiled final output, and merged `develop` into `main` with `--no-ff`. Doc: `docs/issues/08-20260520-vision-image-request-fixes.md` |
-| 2026-05-17 | zen-labels | Zen model version label fix — preserved decimal version labels such as `Claude Opus 4.6`, diagnosed stale installed VSIX artifacts, rebuilt the final `0.1.4` package, and moved `reasoningEffort` changelog wording under Added. Doc: `docs/issues/07-20260517-zen-model-version-labels.md` |
-| 2026-05-17 | thinking-native-submenu | Native Thinking submenu solved — confirmed VS Code configuration pipeline, found diagnostics command was warming provider metadata, added automatic provider metadata warm-up, shortened Copilot-style labels, fixed Kimi/Moonshot tool schema sanitizer, fixed Qwen chat-completions routing and hybrid stream parsing, rebuilt final `0.1.4` VSIX. Doc: `docs/issues/06-20260517-thinking-native-submenu-investigation.md` |
-| 2026-05-17 | thinking | Per-model Thinking controls — documented the feature covering family defaults, `configurationSchema`, `reasoningEffort`, `modelConfiguration`, `models.dev` reasoning options, request payload mapping, and command/settings fallback. Doc: `docs/features/02-20260517-per-model-thinking-controls.md` |
-| 2026-05-17 | PR #1 | First community contribution merged — `opencodego.freeOnly` setting by @Wallacy. Reviewed, tested locally, merged via GitHub UI, synced `develop`. Doc: `docs/issues/04-20260517-pr1-freeonly-review-merge.md` |
-| 2026-05-17 | v0.1.4 | Zen free filtering, thinking controls, schema sanitization, unavailable filtering |
-| 2026-05-16 | v0.1.3 follow-up | Unavailable/deprecated model filtering — hid Ring and Trinity stale IDs, applied `models.dev` deprecated status filtering, and synced model docs |
-| 2026-05-16 | v0.1.3 | Context-size correction and per-provider model limits |
-| 2026-05-15 | investigation | Qwen 3.6 Plus Free tool-call infinite loop — root cause identified. Doc: `docs/issues/01-20260515-qwen36-tool-call-loop.md` |
-| 2026-05-14 | v0.1.0–0.1.2 | Initial Go provider, native BYOK, separate Zen provider |
+| Date | Version | Summary |
+| ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 2026-08-13 | refactor/thinking-request-modules | Thinking refactor (per-provider strategy classes + single VS Code per-model config authority + removed globalState shadow + `effectiveModelId`) + request module split (`src/request/`) + Windows lint fixes (`.cmd` shims + `.gitattributes` LF). 6 commits. CHANGELOG [Unreleased] updated. |
+| 2026-06-13 | docs | Deep audit — all 4 🟢 Active docs verified against codebase + git history + CHANGELOG. All marked ✅ Solved: issue #19 (PR #15 merged), references #01 (research complete), architecture #01 (living ref complete), issue #01 (all code fixed v0.1.9/v0.1.10, remaining tool-call loop is model behavior not code bug). 0 Active docs remain. |
+| 2026-06-13 | docs | Rewrote devlog into work-context format and flagged unrelated `WORK-CONTEXT.md` content |
+| 2026-06-13 | docs | Backdated and consolidated the 2026-05-15 Qwen 3.6 Plus Free tool-call loop investigation |
+| 2026-06-12 | docs | Added provider architecture reference for Go/Zen BYOK setup and later routing/metadata/usage evolution |
+| 2026-06-12 | v0.2.7 | Temperature support fix, Kimi thinking format correction |
+| 2026-06-11 | research | Agents window model visibility — GitHub Issue #11 deep-dive. Investigated VS Code source (`targetChatSessionType`, `chatSessions`, `chatSessionsProvider` proposed API). Concluded Option A (duplicate models with `targetChatSessionType: 'copilotcli'`) is only marketplace-compatible path. Custom "OpenCode" tab blocked by `vsce` proposed API policy. Doc: `docs/references/01-20260611-agents-window-model-visibility.md` |
+| 2026-06-10 | v0.2.6 | Removed message trimming + gzip |
+| 2026-06-10 | v0.2.5 | Removed gzip HTTP 500 path |
+| 2026-06-10 | v0.2.4 | Context size selector, dynamic reasoning, thinking controls, strip think tags |
+| 2026-06-09 | cleanup | Project cleanup — full codebase review (20+ improvements across 5 categories), fixed 4 immediate bugs: redundant activationEvents, stale user-agent version, duplicate CHANGELOG, .vsix gitignore. Doc: `docs/issues/18-20260609-project-cleanup-immediate-bugfixes.md` |
+| 2026-06-09 | v0.2.3 | Output channel cleanup — removed all verbose debug/informational logs from "OpenCode" channel, fixed `Buffer` TS error with `TextDecoder` Web API, refreshed extension icon (commit `c8383735`), version bumped to 0.2.3. Doc: `docs/issues/16-20260609-output-channel-cleanup-textdecoder-fix.md` |
+| 2026-06-08 | v0.2.2 | Strip think tags from model output |
+| 2026-06-08 | icon | Extension icon redesign — replaced generic `>` bracket logo with creative OpenCode Mark design (gradients, glow, grid pattern, sparkle accents). Researched brand assets from `anomalyco/opencode` source. Doc: `docs/features/04-20260608-extension-icon-redesign.md` |
+| 2026-06-06 | v0.2.1 | Removed unused Go usage panel/command |
+| 2026-06-05 | usage-debug | Go Usage Tracker status bar not updating — REST API exhaustive search (all 404), CLI dependency removal, session.percent bug fix, debug output channel, temporary v0.2.1 test VSIX. Doc: `docs/issues/14-20260605-go-usage-status-bar-not-updating.md` |
+| 2026-06-05 | v0.2.0 | Go Usage Tracker feature implementation — GitHub user request → OpenCode pricing research → status bar + Quick Pick design → `goUsageTracker.ts` + `extension.ts` → VSIX build. Doc: `docs/features/03-20260605-go-usage-tracker.md` |
+| 2026-06-05 | v0.1.10 | Qwen routing reverted to Anthropic Messages API; Anthropic SSE tool call parsing; Qwen thinking payload |
+| 2026-06-04 | v0.1.8 | PR #7 review/merge/release — languageModelPricing API, models.dev cost data, 4-tier priceCategory, modality detection, type consolidation, experimental config cleanup. Doc: `docs/issues/11-20260604-pr7-pricing-api-review-merge-release.md` |
+| 2026-06-04 | v0.1.9 | Qwen tool calling fixed (routed to chat-completions); context window for Qwen |
+| 2026-06-04 | v0.1.8 | languageModelPricing, modality detection, cost metadata, capabilities alignment |
+| 2026-05-27 | v0.1.7 | Transport diagnostics + Context Window usage integration — added native `usage` DataPart reporting, kept OpenCode custom usage telemetry, restored richer token counting, integrated PR #6, packaged and installed `0.1.7`, and merged `develop` back to `main`. Doc: `docs/issues/10-20260527-context-window-usage-pr6-integration.md` |
+| 2026-05-24 | v0.1.6 | PR #4 review/merge/release — native Zen routing, models.dev cache, modular split, 5 unit tests, vision fixes preserved, marketplace VSIX packaged. Doc: `docs/issues/09-20260524-pr4-review-merge-release.md` |
+| 2026-05-21 | v0.1.6 | models.dev metadata cache, Zen GPT/Gemini routing, timeouts |
+| 2026-05-20 | v0.1.5 | Vision image request fixes and release consolidation — replaced stack-overflow-prone image byte encoding, diagnosed provider-side Alibaba `429 insufficient_quota`, omitted Qwen `thinking_budget` for image requests when Thinking is `auto`, audited OpenCode attachment metadata, removed incorrect `Vision` capability from GLM/MiniMax/MiMo Pro rows, restored the `0.1.5` changelog entry, compiled final output, and merged `develop` into `main` with `--no-ff`. Doc: `docs/issues/08-20260520-vision-image-request-fixes.md` |
+| 2026-05-17 | zen-labels | Zen model version label fix — preserved decimal version labels such as `Claude Opus 4.6`, diagnosed stale installed VSIX artifacts, rebuilt the final `0.1.4` package, and moved `reasoningEffort` changelog wording under Added. Doc: `docs/issues/07-20260517-zen-model-version-labels.md` |
+| 2026-05-17 | thinking-native-submenu | Native Thinking submenu solved — confirmed VS Code configuration pipeline, found diagnostics command was warming provider metadata, added automatic provider metadata warm-up, shortened Copilot-style labels, fixed Kimi/Moonshot tool schema sanitizer, fixed Qwen chat-completions routing and hybrid stream parsing, rebuilt final `0.1.4` VSIX. Doc: `docs/issues/06-20260517-thinking-native-submenu-investigation.md` |
+| 2026-05-17 | thinking | Per-model Thinking controls — documented the feature covering family defaults, `configurationSchema`, `reasoningEffort`, `modelConfiguration`, `models.dev` reasoning options, request payload mapping, and command/settings fallback. Doc: `docs/features/02-20260517-per-model-thinking-controls.md` |
+| 2026-05-17 | PR #1 | First community contribution merged — `opencodego.freeOnly` setting by @Wallacy. Reviewed, tested locally, merged via GitHub UI, synced `develop`. Doc: `docs/issues/04-20260517-pr1-freeonly-review-merge.md` |
+| 2026-05-17 | v0.1.4 | Zen free filtering, thinking controls, schema sanitization, unavailable filtering |
+| 2026-05-16 | v0.1.3 follow-up | Unavailable/deprecated model filtering — hid Ring and Trinity stale IDs, applied `models.dev` deprecated status filtering, and synced model docs |
+| 2026-05-16 | v0.1.3 | Context-size correction and per-provider model limits |
+| 2026-05-15 | investigation | Qwen 3.6 Plus Free tool-call infinite loop — root cause identified. Doc: `docs/issues/01-20260515-qwen36-tool-call-loop.md` |
+| 2026-05-14 | v0.1.0–0.1.2 | Initial Go provider, native BYOK, separate Zen provider |
---
diff --git a/package.json b/package.json
index 0d4451a..7fe1cc7 100644
--- a/package.json
+++ b/package.json
@@ -66,10 +66,6 @@
"command": "opencodego.manage",
"title": "OpenCode Go: Manage Provider"
},
- {
- "command": "opencodego.setApiKey",
- "title": "OpenCode Go: Set API Key"
- },
{
"command": "opencodego.toggleProvider",
"title": "OpenCode Go: Remove/Re-add Provider in Language Models"
diff --git a/scripts/lint.ts b/scripts/lint.ts
index 10ffe80..4ec1f7e 100644
--- a/scripts/lint.ts
+++ b/scripts/lint.ts
@@ -9,7 +9,12 @@ import pc from "picocolors";
const root = path.resolve(import.meta.dirname, "..");
-const bin = (name: string): string => path.join(root, "node_modules", ".bin", name);
+const bin = (name: string): string => {
+ const base = path.join(root, "node_modules", ".bin", name);
+ // On Windows the npm shims are `.cmd` files and must be spawned through the
+ // shell; spawning the extension-less shim yields ENOENT.
+ return process.platform === "win32" ? `${base}.cmd` : base;
+};
// Strip markdownlint-cli2 banner/summary noise and prettier's status header.
const NOISE = /^(markdownlint-cli2 v|Finding:|Linting:|Summary:|Checking formatting\.\.\.)/;
@@ -45,7 +50,12 @@ const steps: LintStep[] = [
console.log(pc.bold("Lint"));
let failed = false;
for (const step of steps) {
- const res = spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" });
+ const res = spawnSync(step.cmd, step.args, {
+ cwd: root,
+ encoding: "utf8",
+ // Windows cannot exec `.cmd` shims directly; route through the shell.
+ shell: process.platform === "win32",
+ });
const output = clean(`${res.stdout}${res.stderr}`);
if (res.status === 0) {
console.log(` ${pc.green("✔")} ${step.label}`);
diff --git a/scripts/staged-lint.ts b/scripts/staged-lint.ts
index 757c3d1..770146e 100644
--- a/scripts/staged-lint.ts
+++ b/scripts/staged-lint.ts
@@ -23,7 +23,12 @@ import pc from "picocolors";
const root = path.resolve(import.meta.dirname, "..");
-const bin = (name: string): string => path.join(root, "node_modules", ".bin", name);
+const bin = (name: string): string => {
+ const base = path.join(root, "node_modules", ".bin", name);
+ // On Windows the npm shims are `.cmd` files and must be spawned through the
+ // shell; spawning the extension-less shim yields ENOENT.
+ return process.platform === "win32" ? `${base}.cmd` : base;
+};
const SRC_DIRS = ["src", "scripts"];
const TS_EXT = new Set([".ts", ".tsx", ".js", ".cjs", ".cts"]);
@@ -35,7 +40,12 @@ interface CommandResult {
}
function run(cmd: string, args: string[]): CommandResult {
- const res: SpawnSyncReturns = spawnSync(cmd, args, { cwd: root, encoding: "utf8" });
+ const res: SpawnSyncReturns = spawnSync(cmd, args, {
+ cwd: root,
+ encoding: "utf8",
+ // Windows cannot exec `.cmd` shims directly; route through the shell.
+ shell: process.platform === "win32",
+ });
return { status: res.status, output: `${res.stdout}${res.stderr}`.trim() };
}
diff --git a/scripts/validate-models.ts b/scripts/validate-models.ts
index f948dfd..91445e3 100644
--- a/scripts/validate-models.ts
+++ b/scripts/validate-models.ts
@@ -3,7 +3,7 @@
* validate-models.ts — Comprehensive model parameter validation suite.
*
* Reuses the EXACT same logic as the extension:
- * - buildThinkingPayload() from thinking.ts
+ * - buildPayload() from the thinking provider strategy
* - resolveModelRouting() from routing.ts
* - buildOpenCodeGatewayAuthHeaders() from openCodeAuth.ts
*
@@ -16,8 +16,8 @@
*/
import { parseArgs } from "node:util";
-import { buildThinkingPayload, type ThinkingSettings } from "../src/thinking.js";
-import { resolveModelRouting } from "../src/routing.js";
+import { thinkingProviderFor, type ThinkingSettings } from "../src/thinking.js";
+import { resolveModelRouting } from "../src/core/routing.js";
import { buildOpenCodeGatewayAuthHeaders } from "../src/openCodeAuth.js";
// ---------------------------------------------------------------------------
@@ -118,7 +118,7 @@ function detectFamily(id: string): string {
}
// ---------------------------------------------------------------------------
-// Build test parameters using extension's buildThinkingPayload
+// Build test parameters using the extension's thinking provider strategy
// ---------------------------------------------------------------------------
import { THINKING_DEFAULTS } from "../src/config.js";
@@ -218,9 +218,9 @@ async function testParameter(model: ModelInfo, test: ParamTest, apiKey: string):
// Use extension's auth headers
const authHeaders = buildOpenCodeGatewayAuthHeaders(routing.endpointKind, apiKey);
- // Build thinking payload using extension's buildThinkingPayload
+ // Build thinking payload using the extension's thinking provider strategy
const thinking: ThinkingSettings = { ...DEFAULT_SETTINGS, ...test.settings };
- const thinkingPayload = buildThinkingPayload(model.id, thinking, test.hasImageInput);
+ const thinkingPayload = thinkingProviderFor(model.id).buildPayload(thinking, { hasImageInput: test.hasImageInput });
// Build the full request body exactly as the extension would
const body: Record = {
@@ -477,7 +477,7 @@ async function main() {
if (DRY_RUN) {
const summaries = tests.map((t) => {
const thinking: ThinkingSettings = { ...DEFAULT_SETTINGS, ...t.settings };
- const payload = buildThinkingPayload(model.id, thinking, t.hasImageInput);
+ const payload = thinkingProviderFor(model.id).buildPayload(thinking, { hasImageInput: t.hasImageInput });
const fields = Object.keys(payload).filter((k) => k !== "model");
return `${t.name} → ${fields.length > 0 ? JSON.stringify(payload) : "(no thinking params)"}`;
});
diff --git a/scripts/verify-estimate-token-count.ts b/scripts/verify-estimate-token-count.ts
index c24b3ea..e9b608a 100644
--- a/scripts/verify-estimate-token-count.ts
+++ b/scripts/verify-estimate-token-count.ts
@@ -323,7 +323,7 @@ const testCases: TestCase[] = [
// The "new" budget is computed with the production calculateModelLimits so the
// script verifies the real shipped behavior.
-import { calculateModelLimits } from "../src/modelLimits.js";
+import { calculateModelLimits } from "../src/models/modelLimits.js";
function computeMaxTokens(promptTokens: number | undefined, contextWindow: number, maxOutputTokens: number): number {
const limits = calculateModelLimits({ contextWindow, maxOutputTokens }, { maxInputTokens: contextWindow, promptTokens });
diff --git a/src/chatParts.ts b/src/chatParts.ts
index b3c402b..7339e95 100644
--- a/src/chatParts.ts
+++ b/src/chatParts.ts
@@ -1,5 +1,5 @@
import * as vscode from "vscode";
-import { hasUsageSnapshot, toProviderUsagePayload, type UsageSnapshot } from "./usage";
+import { hasUsageSnapshot, toProviderUsagePayload, type UsageSnapshot } from "./usage/usage";
export const OPENCODE_USAGE_DATA_MIME = "application/vnd.opencode.usage+json";
export const COPILOT_USAGE_DATA_MIME = "usage";
diff --git a/src/commands/agentsWindow.ts b/src/commands/agentsWindow.ts
new file mode 100644
index 0000000..516d2cb
--- /dev/null
+++ b/src/commands/agentsWindow.ts
@@ -0,0 +1,130 @@
+import * as vscode from "vscode";
+import {
+ AGENTS_BYOK_BRIDGE_STATE_KEY,
+ AGENT_HOST_BYOK_ENABLED_SETTING,
+ AGENT_HOST_BYOK_MINOR_VERSION,
+ CONFIG_SECTION,
+ EXTENSION_ID,
+ SETTING_AGENTS_WINDOW,
+ SETTING_AUTO_ENABLE_AGENTS_WINDOW,
+ SUPPORT_AGENTS_WINDOW_SETTING,
+ SUPPORT_AGENTS_WINDOW_STATE_KEY,
+} from "../config";
+import { AGENT_GO_VENDOR, AGENT_ZEN_VENDOR, GO_VENDOR, ZEN_VENDOR } from "../providerTypes";
+import { providerEnabledSetting } from "../providerEnablement";
+
+/**
+ * Whether this VS Code has the modern agent-host BYOK bridge (1.129+).
+ *
+ * From VS Code 1.129 the Agents window runs in a separate agent host
+ * process. Extension-provided BYOK models (isBYOK, no `targetChatSessionType`)
+ * are mirrored into agent-host sessions exclusively through the BYOK
+ * language-model bridge, which VS Code keeps OFF by default
+ * (`chat.agentHost.byokModels.enabled`, experimental). On older versions the
+ * extension's own agent-host providers (`targetChatSessionType: "copilotcli"`)
+ * are the only path, which is why they stay registered.
+ */
+function isModernAgentHostVscode(): boolean {
+ const [major = 1, minor = 0] = vscode.version.split(".").map(Number);
+ return major > 1 || (major === 1 && minor >= AGENT_HOST_BYOK_MINOR_VERSION);
+}
+
+/**
+ * Ensure the VS Code core settings that make OpenCode Go/Zen models usable in
+ * the Agents window are enabled (issue #122):
+ *
+ * 1. `extensions.supportAgentsWindow.` — the only way a code extension is
+ * allowed to run in the Agents window (sessions window) process. VS Code
+ * disables any extension with a `main` entry there by default, so without
+ * this setting the extension's `languageModelChatProviders` vendors are
+ * not registered in that window: neither the model picker nor the
+ * "+ Add Models" list can show OpenCode Go/Zen.
+ * 2. `chat.agentHost.byokModels.enabled` (VS Code 1.129+) — the BYOK
+ * language-model bridge that mirrors extension BYOK models into
+ * agent-host sessions. Off by default and experimental.
+ *
+ * CONTRACT:
+ * - Only writes the settings while the user keeps `opencodego.agentsWindow`
+ * and `opencodego.autoEnableAgentsWindow` on; the settings are merged with
+ * existing user values (never clobbering unrelated entries).
+ * - Records in globalState which settings the extension flipped itself, so
+ * {@link revertAgentsWindowSupport} can restore them when the user disables
+ * the Agents feature.
+ * - Both settings take effect after a window reload (extension host /
+ * agent host restart) — surface an actionable notification the first time
+ * anything was changed.
+ */
+export async function ensureAgentsWindowSupport(context: vscode.ExtensionContext): Promise {
+ const opencodeCfg = vscode.workspace.getConfiguration(CONFIG_SECTION);
+ if (!opencodeCfg.get(SETTING_AGENTS_WINDOW, true) || !opencodeCfg.get(SETTING_AUTO_ENABLE_AGENTS_WINDOW, true)) {
+ return;
+ }
+
+ let changed = false;
+ const extensionCfg = vscode.workspace.getConfiguration("extensions");
+ const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {});
+ if (!support[EXTENSION_ID]) {
+ await extensionCfg.update(SUPPORT_AGENTS_WINDOW_SETTING, { ...support, [EXTENSION_ID]: true }, vscode.ConfigurationTarget.Global);
+ await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, true);
+ changed = true;
+ }
+
+ if (isModernAgentHostVscode()) {
+ const agentHostCfg = vscode.workspace.getConfiguration("chat.agentHost");
+ if (!agentHostCfg.get(AGENT_HOST_BYOK_ENABLED_SETTING, false)) {
+ await agentHostCfg.update(AGENT_HOST_BYOK_ENABLED_SETTING, true, vscode.ConfigurationTarget.Global);
+ await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, true);
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ const reload = await vscode.window.showInformationMessage(
+ "OpenCode: enabled VS Code's Agents window support so OpenCode Go/Zen models can run in the Agents window. Reload the window for it to take effect.",
+ "Reload Now",
+ );
+ if (reload === "Reload Now") {
+ await vscode.commands.executeCommand("workbench.action.reloadWindow");
+ }
+ }
+}
+
+/**
+ * Revert the core settings that {@link ensureAgentsWindowSupport} enabled on
+ * this machine (and only those — settings the user configured manually are
+ * left untouched).
+ */
+export async function revertAgentsWindowSupport(context: vscode.ExtensionContext): Promise {
+ const extensionCfg = vscode.workspace.getConfiguration("extensions");
+ if (context.globalState.get(SUPPORT_AGENTS_WINDOW_STATE_KEY)) {
+ const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {});
+ if (support[EXTENSION_ID]) {
+ const next: Record = Object.fromEntries(Object.entries(support).filter(([id]) => id !== EXTENSION_ID));
+ await extensionCfg.update(
+ SUPPORT_AGENTS_WINDOW_SETTING,
+ Object.keys(next).length > 0 ? next : undefined,
+ vscode.ConfigurationTarget.Global,
+ );
+ }
+ await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, undefined);
+ }
+
+ if (context.globalState.get(AGENTS_BYOK_BRIDGE_STATE_KEY)) {
+ await vscode.workspace
+ .getConfiguration("chat.agentHost")
+ .update(AGENT_HOST_BYOK_ENABLED_SETTING, false, vscode.ConfigurationTarget.Global);
+ await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, undefined);
+ }
+}
+
+/** Warm the model picker metadata for every enabled vendor (incl. agent variants). */
+export async function warmModelPickerMetadata(): Promise {
+ const vendors: string[] = [
+ ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(GO_VENDOR), true) ? [GO_VENDOR] : []),
+ ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(ZEN_VENDOR), true) ? [ZEN_VENDOR] : []),
+ ];
+ if (vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true) && vendors.length > 0) {
+ vendors.push(AGENT_GO_VENDOR, AGENT_ZEN_VENDOR);
+ }
+ await Promise.allSettled(vendors.map((v) => vscode.lm.selectChatModels({ vendor: v })));
+}
diff --git a/src/commands/diagnostics.ts b/src/commands/diagnostics.ts
new file mode 100644
index 0000000..ad52092
--- /dev/null
+++ b/src/commands/diagnostics.ts
@@ -0,0 +1,41 @@
+import * as vscode from "vscode";
+import { CONFIG_SECTION, SETTING_AGENTS_WINDOW } from "../config";
+import { AGENT_GO_VENDOR, AGENT_ZEN_VENDOR, GO_VENDOR, ZEN_VENDOR } from "../providerTypes";
+
+/** Dump every visible model + configuration schema into a Markdown doc. */
+export async function showModelPickerDiagnostics(): Promise {
+ const vendors: string[] = [GO_VENDOR, ZEN_VENDOR, "copilot"];
+ if (vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true)) {
+ vendors.splice(2, 0, AGENT_GO_VENDOR, AGENT_ZEN_VENDOR);
+ }
+ const sections: string[] = [];
+
+ for (const vendor of vendors) {
+ const models = await vscode.lm.selectChatModels({ vendor });
+ sections.push(`## vendor: ${vendor}`, "", `models: ${String(models.length)}`, "");
+ for (const model of models) {
+ const internalModel = model as unknown as { configurationSchema?: unknown; detail?: unknown };
+ const schema = internalModel.configurationSchema;
+ sections.push(
+ `### ${model.name}`,
+ "",
+ `- id: \`${model.id}\``,
+ `- family: \`${model.family}\``,
+ `- version: \`${model.version}\``,
+ `- vendor: \`${model.vendor}\``,
+ `- detail: \`${typeof internalModel.detail === "string" ? internalModel.detail : ""}\``,
+ `- schema:`,
+ "```json",
+ JSON.stringify(schema ?? null, null, 2),
+ "```",
+ "",
+ );
+ }
+ }
+
+ const doc = await vscode.workspace.openTextDocument({
+ content: ["# OpenCode Model Picker Diagnostics", "", ...sections].join("\n"),
+ language: "markdown",
+ });
+ await vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside);
+}
diff --git a/src/commands/providers.ts b/src/commands/providers.ts
new file mode 100644
index 0000000..a6a2cc1
--- /dev/null
+++ b/src/commands/providers.ts
@@ -0,0 +1,38 @@
+import * as vscode from "vscode";
+import { SETTING_ENABLED } from "../config";
+
+/** Open the Settings UI filtered to the utility-model config keys. */
+export async function configureUtilityModels(): Promise {
+ await vscode.commands.executeCommand(
+ "workbench.action.openSettings",
+ "@id:chat.byokUtilityModelDefault @id:chat.utilityModel @id:chat.utilitySmallModel",
+ );
+}
+
+/**
+ * Toggle whether a provider (`opencodego` / `opencodezen`) is registered at
+ * all. Disabling removes the provider from the Language Models list and every
+ * model picker — the provider's vendor contribution is gated by the same
+ * `when` clause (`config..enabled`) and its runtime registration is
+ * skipped. Previously configured BYOK groups and API keys are kept, so
+ * re-enabling restores the provider exactly as it was.
+ *
+ * Provider registration happens at startup, so a window reload is required
+ * for the change to take effect.
+ */
+export async function toggleProviderEnabled(vendor: string, displayName: string): Promise {
+ const cfg = vscode.workspace.getConfiguration(vendor);
+ const current = cfg.get(SETTING_ENABLED, true);
+ const next = !current;
+ await cfg.update("enabled", next, vscode.ConfigurationTarget.Global);
+
+ const reload = await vscode.window.showInformationMessage(
+ next
+ ? `${displayName} re-enabled. Reload the window for the provider to appear in Language Models again.`
+ : `${displayName} removed from Language Models. Reload the window for it to disappear from the model picker and the manage list. Your API key and group settings are kept.`,
+ "Reload Now",
+ );
+ if (reload === "Reload Now") {
+ await vscode.commands.executeCommand("workbench.action.reloadWindow");
+ }
+}
diff --git a/src/commands/thinkingPicker.ts b/src/commands/thinkingPicker.ts
new file mode 100644
index 0000000..58333dd
--- /dev/null
+++ b/src/commands/thinkingPicker.ts
@@ -0,0 +1,31 @@
+import * as vscode from "vscode";
+import { CONFIG_SECTION } from "../config";
+import { getSettings } from "../provider/settings";
+import type { ThinkingSettings } from "../thinking";
+
+/** Pick a model family then set its Thinking effort (writes config). */
+export async function showThinkingEffortPicker(): Promise {
+ const families: { label: string; key: keyof ThinkingSettings; options: string[] }[] = [
+ { label: "DeepSeek (deepseek-v4-*)", key: "deepseek", options: ["off", "low", "medium", "high", "max"] },
+ { label: "GLM (glm-5, glm-5.1, glm-5.2)", key: "glm", options: ["off", "high", "max"] },
+ { label: "Kimi (kimi-k2.*)", key: "kimi", options: ["on", "off"] },
+ { label: "Mimo (mimo-v2.*)", key: "mimo", options: ["off", "low", "medium", "high"] },
+ { label: "MiniMax (minimax-m*)", key: "minimax", options: ["off", "on"] },
+ { label: "OpenAI GPT (gpt-*)", key: "openai", options: ["off", "low", "medium", "high", "xhigh"] },
+ { label: "Qwen (qwen3.*)", key: "qwen", options: ["auto", "on", "off"] },
+ { label: "Qwen Thinking Budget", key: "qwenBudget", options: ["auto", "4096", "16384", "32768", "81920"] },
+ ];
+ const settings = getSettings().thinking;
+ const family = await vscode.window.showQuickPick(
+ families.map((f) => ({ label: f.label, description: `current: ${settings[f.key]}`, family: f })),
+ { placeHolder: "Pick a model family to configure Thinking" },
+ );
+ if (!family) return;
+ const choice = await vscode.window.showQuickPick(family.family.options, {
+ placeHolder: `Set ${family.family.label} → Thinking value`,
+ });
+ if (!choice) return;
+ const cfg = vscode.workspace.getConfiguration(`${CONFIG_SECTION}.thinking`);
+ await cfg.update(family.family.key, choice, vscode.ConfigurationTarget.Global);
+ vscode.window.showInformationMessage(`OpenCode Thinking — ${family.family.label}: ${choice}`);
+}
diff --git a/src/config.ts b/src/config.ts
index f4680b7..8e0023e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -17,8 +17,15 @@
/** VS Code extension ID (used for `extensions.supportAgentsWindow.`). */
export const EXTENSION_ID = "ltmoerdani.opencode-copilot-chat";
-/** SecretStorage key for the API key. */
+/** SecretStorage key for the OpenCode Go API key (legacy name preserved). */
export const SECRET_KEY = "opencodego.apiKey";
+/** SecretStorage key for the OpenCode Zen API key (per-vendor, so Go and Zen
+ * never overwrite each other's key). */
+export const ZEN_SECRET_KEY = "opencodezen.apiKey";
+/** Resolve the SecretStorage key for a provider vendor. */
+export function secretKeyFor(vendor: "opencodego" | "opencodezen"): string {
+ return vendor === "opencodezen" ? ZEN_SECRET_KEY : SECRET_KEY;
+}
/** Client name sent in the `x-opencode-client` header. */
export const OPEN_CODE_CLIENT = "vscode-copilot-chat";
/** Fallback only — overridden at runtime from packageJSON.version. */
diff --git a/src/contextWindowHook.ts b/src/contextWindowHook.ts
index bc5e149..2689777 100644
--- a/src/contextWindowHook.ts
+++ b/src/contextWindowHook.ts
@@ -1,6 +1,6 @@
import { AsyncLocalStorage } from "node:async_hooks";
import * as vscode from "vscode";
-import type { UsageSnapshot } from "./usage";
+import type { UsageSnapshot } from "./usage/usage";
import { CONTEXT_HOOK_PROBE_DELAY_MS } from "./config";
import { getErrorMessage, isRecord } from "./utils";
diff --git a/src/contextWindowHookBridge.ts b/src/contextWindowHookBridge.ts
index a372f68..64ec15f 100644
--- a/src/contextWindowHookBridge.ts
+++ b/src/contextWindowHookBridge.ts
@@ -1,5 +1,5 @@
import type { LanguageModelResponsePart2, Progress } from "vscode";
-import type { UsageSnapshot } from "./usage";
+import type { UsageSnapshot } from "./usage/usage";
import { getErrorMessage } from "./utils";
type ContextWindowHookModule = typeof import("./contextWindowHook.js");
diff --git a/src/core/registry.ts b/src/core/registry.ts
new file mode 100644
index 0000000..6a5f67d
--- /dev/null
+++ b/src/core/registry.ts
@@ -0,0 +1,142 @@
+import type { ProviderVendor } from "../providerTypes";
+import type { ThinkingFamily } from "../thinking/types";
+
+/**
+ * Data-driven model registry — the single source of truth for "which model
+ * family uses which transport + thinking strategy".
+ *
+ * Adding a new model family = adding ONE row here (+ optionally a thinking
+ * strategy class in `src/thinking/`). Both the transport router
+ * (`core/routing.ts` → {@link resolveModelRouting}) and the thinking-family
+ * detector (`thinking/provider.ts` → {@link thinkingFamily}) consult this
+ * table, so a family's wiring never lives in two places again.
+ *
+ * Per-model context limits / capabilities / cost are NOT in this table —
+ * those come from the live models.dev metadata (`models/metadata.ts`), which
+ * is already data-driven. The registry covers the two places that used to
+ * hardcode per-family decisions in code.
+ *
+ * CONTRACT: pure — no `vscode` import, no side effects.
+ */
+
+/** Wire-format endpoint a model family speaks. */
+export type ModelEndpointKind = "chat-completions" | "messages" | "responses" | "google";
+
+export interface ModelRegistryEntry {
+ /** Human-readable family label (used in diagnostics). */
+ family: string;
+ /**
+ * Model-id patterns this entry matches. Entries are evaluated in table
+ * order and the FIRST match wins, so order matters — put the most specific
+ * patterns (e.g. `minimax-m2.` before `minimax-`) earlier.
+ */
+ patterns: RegExp[];
+ /** Transport the family uses. */
+ endpointKind: ModelEndpointKind;
+ /** SDK package hint for the endpoint (diagnostics / future adapter wiring). */
+ sdkPackage?: string;
+ /**
+ * Thinking strategy family (maps to `src/thinking/.ts`).
+ * `null` = no dedicated strategy (the generic fallback handles it).
+ */
+ thinkingFamily: ThinkingFamily;
+ /**
+ * Optional vendor restriction: the entry only applies when the model is
+ * served by one of these base vendors. Absent = applies to every vendor.
+ */
+ vendors?: ProviderVendor[];
+}
+
+export const MODEL_REGISTRY: ModelRegistryEntry[] = [
+ // GPT family → OpenAI Responses API (reasoning only supported there).
+ { family: "gpt", patterns: [/^gpt-/i], endpointKind: "responses", sdkPackage: "@ai-sdk/openai", thinkingFamily: "openai" },
+ // Claude family → Anthropic Messages API (any vendor).
+ { family: "claude", patterns: [/^claude-/i], endpointKind: "messages", sdkPackage: "@ai-sdk/anthropic", thinkingFamily: null },
+ // MiniMax m2.x served by Go → Anthropic Messages API; on Zen it falls back
+ // to chat-completions (the generic minimax row below).
+ {
+ family: "minimax-m2",
+ patterns: [/^minimax-m2\./i],
+ endpointKind: "messages",
+ sdkPackage: "@ai-sdk/anthropic",
+ thinkingFamily: "minimax",
+ vendors: ["opencodego"],
+ },
+ // Qwen models that use the Anthropic Messages API (the rest go chat-completions).
+ {
+ family: "qwen-messages",
+ patterns: [/^qwen3\.(?:5|6)-plus(?:-free)?$/i, /^qwen3\.7-max$/i],
+ endpointKind: "messages",
+ sdkPackage: "@ai-sdk/anthropic",
+ thinkingFamily: "qwen",
+ },
+ // Gemini family served by Zen → Google Generative Language API.
+ {
+ family: "gemini",
+ patterns: [/^gemini-/i],
+ endpointKind: "google",
+ sdkPackage: "@ai-sdk/google",
+ thinkingFamily: null,
+ vendors: ["opencodezen"],
+ },
+ // Everything else that has a dedicated thinking strategy → chat-completions.
+ {
+ family: "minimax",
+ patterns: [/^minimax-/i],
+ endpointKind: "chat-completions",
+ sdkPackage: "@ai-sdk/openai-compatible",
+ thinkingFamily: "minimax",
+ },
+ {
+ family: "deepseek",
+ patterns: [/^deepseek-/i],
+ endpointKind: "chat-completions",
+ sdkPackage: "@ai-sdk/openai-compatible",
+ thinkingFamily: "deepseek",
+ },
+ { family: "glm", patterns: [/^glm-/i], endpointKind: "chat-completions", sdkPackage: "@ai-sdk/openai-compatible", thinkingFamily: "glm" },
+ {
+ family: "kimi",
+ patterns: [/^kimi-/i],
+ endpointKind: "chat-completions",
+ sdkPackage: "@ai-sdk/openai-compatible",
+ thinkingFamily: "kimi",
+ },
+ {
+ family: "mimo",
+ patterns: [/^mimo-/i],
+ endpointKind: "chat-completions",
+ sdkPackage: "@ai-sdk/openai-compatible",
+ thinkingFamily: "mimo",
+ },
+ {
+ family: "qwen",
+ patterns: [/^qwen3(?:\.|-)/i],
+ endpointKind: "chat-completions",
+ sdkPackage: "@ai-sdk/openai-compatible",
+ thinkingFamily: "qwen",
+ },
+ // Catch-all: unknown families use chat-completions with the generic strategy.
+ { family: "default", patterns: [/.*/], endpointKind: "chat-completions", sdkPackage: "@ai-sdk/openai-compatible", thinkingFamily: null },
+];
+
+/**
+ * Look up the first registry entry matching `modelId`.
+ *
+ * @param vendor Optional base vendor to honor `vendors` restrictions (pass
+ * `undefined` — e.g. for the thinking-family lookup — to ignore
+ * them). The router always passes the resolved base vendor.
+ */
+export function lookupModelRegistryEntry(modelId: string, vendor?: ProviderVendor): ModelRegistryEntry {
+ for (const entry of MODEL_REGISTRY) {
+ if (vendor !== undefined && entry.vendors !== undefined && !entry.vendors.includes(vendor)) {
+ continue;
+ }
+ if (entry.patterns.some((pattern) => pattern.test(modelId))) {
+ return entry;
+ }
+ }
+ // Unreachable in practice (the default catch-all row matches everything),
+ // kept as a safety net.
+ return MODEL_REGISTRY[MODEL_REGISTRY.length - 1];
+}
diff --git a/src/routing.ts b/src/core/routing.ts
similarity index 90%
rename from src/routing.ts
rename to src/core/routing.ts
index 4890b70..3fe45f0 100644
--- a/src/routing.ts
+++ b/src/core/routing.ts
@@ -1,51 +1,47 @@
-import { GO_VENDOR, ZEN_VENDOR, resolveBaseVendor, type ProviderRoutingDefinition } from "./providerTypes";
-
-function isMessagesQwenModel(modelId: string): boolean {
- return /^qwen3\.(?:5|6)-plus(?:-free)?$/i.test(modelId) || /^qwen3\.7-max$/i.test(modelId);
-}
-
+import { resolveBaseVendor, type ProviderRoutingDefinition } from "../providerTypes";
+import { lookupModelRegistryEntry, type ModelEndpointKind } from "./registry";
+
+/**
+ * Resolve the transport for a raw model id from the data-driven registry
+ * (`core/registry.ts`). Adding a model family = adding a row to the registry,
+ * not editing this switch.
+ *
+ * Agent-host variants are resolved to their base vendor first (they mirror
+ * the vendor they serve).
+ */
export function resolveModelRouting(
modelId: string,
provider: ProviderRoutingDefinition,
): {
- endpointKind: "chat-completions" | "messages" | "responses" | "google";
+ endpointKind: ModelEndpointKind;
endpointUrl: string;
sdkPackage?: string;
} {
// Resolve agent-host variants to their base vendor for routing decisions.
const baseVendor = resolveBaseVendor(provider.vendor);
-
- // GPT models use the Responses API (not chat-completions).
- // OpenCode Go docs require gpt-5.6-luna on /v1/responses.
- // OpenAI reasoning models (GPT-5.x) only support reasoning via Responses API.
- if (/^gpt-/i.test(modelId)) {
- return {
- endpointKind: "responses",
- endpointUrl: provider.responsesUrl ?? provider.chatCompletionsUrl,
- sdkPackage: "@ai-sdk/openai",
- };
- }
-
- if (/^claude-/i.test(modelId) || (baseVendor === GO_VENDOR && /^minimax-m2\./i.test(modelId)) || isMessagesQwenModel(modelId)) {
- return {
- endpointKind: "messages",
- endpointUrl: provider.messagesUrl,
- sdkPackage: "@ai-sdk/anthropic",
- };
- }
-
- if (baseVendor === ZEN_VENDOR && /^gemini-/i.test(modelId)) {
- return {
- endpointKind: "google",
- endpointUrl: `${provider.modelsUrl}/${modelId}`,
- sdkPackage: "@ai-sdk/google",
- };
+ const entry = lookupModelRegistryEntry(modelId, baseVendor);
+
+ let endpointUrl: string;
+ switch (entry.endpointKind) {
+ case "responses":
+ // GPT models use the Responses API (not chat-completions). OpenCode Go
+ // docs require gpt-5.6-luna on /v1/responses.
+ endpointUrl = provider.responsesUrl ?? provider.chatCompletionsUrl;
+ break;
+ case "messages":
+ endpointUrl = provider.messagesUrl;
+ break;
+ case "google":
+ endpointUrl = `${provider.modelsUrl}/${modelId}`;
+ break;
+ default:
+ endpointUrl = provider.chatCompletionsUrl;
}
return {
- endpointKind: "chat-completions",
- endpointUrl: provider.chatCompletionsUrl,
- sdkPackage: "@ai-sdk/openai-compatible",
+ endpointKind: entry.endpointKind,
+ endpointUrl,
+ ...(entry.sdkPackage ? { sdkPackage: entry.sdkPackage } : {}),
};
}
diff --git a/src/core/transport.ts b/src/core/transport.ts
new file mode 100644
index 0000000..1efbb85
--- /dev/null
+++ b/src/core/transport.ts
@@ -0,0 +1,70 @@
+import type * as vscode from "vscode";
+
+/**
+ * Transport contract types shared by every streaming adapter in `transports/`.
+ * Types only — no runtime logic (safe for pure modules to import).
+ */
+export interface StreamRequestOptions {
+ url: string;
+ providerDisplayName: string;
+ apiKey: string;
+ modelId: string;
+ body: unknown;
+ requestHeaders: Record;
+ progress: vscode.Progress;
+ token: vscode.CancellationToken;
+ output?: vscode.OutputChannel;
+ debugReasoning: boolean;
+ requestTimeoutMs: number;
+ streamIdleTimeoutMs: number;
+ contextWindowOutputBuffer?: number;
+ authHeaders?: Record;
+ onReasoningContent?: (toolCallIds: string[], reasoningContent: string) => void;
+ capacityLimitedModelNotes?: Record;
+ onTransportSummary?: (summary: TransportRequestSummary) => void;
+ /**
+ * Whether `reasoning_content` should be surfaced as visible text instead of
+ * a thinking part. Computed UPSTREAM by the thinking provider strategy from
+ * the resolved thinking config — never inferred from the body here.
+ *
+ * Currently false for every family: reasoning models emit genuine CoT in
+ * `reasoning_content`, so it always goes to the thinking panel. (The old
+ * gateway #37635 mislabel is the gateway's bug, not worked around here.)
+ */
+ treatReasoningAsContent?: boolean;
+ /**
+ * Controls whether `...` tags inlined in the model's text
+ * content are stripped and accumulated as reasoning content.
+ *
+ * - "never" — pass text through unchanged
+ * - "auto" — strip only for models known to inline thinking tags
+ * (currently: minimax-m*)
+ * - "always" — strip for every model
+ */
+ stripThinkTags?: "never" | "auto" | "always";
+}
+
+export interface TransportRequestSummary {
+ providerDisplayName: string;
+ modelId: string;
+ url: string;
+ requestId?: string;
+ sessionId?: string;
+ status?: number;
+ contentType?: string;
+ payloadBytes: number;
+ totalBytes: number;
+ totalEvents: number;
+ durationMs: number;
+ ttfbMs?: number;
+ promptTokens?: number;
+ completionTokens?: number;
+ totalTokens?: number;
+ cachedTokens?: number;
+ finishReason?: string;
+ /** Credits for VS Code session cost (1 credit = $0.01). */
+ copilotCredits?: number;
+ rateLimitSummary?: string;
+ abortedReason?: "request-timeout" | "stream-idle-timeout" | "cancelled";
+ errorMessage?: string;
+}
diff --git a/src/extension.ts b/src/extension.ts
index ccedda7..d5952d3 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,685 +1,69 @@
import * as vscode from "vscode";
-import { OpenCodeRequestError } from "./errors";
-import {
- MODEL_METADATA_CACHE_KEY,
- MODEL_METADATA_REVISION,
- MODELS_DEV_API_URL,
- bundledModelMetadataSnapshot,
- getContextSizeOptionsForModel,
- hasExplicitModelLimits,
- isFreshModelMetadata,
- normalizeLiveModelMetadata,
- normalizeModelsDevSnapshot,
- resolveModelMetadata,
- toEffectiveModelId,
- VISION_CAPABLE_MODELS,
- type CachedModelMetadataSnapshot,
- type ModelMetadataFields,
- type ModelsDevResponse,
- type ResolvedModelMetadata,
-} from "./metadata";
-import { resolveModelRouting } from "./routing";
-import {
- buildFamilyThinkingSchema,
- buildQwenAnthropicThinkingPayload,
- buildThinkingPayload,
- applyRequestThinkingOverride,
- thinkingFamily,
- type ThinkingSettings,
-} from "./thinking";
-import { shouldEchoThinkingHistory, thinkingTextFromValue } from "./reasoningHistory";
-import { buildOpenCodeGatewayAuthHeaders } from "./openCodeAuth";
-import {
- streamAnthropicMessages as runStreamAnthropicMessages,
- streamChatCompletions as runStreamChatCompletions,
- streamGoogleGenerateContent as runStreamGoogleGenerateContent,
- streamResponsesApi as runStreamResponsesApi,
- type TransportRequestSummary,
-} from "./streaming";
-import {
- GO_VENDOR,
- ZEN_VENDOR,
- AGENT_GO_VENDOR,
- AGENT_ZEN_VENDOR,
- resolveBaseVendor,
- type AllProviderVendor,
- type ProviderVendor,
-} from "./providerTypes";
-import { providerEnabledSetting } from "./providerEnablement";
-import { isInternalDataPart, isReasoningMarkerPart, readReasoningMarker } from "./chatParts";
import { registerInlineCompletions } from "./autocomplete";
-import { completionUsageToSeries, type CompletionUsageDay } from "./autocomplete/usage";
-import { getImageDataUrlBase64Bytes, MAX_IMAGE_BASE64_BYTES, normalizeImageDataUrl } from "./imageNormalizer";
-import { imageDescriptionKey, lookupImageDescriptions, storeImageDescriptions } from "./visionProxyCache";
-import { providerModelDisplayName } from "./modelNames";
-import { buildStableModelCapabilities } from "./modelCapabilities";
-import { calculateModelLimits, type ModelLimits } from "./modelLimits";
-import { buildResponsesRequestEnvelope, joinedTextContent, responsesInputItemsFromMessage } from "./responsesRequest";
-import { runtimeDiagnosticsLines } from "./runtimeDiagnostics";
-import { estimatePromptTokenCount, estimateTokenCount } from "./tokenEstimate";
+import { ensureAgentsWindowSupport, revertAgentsWindowSupport, warmModelPickerMetadata } from "./commands/agentsWindow";
+import { showModelPickerDiagnostics } from "./commands/diagnostics";
+import { showThinkingEffortPicker } from "./commands/thinkingPicker";
+import { configureUtilityModels, toggleProviderEnabled } from "./commands/providers";
import {
- AGENTS_BYOK_BRIDGE_STATE_KEY,
- AGENT_HOST_BYOK_ENABLED_SETTING,
- AGENT_HOST_BYOK_MINOR_VERSION,
- CAPACITY_LIMITED_MODEL_NOTES,
- COMPLETION_USAGE_KEY,
CONFIG_SECTION,
- DEFAULT_REQUEST_TIMEOUT_SECONDS,
- DEFAULT_STREAM_IDLE_TIMEOUT_SECONDS,
- DEFAULT_VISION_PROXY_PROMPT,
- EXTENSION_ID,
- FALLBACK_USER_AGENT,
- FREE_ZEN_MODEL_IDS,
- IMAGE_TOKEN_ESTIMATE,
- KNOWN_UNAVAILABLE_MODEL_IDS,
- MAX_HISTORY_IMAGES_KEPT,
- MAX_TOOL_RESULT_IMAGE_BYTES,
- MESSAGE_NAME_TOKEN_OVERHEAD,
- MESSAGE_TOKEN_OVERHEAD,
- MODEL_LIST_CACHE_KEY_PREFIX,
- MODEL_LIST_CACHE_TTL_MS,
- MODEL_LIST_FETCH_MAX_RETRIES,
- MODEL_LIST_FETCH_RETRY_BASE_MS,
- MODEL_LIST_FETCH_TIMEOUT_MS,
- MODEL_METADATA_FETCH_TIMEOUT_MS,
- OPEN_CODE_CLIENT,
- RECENT_TRANSPORT_SUMMARY_LIMIT,
- RECENT_TRANSPORT_SUMMARY_STORAGE_PREFIX,
- SECRET_KEY,
+ DEFAULT_USAGE_CHART_DAYS,
SETTING_AGENTS_WINDOW,
SETTING_AUTO_ENABLE_AGENTS_WINDOW,
- SETTING_DEBUG_REASONING,
- SETTING_ENABLED,
- SETTING_FREE_ONLY,
- SETTING_MAX_INPUT_TOKENS,
- SETTING_MAX_TOKENS,
- SETTING_REQUEST_TIMEOUT_SECONDS,
SETTING_SHOW_PROVIDER_PREFIX,
SETTING_SHOW_USAGE_STATUS_BAR,
- SETTING_STREAM_IDLE_TIMEOUT_SECONDS,
- SETTING_STRIP_THINK_TAGS,
- SETTING_TEMPERATURE,
- SETTING_THINKING_DEEPSEEK,
- SETTING_THINKING_GLM,
- SETTING_THINKING_KIMI,
- SETTING_THINKING_MIMO,
- SETTING_THINKING_MINIMAX,
- SETTING_THINKING_OPENAI,
- SETTING_THINKING_QWEN,
- SETTING_THINKING_QWEN_BUDGET,
- SETTING_VISION_PROXY_WHOLE_CONVERSATION,
- SUPPORT_AGENTS_WINDOW_SETTING,
- SUPPORT_AGENTS_WINDOW_STATE_KEY,
- TEST_CONNECTION_TIMEOUT_MS,
- THINKING_DEFAULTS,
- TOOL_CALL_TOKEN_OVERHEAD,
- TOOL_RESULT_TOKEN_OVERHEAD,
- VISION_PROXY_MODEL_ID_KEY,
- VISION_PROXY_PROMPT_KEY,
- DEFAULT_USAGE_CODEBASE_ROW,
- DEFAULT_USAGE_CODEBASE_WINDOW_DAYS,
- DEFAULT_USAGE_DAY_BOUNDARY,
- DEFAULT_USAGE_REFRESH_INTERVAL_SECONDS,
- DEFAULT_USAGE_CHART_DAYS,
- DEFAULT_USAGE_ROLLING_SESSION_METER,
- DEFAULT_USAGE_TODAY_YESTERDAY_SOURCE,
- SETTING_USAGE_CODEBASE_ROW,
- SETTING_USAGE_CODEBASE_WINDOW_DAYS,
- SETTING_USAGE_DAY_BOUNDARY,
- SETTING_USAGE_REFRESH_INTERVAL_SECONDS,
SETTING_USAGE_CHART_DAYS,
- SETTING_USAGE_ROLLING_SESSION_METER,
- SETTING_USAGE_TODAY_YESTERDAY_SOURCE,
- type UsageTodayYesterdaySource,
+ secretKeyFor,
} from "./config";
-import {
- escapeHtml,
- formatCount,
- formatRelativeTime,
- formatTokenCount,
- formatUsd,
- getErrorMessage,
- isRecord,
- sleep,
- toFiniteNumber,
-} from "./utils";
-import { parseToolInput as parseToolInputShared } from "./toolCallAccumulator";
-import { isFreeModel } from "./metadata";
-
-import { formatCacheHitRatio, formatUsageStatusBarText, formatUsageStatusBarTooltip, type UsageSnapshot } from "./usage";
-import {
- GoUsageTracker,
- GO_LIMITS,
- formatGoUsageStatusBarText,
- buildUsageQuickPickItems,
- estimateCost,
- type GoUsageTrackerOptions,
- type UsageBaselineTargets,
-} from "./goUsageTracker";
-import { resolveResponseApiKey } from "./apiKeyResolution";
+import { GoUsageTracker } from "./usage/tracker";
+import { buildUsageQuickPickItems } from "./usage/formatting";
+import { PROVIDERS } from "./provider/definitions";
+import { OpenCodeProvider } from "./provider/OpenCodeProvider";
+import { getModelMetadataSnapshot } from "./models/metadataFetcher";
+import { GO_VENDOR, ZEN_VENDOR, AGENT_GO_VENDOR, AGENT_ZEN_VENDOR } from "./providerTypes";
+import { providerEnabledSetting } from "./providerEnablement";
+import { showVisionProxyPicker } from "./provider/visionProxy";
+import { formatCount, formatTokenCount, formatUsd } from "./utils";
import {
LEGACY_FINGERPRINT,
+ findProfile,
keyFingerprint,
readActiveProfile,
+ readActiveProfiles,
readProfiles,
+ renameProfile,
writeActiveProfile,
writeProfiles,
- readActiveProfiles,
- readMigratedTo,
- writeMigratedTo,
- findProfile,
- renameProfile,
- nonLegacyCount,
- type UsageProfile,
-} from "./usageProfile";
-
-/**
- * VS Code core settings the extension manages (auto-configures and reverts)
- * so OpenCode models work in the Agents window (issue #122):
- *
- * - `chat.agentHost.byokModels.enabled`: wires the agent-host BYOK bridge
- * (VS Code 1.129+); off by default, so extension-provided BYOK models never
- * reach agent-host sessions until it is flipped on.
- * - `extensions.supportAgentsWindow.`: the ONLY way a code extension is
- * allowed to run in the Agents window (sessions window) process. Without
- * it the extension is disabled there, its `languageModelChatProviders`
- * vendors are not registered, and neither the model picker nor the
- * "+ Add Models" list knows OpenCode Go/Zen.
- */
-
-let usageStatusBarItem: vscode.StatusBarItem | undefined;
-let goUsageStatusBarItem: vscode.StatusBarItem | undefined;
-/** Singleton tracker — the first/legacy account. Used for backward compat until first migration. */
-let goUsageTracker: GoUsageTracker | undefined;
-/** Per-profile trackers indexed by key fingerprint. */
-const goUsageTrackers = new Map();
-/** API key per profile fingerprint — lets refreshes sync the active profile's own key. */
-const profileApiKeys = new Map();
-let usageWebviewPanel: vscode.WebviewPanel | undefined;
-
-let profilesCache: UsageProfile[] = [];
-let activeProfileFingerprint: string = LEGACY_FINGERPRINT;
-
-/**
- * Resolvers for the per-view usage knobs, read live from configuration so
- * changing a setting repaints the status bar / tooltip / card immediately.
- */
-function usageTrackerOptions(): GoUsageTrackerOptions {
- const config = () => vscode.workspace.getConfiguration(CONFIG_SECTION);
- return {
- resolveWorkspaceFolders: () => vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? [],
- resolveTodayYesterdaySource: () =>
- config().get(SETTING_USAGE_TODAY_YESTERDAY_SOURCE, DEFAULT_USAGE_TODAY_YESTERDAY_SOURCE),
- resolveCodebaseWindowDays: () => config().get(SETTING_USAGE_CODEBASE_WINDOW_DAYS, DEFAULT_USAGE_CODEBASE_WINDOW_DAYS),
- resolveDayBoundary: () => config().get<"utc" | "local">(SETTING_USAGE_DAY_BOUNDARY, DEFAULT_USAGE_DAY_BOUNDARY),
- };
-}
-
-/** Whether the detailed usage views show the server 5h rolling meter. */
-function usageRollingMeterVisible(): boolean {
- return vscode.workspace
- .getConfiguration(CONFIG_SECTION)
- .get(SETTING_USAGE_ROLLING_SESSION_METER, DEFAULT_USAGE_ROLLING_SESSION_METER);
-}
-
-/** Whether the detailed usage views show the all-time codebase row. */
-function usageCodebaseRowVisible(): boolean {
- return vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_USAGE_CODEBASE_ROW, DEFAULT_USAGE_CODEBASE_ROW);
-}
-
-/** Every usage-view setting — a change to any of these repaints immediately. */
-const USAGE_DISPLAY_SETTING_KEYS = [
- SETTING_USAGE_TODAY_YESTERDAY_SOURCE,
- SETTING_USAGE_CODEBASE_ROW,
- SETTING_USAGE_CODEBASE_WINDOW_DAYS,
- SETTING_USAGE_DAY_BOUNDARY,
- SETTING_USAGE_ROLLING_SESSION_METER,
- SETTING_USAGE_REFRESH_INTERVAL_SECONDS,
-];
-
-/**
- * Realtime usage updates: re-render the status bar (and webview) on a
- * configurable cadence so terminal-side OpenCode CLI usage, server meters and
- * day rollovers show up without waiting for the next chat request. The
- * interval re-reads the setting on every tick, so changes apply live.
- */
-function startUsageRefreshLoop(context: vscode.ExtensionContext): void {
- let timer: ReturnType | undefined;
- const schedule = (): void => {
- timer = setTimeout(() => {
- refreshGoUsageStatusBar();
- schedule();
- }, usageRefreshIntervalSeconds() * 1000);
- };
- schedule();
- context.subscriptions.push({
- dispose: () => {
- if (timer) clearTimeout(timer);
- },
- });
-}
-
-function usageRefreshIntervalSeconds(): number {
- return Math.max(
- 5,
- vscode.workspace
- .getConfiguration(CONFIG_SECTION)
- .get(SETTING_USAGE_REFRESH_INTERVAL_SECONDS, DEFAULT_USAGE_REFRESH_INTERVAL_SECONDS),
- );
-}
-
-/** Look up (or create) the GoUsageTracker for a given key fingerprint. */
-function getOrCreateTracker(fingerprint: string): GoUsageTracker {
- // The singleton tracker does not have a storage suffix
- if (fingerprint === LEGACY_FINGERPRINT && goUsageTracker) return goUsageTracker;
- let tracker = goUsageTrackers.get(fingerprint);
- if (tracker) return tracker;
- tracker = new GoUsageTracker(
- extensionContext(),
- (msg) => {
- usageLogChannel().appendLine(`[${new Date().toISOString()}] [${fingerprint}] ${msg}`);
- },
- (modelId) => modelMetadataSnapshot?.providers[GO_VENDOR]?.[modelId]?.cost,
- fingerprint,
- usageTrackerOptions(),
- );
- goUsageTrackers.set(fingerprint, tracker);
- return tracker;
-}
-
-/** Return the tracker for the currently active profile. */
-function activeGoUsageTracker(): GoUsageTracker | undefined {
- if (activeProfileFingerprint === LEGACY_FINGERPRINT) return goUsageTracker;
- return goUsageTrackers.get(activeProfileFingerprint);
-}
-
-/** Switch the active profile and refresh the UI. */
-async function setActiveProfile(fingerprint: string): Promise {
- activeProfileFingerprint = fingerprint;
- await writeActiveProfile(extensionContext(), fingerprint);
- refreshGoUsageStatusBar();
- updateWebviewContent();
-}
-
-/**
- * Ensure a profile exists in the in-memory cache for the given API key.
- * This is called both from provideLanguageModelChatInformation (at startup,
- * when VS Code resolves all providers) and from onTransportSummary (when
- * a request completes). The first call creates the profile; subsequent
- * calls are no-ops. Persistence is fire-and-forget.
- */
-function ensureProfileSync(apiKey: string): void {
- const fp = keyFingerprint(apiKey);
- const tracker = getOrCreateTracker(fp);
-
- if (!findProfile(profilesCache, fp)) {
- const nextNumber = nonLegacyCount(profilesCache) + 1;
- profilesCache.push({
- fingerprint: fp,
- label: `Profile ${String(nextNumber)}`,
- lastSeenAt: Date.now(),
- });
- void writeProfiles(extensionContext(), profilesCache);
- }
-
- // One-time migration from singleton
- if (!readMigratedTo(extensionContext())) {
- if (goUsageTracker && fp !== LEGACY_FINGERPRINT) {
- tracker.migrateFromSingleton();
- }
- void writeMigratedTo(extensionContext(), fp);
- profilesCache = readProfiles(extensionContext());
- }
-
- // Update active profile to this one
- activeProfileFingerprint = fp;
- void writeActiveProfile(extensionContext(), fp);
-}
-
-/**
- * Same as ensureProfileSync, but also refreshes the UI.
- * Called from onTransportSummary during request recording.
- */
-function ensureProfileForApiKey(apiKey: string): GoUsageTracker {
- ensureProfileSync(apiKey);
- // Remember which API key owns each profile, so status-bar refreshes can
- // sync the ACTIVE profile's meters with its own key instead of the
- // extension secret (which may belong to another account).
- profileApiKeys.set(keyFingerprint(apiKey), apiKey);
- return getOrCreateTracker(keyFingerprint(apiKey));
-}
-
-let _extensionContext: vscode.ExtensionContext | undefined;
-let _usageLogChannel: vscode.OutputChannel | undefined;
-
-/**
- * Returns the extension context, or throws if the extension has not been
- * activated yet. Callers must be reached after `activate()` has run.
- */
-function extensionContext(): vscode.ExtensionContext {
- if (!_extensionContext) {
- throw new Error("extension context not initialized");
- }
- return _extensionContext;
-}
-
-/**
- * Returns the usage log output channel, or throws if the extension has not
- * been activated yet. Callers must be reached after `activate()` has run.
- */
-function usageLogChannel(): vscode.OutputChannel {
- if (!_usageLogChannel) {
- throw new Error("usage log channel not initialized");
- }
- return _usageLogChannel;
-}
-
-interface ProviderDefinition {
- vendor: AllProviderVendor;
- displayName: string;
- modelNamePrefix: string;
- modelsUrl: string;
- chatCompletionsUrl: string;
- messagesUrl: string;
- responsesUrl?: string;
- testModelId: string;
- fallbackModels: string[];
- filterModel?: (modelId: string) => boolean;
- /** When true, this provider only serves agent-host models (targetChatSessionType=copilotcli). */
- isAgentVariant?: boolean;
- /** The vendor key for the main (non-agent) provider definition this variant mirrors. */
- baseVendor?: typeof GO_VENDOR | typeof ZEN_VENDOR;
-}
-
-type ModelEndpointKind = "chat-completions" | "messages" | "responses" | "google";
-
-let cachedUserAgent: string | undefined;
-
-/**
- * Build the User-Agent string from the extension's declared version.
- *
- * CONTRACT:
- * - Reads `context.extension.packageJSON.version` once, caches the result.
- * - Falls back to {@link FALLBACK_USER_AGENT} when version is unavailable
- * (e.g. tests that construct a stub context).
- * - Avoids the drift that previously hardcoded a version literal here
- * (issue #78: header reported `0.3.6` while package.json was `0.4.1`).
- */
-function getUserAgent(): string {
- if (cachedUserAgent) return cachedUserAgent;
- const packageJSON = vscode.extensions.getExtension("ltmoerdani.opencode-copilot-chat")?.packageJSON as { version?: unknown } | undefined;
- const version = typeof packageJSON?.version === "string" ? packageJSON.version : undefined;
- cachedUserAgent = version ? `opencode-copilot-chat/${version} VSCode` : FALLBACK_USER_AGENT;
- return cachedUserAgent;
-}
-
-/**
- * Classify a fetch error as transient (worth retrying) vs. permanent.
- *
- * RULES:
- * - Network-layer errors (DNS, TCP reset, connect timeout, socket errors)
- * are transient — undici exposes the real code via `error.cause`.
- * - HTTP 4xx (except 408/429) is permanent — retrying won't help.
- * - HTTP 408/429/5xx is transient — gateway/rate-limit style failures.
- * These arrive via the "Model list request failed (NNN): ..." message
- * that `fetchModels()` throws on a non-2xx response.
- * - AbortError from a CancellationToken is NEVER retried. TimeoutError from
- * AbortSignal.timeout is transient and can be retried.
- */
-function isTransientFetchError(error: unknown): boolean {
- // DOMException is a global since Node 17; guard anyway so a hypothetical
- // older host never crashes inside error classification.
- if (typeof DOMException === "function" && error instanceof DOMException) {
- if (error.name === "AbortError") return false;
- if (error.name === "TimeoutError") return true;
- }
- const cause = (error as { cause?: { code?: string; name?: string } } | undefined)?.cause;
- const code = cause?.code ?? (error as { code?: string } | undefined)?.code;
- const name = cause?.name ?? (error as { name?: string } | undefined)?.name;
- // undici network error codes
- if (code && /^E(AI_AGAIN|CONNRESET|CONNREFUSED|CONNABORTED|TIMEDOUT|HOSTUNREACH|NETUNREACH|PROTO|PIPE)$/.test(code)) {
- return true;
- }
- if (name && /^UND_ERR_(CONNECT_TIMEOUT|SOCKET|REQUEST_TIMEOUT)$/.test(name)) {
- return true;
- }
- // TypeError: fetch failed (the generic wrapper undici throws) — always retry;
- // if the cause turns out to be non-transient, the inner check above handles it.
- if (error instanceof TypeError && /fetch failed/i.test(error.message)) return true;
- // Extract HTTP status from either an explicit `.status` field or the
- // "Model list request failed (NNN): ..." message pattern.
- const explicitStatus = (error as { status?: number } | undefined)?.status;
- const msg = getErrorMessage(error);
- const msgMatch = msg.match(/\((\d{3})\)/);
- const httpStatus = typeof explicitStatus === "number" ? explicitStatus : msgMatch ? Number(msgMatch[1]) : undefined;
- if (typeof httpStatus === "number") {
- if (httpStatus === 408 || httpStatus === 429 || httpStatus >= 500) return true;
- return false;
- }
- return false;
-}
-
-/** Create an agent-variant provider definition that inherits URLs, models, and filters from a base. */
-function providerVariant(
- base: ProviderDefinition,
- agentVendor: typeof AGENT_GO_VENDOR | typeof AGENT_ZEN_VENDOR,
- displayName: string,
-): ProviderDefinition {
- return {
- vendor: agentVendor,
- displayName,
- modelNamePrefix: base.modelNamePrefix,
- modelsUrl: base.modelsUrl,
- chatCompletionsUrl: base.chatCompletionsUrl,
- messagesUrl: base.messagesUrl,
- responsesUrl: base.responsesUrl,
- testModelId: base.testModelId,
- fallbackModels: base.fallbackModels,
- filterModel: base.filterModel,
- };
-}
-
-const PROVIDERS: Record = (() => {
- const go: ProviderDefinition = {
- vendor: GO_VENDOR,
- displayName: "OpenCode Go",
- modelNamePrefix: "OpenCode Go",
- modelsUrl: "https://opencode.ai/zen/go/v1/models",
- chatCompletionsUrl: "https://opencode.ai/zen/go/v1/chat/completions",
- messagesUrl: "https://opencode.ai/zen/go/v1/messages",
- responsesUrl: "https://opencode.ai/zen/go/v1/responses",
- testModelId: "deepseek-v4-flash",
- fallbackModels: [
- "deepseek-v4-pro",
- "deepseek-v4-flash",
- "glm-5.1",
- "glm-5",
- "hy3-preview",
- "kimi-k2.6",
- "kimi-k2.5",
- "mimo-v2-omni",
- "mimo-v2-pro",
- "mimo-v2.5",
- "mimo-v2.5-pro",
- "minimax-m2.7",
- "minimax-m2.5",
- "qwen3.7-max",
- "qwen3.7-plus",
- "qwen3.6-plus",
- "qwen3.5-plus",
- "gpt-5.6-luna",
- ],
- };
- const zen: ProviderDefinition = {
- vendor: ZEN_VENDOR,
- displayName: "OpenCode Zen",
- modelNamePrefix: "OpenCode Zen",
- modelsUrl: "https://opencode.ai/zen/v1/models",
- chatCompletionsUrl: "https://opencode.ai/zen/v1/chat/completions",
- messagesUrl: "https://opencode.ai/zen/v1/messages",
- responsesUrl: "https://opencode.ai/zen/v1/responses",
- testModelId: "deepseek-v4-flash-free",
- fallbackModels: [
- "claude-opus-4-7",
- "claude-opus-4-6",
- "claude-opus-4-5",
- "claude-opus-4-1",
- "claude-sonnet-4-6",
- "claude-sonnet-4-5",
- "claude-sonnet-4",
- "claude-haiku-4-5",
- "deepseek-v4-flash-free",
- "gemini-3.5-flash",
- "gemini-3.1-pro",
- "gemini-3-flash",
- "glm-5.1",
- "glm-5",
- "gpt-5.5",
- "gpt-5.5-pro",
- "gpt-5.4",
- "gpt-5.4-pro",
- "gpt-5.4-mini",
- "gpt-5.4-nano",
- "gpt-5.3-codex",
- "gpt-5.3-codex-spark",
- "gpt-5.2",
- "gpt-5.2-codex",
- "gpt-5.1",
- "gpt-5.1-codex",
- "gpt-5.1-codex-max",
- "gpt-5.1-codex-mini",
- "gpt-5",
- "gpt-5-codex",
- "gpt-5-nano",
- "grok-build-0.1",
- "kimi-k2.6",
- "kimi-k2.5",
- "minimax-m2.7",
- "minimax-m2.5",
- "minimax-m2.5-free",
- "nemotron-3-super-free",
- "qwen3.6-plus",
- "qwen3.6-plus-free",
- "qwen3.5-plus",
- "big-pickle",
- ],
- filterModel: (modelId) =>
- vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_FREE_ONLY, true)
- ? modelId.endsWith("-free") || FREE_ZEN_MODEL_IDS.has(modelId)
- : true,
- };
- return {
- [GO_VENDOR]: go,
- [ZEN_VENDOR]: zen,
- [AGENT_GO_VENDOR]: { ...providerVariant(go, AGENT_GO_VENDOR, "OpenCode Go (Agents)"), isAgentVariant: true, baseVendor: GO_VENDOR },
- [AGENT_ZEN_VENDOR]: {
- ...providerVariant(zen, AGENT_ZEN_VENDOR, "OpenCode Zen (Agents)"),
- isAgentVariant: true,
- baseVendor: ZEN_VENDOR,
- },
- };
-})();
-
-type ApiRole = "user" | "assistant" | "tool";
-
-interface OpenCodeModel extends vscode.LanguageModelChatInformation {
- endpointKind: ModelEndpointKind;
- provider: ProviderDefinition;
- rawModelId?: string;
- isUserSelectable?: boolean;
- configurationSchema?: vscode.LanguageModelConfigurationSchema;
-}
-
-interface ModelListEntry {
- id?: string;
- owned_by?: string;
- status?: string;
- deprecated?: boolean;
- limit?: {
- context?: number;
- output?: number;
- };
- context_window?: number;
- contextWindow?: number;
- max_output_tokens?: number;
- maxOutputTokens?: number;
- attachment?: boolean;
- image_input?: boolean;
- imageInput?: boolean;
- reasoning?: boolean;
- modalities?: {
- input?: string[];
- output?: string[];
- };
-}
-
-interface ModelListResponse {
- data?: ModelListEntry[];
-}
-
-interface ApiMessage {
- role: ApiRole;
- content: string | null | OpenAiContentPart[];
- reasoning_content?: string;
- tool_call_id?: string;
- tool_calls?: OpenAiToolCall[];
-}
-
-interface OpenAiContentPart {
- type: "text" | "image_url";
- text?: string;
- image_url?: {
- url: string;
- };
-}
-
-interface OpenAiToolCall {
- id: string;
- type: "function";
- function: {
- name: string;
- arguments: string;
- };
-}
-
-interface ConvertedMessageResult {
- messages: ApiMessage[];
- normalizedImageCount: number;
-}
-
-/**
- * Reasoning effort levels per model family, sourced from the upstream
- * OpenCode provider transform (anomalyco/opencode, packages/opencode/src/provider/transform.ts):
- *
- * WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
- * OPENAI_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"]
- *
- * For @ai-sdk/openai-compatible (Mimo, and most models routed through
- * chat-completions): the default is WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"].
- * DeepSeek V4 on openai-compatible additionally adds "max" → ["low", "medium", "high", "max"].
- */
-interface ApiSettings {
- temperature: number;
- maxOutputTokensOverride: number;
- maxInputTokensOverride: number;
- debugReasoning: boolean;
- requestTimeoutMs: number;
- streamIdleTimeoutMs: number;
- thinking: ThinkingSettings;
- stripThinkTags: "never" | "auto" | "always";
-}
-
-interface LanguageModelConfiguration {
- apiKey?: unknown;
-}
-
-type ConfiguredLanguageModelInfoOptions = vscode.PrepareLanguageModelChatModelOptions & {
- configuration?: LanguageModelConfiguration;
-};
-
-type ConfiguredLanguageModelResponseOptions = vscode.ProvideLanguageModelChatResponseOptions & {
- configuration?: LanguageModelConfiguration;
-};
+} from "./usage/usageProfile";
+import {
+ USAGE_DISPLAY_SETTING_KEYS,
+ _extensionContext,
+ activeGoUsageTracker,
+ activeProfileFingerprint,
+ ensureGoUsageStatusBar,
+ ensureUsageStatusBar,
+ extensionContext,
+ getOrCreateTracker,
+ goUsageTrackers,
+ profileApiKeys,
+ profilesCache,
+ refreshGoUsageStatusBar,
+ resetUsageStatusBar,
+ setActiveProfile,
+ setActiveProfileFingerprint,
+ setExtensionContext,
+ setGoUsageTracker,
+ setProfilesCache,
+ setUsageChartWindowDays,
+ setUsageLogChannel,
+ showUsageTargetEditor,
+ showUsageWebview,
+ startUsageRefreshLoop,
+ syncTrackerUsage,
+ updateWebviewContent,
+ usageCodebaseRowVisible,
+ usageRollingMeterVisible,
+ usageTrackerOptions,
+} from "./usage/dashboard";
/**
* Hard upper limit (in bytes of raw image data) for a single image embedded
@@ -719,104 +103,26 @@ type ConfiguredLanguageModelResponseOptions = vscode.ProvideLanguageModelChatRes
* for understanding agent-loop context) without incurring the payload cost.
*/
-let modelMetadataSnapshot: CachedModelMetadataSnapshot | undefined;
-let modelMetadataRefreshPromise: Promise | undefined;
-
-interface OpenAiToolDefinition {
- type: "function";
- function: {
- name: string;
- description: string;
- parameters: object;
- };
-}
-
-interface AnthropicToolDefinition {
- name: string;
- description: string;
- input_schema: object;
-}
-
-interface AnthropicCacheControl {
- type: "ephemeral";
-}
-
-interface AnthropicTextBlock {
- type: "text";
- text: string;
- cache_control?: AnthropicCacheControl;
-}
-
-interface AnthropicImageSourceUrl {
- type: "url";
- url: string;
-}
-
-interface AnthropicImageSourceBase64 {
- type: "base64";
- media_type: string;
- data: string;
-}
-
-type AnthropicImageSource = AnthropicImageSourceUrl | AnthropicImageSourceBase64;
-
-interface AnthropicImageBlock {
- type: "image";
- source: AnthropicImageSource;
- cache_control?: AnthropicCacheControl;
-}
-
-interface AnthropicToolUseBlock {
- type: "tool_use";
- id: string;
- name: string;
- input: unknown;
- cache_control?: AnthropicCacheControl;
-}
-
-interface AnthropicToolResultBlock {
- type: "tool_result";
- tool_use_id: string;
- // Anthropic tool_result.content may be either a plain string or a list of
- // content blocks (text + image) per the Messages API spec. We support the
- // array form so MCP tool results that include images (e.g. screenshots) are
- // forwarded to vision-capable Anthropic models instead of being dropped.
- content: string | AnthropicContentBlock[];
- cache_control?: AnthropicCacheControl;
-}
-
-type AnthropicContentBlock = AnthropicTextBlock | AnthropicImageBlock | AnthropicToolUseBlock | AnthropicToolResultBlock;
-
-interface AnthropicRequestMessage {
- role: "user" | "assistant";
- content: AnthropicContentBlock[];
-}
-
-interface RecentTransportSummary extends TransportRequestSummary {
- recordedAt: string;
- endpointKind: string;
- metadataSource: string;
- requestInitiator?: string;
-}
-
export function activate(context: vscode.ExtensionContext) {
const goUsageLogChannel = vscode.window.createOutputChannel("OpenCode Go Usage");
context.subscriptions.push(goUsageLogChannel);
- goUsageTracker = new GoUsageTracker(
- context,
- (msg) => {
- goUsageLogChannel.appendLine(`[${new Date().toISOString()}] ${msg}`);
- },
- (modelId) => {
- return modelMetadataSnapshot?.providers[GO_VENDOR]?.[modelId]?.cost;
- },
- "",
- usageTrackerOptions(),
+ setGoUsageTracker(
+ new GoUsageTracker(
+ context,
+ (msg) => {
+ goUsageLogChannel.appendLine(`[${new Date().toISOString()}] ${msg}`);
+ },
+ (modelId) => {
+ return getModelMetadataSnapshot()?.providers[GO_VENDOR]?.[modelId]?.cost;
+ },
+ "",
+ usageTrackerOptions(),
+ ),
);
- _extensionContext = context;
- _usageLogChannel = goUsageLogChannel;
- profilesCache = readProfiles(context);
- activeProfileFingerprint = readActiveProfile(context);
+ setExtensionContext(context);
+ setUsageLogChannel(goUsageLogChannel);
+ setProfilesCache(readProfiles(context));
+ setActiveProfileFingerprint(readActiveProfile(context));
// Eagerly load the tracker for the active profile so the status bar
// has data to display immediately, even before the first request.
@@ -841,7 +147,7 @@ export function activate(context: vscode.ExtensionContext) {
}
// Pull the server-accurate account meters once at startup (TTL-guarded).
void (async () => {
- const apiKey = await context.secrets.get(SECRET_KEY);
+ const apiKey = await context.secrets.get(secretKeyFor(GO_VENDOR));
if (!apiKey) return;
await syncTrackerUsage(getOrCreateTracker(keyFingerprint(apiKey)), apiKey);
})();
@@ -864,7 +170,6 @@ export function activate(context: vscode.ExtensionContext) {
...(zenProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(ZEN_VENDOR, zenProvider)] : []),
vscode.commands.registerCommand("opencodego.manage", () => goProvider.manage()),
vscode.commands.registerCommand("opencodego.diagnostics", () => goProvider.showDiagnostics()),
- vscode.commands.registerCommand("opencodego.setApiKey", () => goProvider.setApiKey()),
vscode.commands.registerCommand("opencodego.refreshModels", () => goProvider.refreshModels()),
vscode.commands.registerCommand("opencodego.toggleProvider", () => toggleProviderEnabled(GO_VENDOR, "OpenCode Go")),
vscode.commands.registerCommand("opencodego.configureUtilityModels", () => configureUtilityModels()),
@@ -976,7 +281,7 @@ export function activate(context: vscode.ExtensionContext) {
});
if (!newLabel || !newLabel.trim()) return;
await renameProfile(extensionContext(), activeProfileFingerprint, newLabel);
- profilesCache = readProfiles(extensionContext());
+ setProfilesCache(readProfiles(extensionContext()));
refreshGoUsageStatusBar();
updateWebviewContent();
vscode.window.showInformationMessage(`Profile renamed to "${newLabel}".`);
@@ -1014,10 +319,10 @@ export function activate(context: vscode.ExtensionContext) {
const remaining = readProfiles(ctx).filter((p) => p.fingerprint !== fp);
await writeProfiles(ctx, remaining);
- profilesCache = remaining;
+ setProfilesCache(remaining);
if (activeProfileFingerprint === fp) {
- activeProfileFingerprint = LEGACY_FINGERPRINT;
+ setActiveProfileFingerprint(LEGACY_FINGERPRINT);
await writeActiveProfile(ctx, LEGACY_FINGERPRINT);
}
@@ -1082,9 +387,9 @@ export function activate(context: vscode.ExtensionContext) {
// immediately (no waiting for the next request or refresh tick).
if (USAGE_DISPLAY_SETTING_KEYS.some((key) => event.affectsConfiguration(`${CONFIG_SECTION}.${key}`))) {
if (event.affectsConfiguration(`${CONFIG_SECTION}.${SETTING_USAGE_CHART_DAYS}`)) {
- usageChartWindowDays = vscode.workspace
- .getConfiguration(CONFIG_SECTION)
- .get(SETTING_USAGE_CHART_DAYS, DEFAULT_USAGE_CHART_DAYS);
+ setUsageChartWindowDays(
+ vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_USAGE_CHART_DAYS, DEFAULT_USAGE_CHART_DAYS),
+ );
}
refreshGoUsageStatusBar();
updateWebviewContent();
@@ -1102,4226 +407,9 @@ export function activate(context: vscode.ExtensionContext) {
chatCompletionsUrl: PROVIDERS[GO_VENDOR].chatCompletionsUrl,
// Same resolution order as the chat path: the active profile's own key
// first (covers multi-profile / BYOK-group setups), then the secret.
- resolveApiKey: async () => profileApiKeys.get(activeProfileFingerprint) ?? _extensionContext?.secrets.get(SECRET_KEY),
- });
-}
-
-async function configureUtilityModels(): Promise {
- await vscode.commands.executeCommand(
- "workbench.action.openSettings",
- "@id:chat.byokUtilityModelDefault @id:chat.utilityModel @id:chat.utilitySmallModel",
- );
-}
-
-/**
- * Toggle whether a provider (`opencodego` / `opencodezen`) is registered at
- * all. Disabling removes the provider from the Language Models list and every
- * model picker — the provider's vendor contribution is gated by the same
- * `when` clause (`config..enabled`) and its runtime registration is
- * skipped. Previously configured BYOK groups and API keys are kept, so
- * re-enabling restores the provider exactly as it was.
- *
- * Provider registration happens at startup, so a window reload is required
- * for the change to take effect.
- */
-async function toggleProviderEnabled(vendor: string, displayName: string): Promise {
- const cfg = vscode.workspace.getConfiguration(vendor);
- const current = cfg.get(SETTING_ENABLED, true);
- const next = !current;
- await cfg.update("enabled", next, vscode.ConfigurationTarget.Global);
-
- const reload = await vscode.window.showInformationMessage(
- next
- ? `${displayName} re-enabled. Reload the window for the provider to appear in Language Models again.`
- : `${displayName} removed from Language Models. Reload the window for it to disappear from the model picker and the manage list. Your API key and group settings are kept.`,
- "Reload Now",
- );
- if (reload === "Reload Now") {
- await vscode.commands.executeCommand("workbench.action.reloadWindow");
- }
-}
-
-/**
- * Whether this VS Code has the modern agent-host BYOK bridge (1.129+).
- *
- * From VS Code 1.129 the Agents window runs in a separate agent host
- * process. Extension-provided BYOK models (isBYOK, no `targetChatSessionType`)
- * are mirrored into agent-host sessions exclusively through the BYOK
- * language-model bridge, which VS Code keeps OFF by default
- * (`chat.agentHost.byokModels.enabled`, experimental). On older versions the
- * extension's own agent-host providers (`targetChatSessionType: "copilotcli"`)
- * are the only path, which is why they stay registered.
- */
-function isModernAgentHostVscode(): boolean {
- const [major = 1, minor = 0] = vscode.version.split(".").map(Number);
- return major > 1 || (major === 1 && minor >= AGENT_HOST_BYOK_MINOR_VERSION);
-}
-
-/**
- * Ensure the VS Code core settings that make OpenCode Go/Zen models usable in
- * the Agents window are enabled (issue #122):
- *
- * 1. `extensions.supportAgentsWindow.` — the only way a code extension is
- * allowed to run in the Agents window (sessions window) process. VS Code
- * disables any extension with a `main` entry there by default, so without
- * this setting the extension's `languageModelChatProviders` vendors are
- * not registered in that window: neither the model picker nor the
- * "+ Add Models" list can show OpenCode Go/Zen.
- * 2. `chat.agentHost.byokModels.enabled` (VS Code 1.129+) — the BYOK
- * language-model bridge that mirrors extension BYOK models into
- * agent-host sessions. Off by default and experimental.
- *
- * CONTRACT:
- * - Only writes the settings while the user keeps `opencodego.agentsWindow`
- * and `opencodego.autoEnableAgentsWindow` on; the settings are merged with
- * existing user values (never clobbering unrelated entries).
- * - Records in globalState which settings the extension flipped itself, so
- * {@link revertAgentsWindowSupport} can restore them when the user disables
- * the Agents feature.
- * - Both settings take effect after a window reload (extension host /
- * agent host restart) — surface an actionable notification the first time
- * anything was changed.
- */
-async function ensureAgentsWindowSupport(context: vscode.ExtensionContext): Promise {
- const opencodeCfg = vscode.workspace.getConfiguration(CONFIG_SECTION);
- if (!opencodeCfg.get(SETTING_AGENTS_WINDOW, true) || !opencodeCfg.get(SETTING_AUTO_ENABLE_AGENTS_WINDOW, true)) {
- return;
- }
-
- let changed = false;
- const extensionCfg = vscode.workspace.getConfiguration("extensions");
- const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {});
- if (!support[EXTENSION_ID]) {
- await extensionCfg.update(SUPPORT_AGENTS_WINDOW_SETTING, { ...support, [EXTENSION_ID]: true }, vscode.ConfigurationTarget.Global);
- await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, true);
- changed = true;
- }
-
- if (isModernAgentHostVscode()) {
- const agentHostCfg = vscode.workspace.getConfiguration("chat.agentHost");
- if (!agentHostCfg.get(AGENT_HOST_BYOK_ENABLED_SETTING, false)) {
- await agentHostCfg.update(AGENT_HOST_BYOK_ENABLED_SETTING, true, vscode.ConfigurationTarget.Global);
- await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, true);
- changed = true;
- }
- }
-
- if (changed) {
- const reload = await vscode.window.showInformationMessage(
- "OpenCode: enabled VS Code's Agents window support so OpenCode Go/Zen models can run in the Agents window. Reload the window for it to take effect.",
- "Reload Now",
- );
- if (reload === "Reload Now") {
- await vscode.commands.executeCommand("workbench.action.reloadWindow");
- }
- }
-}
-
-/**
- * Revert the core settings that {@link ensureAgentsWindowSupport} enabled on
- * this machine (and only those — settings the user configured manually are
- * left untouched).
- */
-async function revertAgentsWindowSupport(context: vscode.ExtensionContext): Promise {
- const extensionCfg = vscode.workspace.getConfiguration("extensions");
- if (context.globalState.get(SUPPORT_AGENTS_WINDOW_STATE_KEY)) {
- const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {});
- if (support[EXTENSION_ID]) {
- const next: Record = Object.fromEntries(Object.entries(support).filter(([id]) => id !== EXTENSION_ID));
- await extensionCfg.update(
- SUPPORT_AGENTS_WINDOW_SETTING,
- Object.keys(next).length > 0 ? next : undefined,
- vscode.ConfigurationTarget.Global,
- );
- }
- await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, undefined);
- }
-
- if (context.globalState.get(AGENTS_BYOK_BRIDGE_STATE_KEY)) {
- await vscode.workspace
- .getConfiguration("chat.agentHost")
- .update(AGENT_HOST_BYOK_ENABLED_SETTING, false, vscode.ConfigurationTarget.Global);
- await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, undefined);
- }
-}
-
-async function warmModelPickerMetadata(): Promise {
- const vendors: string[] = [
- ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(GO_VENDOR), true) ? [GO_VENDOR] : []),
- ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(ZEN_VENDOR), true) ? [ZEN_VENDOR] : []),
- ];
- if (vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true) && vendors.length > 0) {
- vendors.push(AGENT_GO_VENDOR, AGENT_ZEN_VENDOR);
- }
- await Promise.allSettled(vendors.map((v) => vscode.lm.selectChatModels({ vendor: v })));
-}
-
-async function showModelPickerDiagnostics(): Promise {
- const vendors: string[] = [GO_VENDOR, ZEN_VENDOR, "copilot"];
- if (vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true)) {
- vendors.splice(2, 0, AGENT_GO_VENDOR, AGENT_ZEN_VENDOR);
- }
- const sections: string[] = [];
-
- for (const vendor of vendors) {
- const models = await vscode.lm.selectChatModels({ vendor });
- sections.push(`## vendor: ${vendor}`, "", `models: ${String(models.length)}`, "");
- for (const model of models) {
- const internalModel = model as unknown as { configurationSchema?: unknown; detail?: unknown };
- const schema = internalModel.configurationSchema;
- sections.push(
- `### ${model.name}`,
- "",
- `- id: \`${model.id}\``,
- `- family: \`${model.family}\``,
- `- version: \`${model.version}\``,
- `- vendor: \`${model.vendor}\``,
- `- detail: \`${typeof internalModel.detail === "string" ? internalModel.detail : ""}\``,
- `- schema:`,
- "```json",
- JSON.stringify(schema ?? null, null, 2),
- "```",
- "",
- );
- }
- }
-
- const doc = await vscode.workspace.openTextDocument({
- content: ["# OpenCode Model Picker Diagnostics", "", ...sections].join("\n"),
- language: "markdown",
- });
- await vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside);
-}
-
-async function showThinkingEffortPicker(): Promise {
- const families: { label: string; key: keyof ThinkingSettings; options: string[] }[] = [
- { label: "DeepSeek (deepseek-v4-*)", key: "deepseek", options: ["off", "low", "medium", "high", "max"] },
- { label: "GLM (glm-5, glm-5.1, glm-5.2)", key: "glm", options: ["off", "high", "max"] },
- { label: "Kimi (kimi-k2.*)", key: "kimi", options: ["on", "off"] },
- { label: "Mimo (mimo-v2.*)", key: "mimo", options: ["off", "low", "medium", "high"] },
- { label: "MiniMax (minimax-m*)", key: "minimax", options: ["off", "on"] },
- { label: "OpenAI GPT (gpt-*)", key: "openai", options: ["off", "low", "medium", "high", "xhigh"] },
- { label: "Qwen (qwen3.*)", key: "qwen", options: ["auto", "on", "off"] },
- { label: "Qwen Thinking Budget", key: "qwenBudget", options: ["auto", "4096", "16384", "32768", "81920"] },
- ];
- const settings = getSettings().thinking;
- const family = await vscode.window.showQuickPick(
- families.map((f) => ({ label: f.label, description: `current: ${settings[f.key]}`, family: f })),
- { placeHolder: "Pick a model family to configure Thinking" },
- );
- if (!family) return;
- const choice = await vscode.window.showQuickPick(family.family.options, {
- placeHolder: `Set ${family.family.label} → Thinking value`,
+ resolveApiKey: async () => profileApiKeys.get(activeProfileFingerprint) ?? _extensionContext?.secrets.get(secretKeyFor(GO_VENDOR)),
});
- if (!choice) return;
- const cfg = vscode.workspace.getConfiguration(`${CONFIG_SECTION}.thinking`);
- await cfg.update(family.family.key, choice, vscode.ConfigurationTarget.Global);
- vscode.window.showInformationMessage(`OpenCode Thinking — ${family.family.label}: ${choice}`);
}
-
export async function deactivate(): Promise {
// no-op: experimental context indicator hooks removed in 0.1.8
}
-
-function ensureUsageStatusBar(context: vscode.ExtensionContext): vscode.StatusBarItem {
- if (!usageStatusBarItem) {
- usageStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 95);
- context.subscriptions.push(usageStatusBarItem);
- }
-
- resetUsageStatusBar();
- return usageStatusBarItem;
-}
-
-function shouldShowUsageStatusBar(): boolean {
- return vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_SHOW_USAGE_STATUS_BAR, true);
-}
-
-function resetUsageStatusBar(): void {
- if (!usageStatusBarItem) {
- return;
- }
-
- if (!shouldShowUsageStatusBar()) {
- usageStatusBarItem.hide();
- return;
- }
-
- usageStatusBarItem.text = "OpenCode";
- usageStatusBarItem.tooltip = "OpenCode usage summary";
- usageStatusBarItem.show();
-}
-
-function updateUsageStatusBar(providerDisplayName: string, modelId: string, summary: TransportRequestSummary): void {
- if (!usageStatusBarItem) {
- return;
- }
-
- if (!shouldShowUsageStatusBar()) {
- usageStatusBarItem.hide();
- return;
- }
-
- const usage: UsageSnapshot = {
- promptTokens: summary.promptTokens,
- completionTokens: summary.completionTokens,
- totalTokens: summary.totalTokens,
- cachedTokens: summary.cachedTokens,
- finishReason: summary.finishReason,
- };
- const text = formatUsageStatusBarText(providerDisplayName, usage);
-
- usageStatusBarItem.text = text ?? providerDisplayName;
- usageStatusBarItem.tooltip = formatUsageStatusBarTooltip(providerDisplayName, modelId, usage);
- usageStatusBarItem.show();
-}
-
-function ensureGoUsageStatusBar(context: vscode.ExtensionContext): void {
- if (goUsageStatusBarItem) return;
- goUsageStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 94);
- goUsageStatusBarItem.command = "opencodego.showUsageQuickPick";
- context.subscriptions.push(goUsageStatusBarItem);
- refreshGoUsageStatusBar();
-}
-
-function refreshGoUsageStatusBar(): void {
- if (!goUsageStatusBarItem) return;
- const tracker = activeGoUsageTracker();
- if (!tracker) {
- goUsageStatusBarItem.text = "OpenCode Go";
- goUsageStatusBarItem.tooltip = new vscode.MarkdownString("");
- goUsageStatusBarItem.show();
- return;
- }
- const s = tracker.getSummary();
- const activeProfile = findProfile(profilesCache, activeProfileFingerprint);
- const baseText = formatGoUsageStatusBarText(s);
- goUsageStatusBarItem.text = activeProfile && profilesCache.length > 1 ? `${baseText} [${activeProfile.label}]` : baseText;
- goUsageStatusBarItem.tooltip = buildUsageTooltip(s);
- goUsageStatusBarItem.show();
- updateWebviewContent();
-
- // Refresh the server-accurate meters in the background (TTL-guarded); when
- // a new snapshot lands, rebuild the status bar with it. Use the active
- // profile's own key when known, falling back to the extension secret.
- void (async () => {
- const apiKey = profileApiKeys.get(activeProfileFingerprint) ?? (await _extensionContext?.secrets.get(SECRET_KEY));
- if (!apiKey) return;
- const changed = await tracker.syncServerUsage(apiKey);
- if (changed) refreshGoUsageStatusBar();
- })();
-}
-
-/**
- * Fetch server-accurate usage for a key and repaint the status bar when a new
- * snapshot arrived. Uses the tracker owning that key (creating its profile on
- * first use), so multi-account setups keep per-key meters.
- */
-async function syncTrackerUsage(tracker: GoUsageTracker, apiKey: string): Promise {
- const changed = await tracker.syncServerUsage(apiKey);
- if (changed) refreshGoUsageStatusBar();
-}
-
-function showUsageWebview(context: vscode.ExtensionContext): void {
- if (usageWebviewPanel) {
- usageWebviewPanel.reveal(vscode.ViewColumn.Beside);
- return;
- }
-
- usageWebviewPanel = vscode.window.createWebviewPanel("opencodego.usageWebview", "OpenCode Usage", vscode.ViewColumn.Beside, {
- enableScripts: true,
- retainContextWhenHidden: true,
- });
-
- usageWebviewPanel.onDidDispose(
- () => {
- usageWebviewPanel = undefined;
- usageWebviewRendered = false;
- },
- null,
- context.subscriptions,
- );
-
- usageWebviewPanel.webview.onDidReceiveMessage(
- (message: { type?: string }) => {
- switch (message.type) {
- case "refresh":
- refreshGoUsageStatusBar();
- break;
- case "setTargets":
- void vscode.commands.executeCommand("opencodego.setUsageTargets");
- break;
- case "renameProfile":
- void vscode.commands.executeCommand("opencodego.renameActiveProfile");
- break;
- case "window": {
- const days = Number((message as { days?: unknown }).days);
- if (Number.isFinite(days) && days >= 0 && days <= 370) {
- usageChartWindowDays = days;
- updateWebviewContent();
- }
- break;
- }
- }
- },
- null,
- context.subscriptions,
- );
-
- usageWebviewRendered = false;
- updateWebviewContent();
-}
-
-/** Escape a JSON payload for embedding in an HTML
-
-