refactor: split god files into domain modules + data-driven model registry - #155
Conversation
…nai/anthropic/google)
lint.ts spawned the extension-less node_modules/.bin shims, which ENOENTs on Windows; route them through the shell and use the .cmd variant, matching the staged-lint fix. Add .gitattributes enforcing LF normalization so files are checked out with LF even with core.autocrlf=true, keeping prettier and shellcheck green on Windows.
Add an [Unreleased] changelog entry and update the devlog (session entry, Session Handoff, Completed History) for the per-provider thinking strategies, single-config-authority resolution, request module split and Windows lint fixes.
Remove the OpenCode Go/Zen Set API Key commands and the Set/Clear API Key items in Manage Provider; keys are now configured exclusively through VS Codes native BYOK flow (Language Models -> + Add Models). SecretStorage 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, fixing the latent collision where Go and Zen shared a single secret and overwrote each others key. Refresh Models / Test Connection now point at the BYOK flow when no key is configured.
# Conflicts: # src/streaming.ts
There was a problem hiding this comment.
Pull request overview
This PR performs a large-scale refactor of the extension by splitting prior “god files” into domain-focused modules, while also introducing a data-driven MODEL_REGISTRY so model-family routing (endpoint + SDK hint) and thinking-family detection share a single source of truth.
Changes:
- Split usage, transports, provider, models, commands, request-building, and core routing/contract logic into dedicated modules.
- Added
src/core/registry.tsmodel registry and updated routing + thinking-family resolution to consult it. - Refactored thinking into per-family strategy classes (
src/thinking/) and updated request builders to consume those strategies.
Reviewed changes
Copilot reviewed 84 out of 87 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/usage/usageProfile.ts | Updates config imports/exports to new shared config location. |
| src/usage/usage.ts | Fixes formatTokenCount import path after module split. |
| src/usage/pricing.ts | New: centralized Go usage cost estimation with live resolver + bundled fallback table. |
| src/usage/history.ts | New: reads OpenCode CLI SQLite usage history and buckets usage for charts. |
| src/usage/goUsageSync.ts | Updates tracker/config imports for new usage module layout. |
| src/usage/formatting.ts | New: UI formatting helpers for usage status bar + quick pick content. |
| src/transports/thinkTags.ts | New: streaming <think>...</think> stripping filter. |
| src/transports/streamParts.ts | New: shared stream-part helpers incl. thinking part emission plumbing. |
| src/transports/sse.ts | New: pure SSE data: parsing helper. |
| src/transports/responses.ts | New: OpenAI Responses transport adapter. |
| src/transports/google.ts | New: Google/Gemini transport adapter. |
| src/transports/extract.ts | New: response extraction helpers + usage summary accumulation. |
| src/transports/engine.ts | New: shared streaming engine (fetch, retries, SSE loop, summaries). |
| src/transports/chatCompletions.ts | New: OpenAI-compatible chat-completions transport adapter. |
| src/transports/anthropic.ts | New: Anthropic Messages transport adapter. |
| src/thinking/types.ts | New: thinking system shared types (pure). |
| src/thinking/schema.ts | New: schema builders for per-model thinking configuration UI. |
| src/thinking/resolve.ts | New: resolves effective thinking settings with provenance. |
| src/thinking/qwen.ts | New: Qwen thinking strategy implementation. |
| src/thinking/provider.ts | New: thinking provider factory + family mapping via model registry. |
| src/thinking/payload.ts | New: pure payload inspection (bodyRequestsThinking). |
| src/thinking/openai.ts | New: OpenAI GPT thinking strategy implementation. |
| src/thinking/minimax.ts | New: MiniMax thinking strategy implementation. |
| src/thinking/mimo.ts | New: Mimo thinking strategy implementation. |
| src/thinking/kimi.ts | New: Kimi thinking strategy implementation. |
| src/thinking/glm.ts | New: GLM thinking strategy implementation. |
| src/thinking/fallback.ts | New: fallback strategy for unknown families / metadata-only reasoning. |
| src/thinking/deepseek.ts | New: DeepSeek thinking strategy implementation. |
| src/thinking/base.ts | New: shared base class for thinking providers. |
| src/thinking.ts | Converts legacy monolith into a public barrel re-exporting new thinking modules. |
| src/test/visionProxy.test.ts | Updates import paths to new src/models/ layout. |
| src/test/usageProfile.test.ts | Updates dynamic import path to new src/usage/ layout. |
| src/test/registry.test.ts | New: unit tests for registry-based routing + thinking-family lookup. |
| src/test/modelNames.test.ts | Updates import path to src/models/modelNames.ts. |
| src/test/modelLimits.test.ts | Updates import path to src/models/modelLimits.ts. |
| src/test/metadata.test.ts | Updates import path to src/models/metadata.ts. |
| src/test/goUsageTracker.test.ts | Updates imports and module loading to new usage/* files. |
| src/test/goUsageSync.test.ts | Updates import paths to new usage modules. |
| src/test/config.test.ts | Adds tests for per-vendor secret-key resolution. |
| src/request/types.ts | New: shared request/transport payload types (pure). |
| src/request/shared.ts | New: shared request-builder helpers (image detection). |
| src/request/schema.ts | New: sanitizes tool JSON schema for upstream compatibility. |
| src/request/openai.ts | New: request body builders for chat-completions + Responses API. |
| src/request/headers.ts | New: builds OpenCode request/session headers + stable hash utilities. |
| src/request/google.ts | New: Gemini generateContent request body builder. |
| src/request/builders.ts | New: request-builders public barrel for compatibility. |
| src/request/anthropic.ts | New: Anthropic Messages request body builder + message conversion. |
| src/provider/visionProxy.ts | New: vision proxy logic + quick pick configuration UI. |
| src/provider/tokens.ts | New: token estimation + text extraction for VS Code parts. |
| src/provider/settings.ts | New: settings + per-model schema + capabilities/limits resolution. |
| src/provider/messages.ts | New: converts VS Code chat messages into wire messages (incl. images/tools/thinking). |
| src/provider/definitions.ts | New: provider definitions + user agent + transient error classification. |
| src/models/pricing.ts | New: maps models.dev cost to VS Code pricing fields and categories. |
| src/models/modelNames.ts | New: model-id → human-friendly name formatting helpers. |
| src/models/modelLimits.ts | Fixes config import path after refactor. |
| src/models/modelCapabilities.ts | New: stable capabilities builder for marketplace-safe installs. |
| src/models/metadataFetcher.ts | New: models.dev cache fetch/orchestration + globalState persistence. |
| src/models/metadata.ts | Updates config/utils/providerTypes import paths after split. |
| src/core/transport.ts | New: transport contract types shared by all transports. |
| src/core/routing.ts | Refactors routing to consult core/registry.ts instead of hardcoded logic. |
| src/core/registry.ts | New: data-driven model-family registry (endpoint + sdkPackage + thinkingFamily + vendors). |
| src/contextWindowHookBridge.ts | Updates UsageSnapshot import path after usage module split. |
| src/contextWindowHook.ts | Updates UsageSnapshot import path after usage module split. |
| src/config.ts | Adds Zen secret key + secretKeyFor() vendor mapping. |
| src/commands/thinkingPicker.ts | New: command to update global thinking effort settings. |
| src/commands/providers.ts | New: provider-related commands (utility models + enable/disable). |
| src/commands/diagnostics.ts | New: model picker diagnostics dump command. |
| src/commands/agentsWindow.ts | New: agent-window support enablement/warmup utilities. |
| src/chatParts.ts | Updates UsageSnapshot import path after usage module split. |
| scripts/verify-estimate-token-count.ts | Updates import path to new src/models/modelLimits.ts. |
| scripts/validate-models.ts | Updates to new thinking provider strategy + routing module paths. |
| scripts/staged-lint.ts | Windows-safe .cmd shim handling + shell execution for spawnSync. |
| scripts/lint.ts | Windows-safe .cmd shim handling + shell execution for spawnSync. |
| README.md | Updates key-storage documentation and command list for new BYOK flow. |
| package.json | Removes legacy “Set API Key” command registration. |
| docs/architecture/02-20260809-provider-adapter-architecture.md | Updates timeline with completed refactor + registry work. |
| docs/architecture/01-20260514-open-code-provider-architecture.md | Updates architecture notes to reflect removal of legacy key entry and new behavior. |
| CHANGELOG.md | Adds Unreleased notes describing refactor + registry + thinking changes. |
| .gitattributes | New: enforce LF normalization (esp. for Windows tooling consistency). |
Suppressed comments (1)
src/transports/thinkTags.ts:115
- Same as the case: the comment/doc says whitespace after
<think>is trimmed, but the implementation only skips newlines. If the model emits<think>the first visible reasoning character will be a space.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const bucketCount = Math.round((dayStartMs - firstDay) / DAY_MS) + 1; | ||
| const buckets: UsageDayPoint[] = Array.from({ length: bucketCount }, (_, i) => ({ | ||
| dayStart: firstDay + i * DAY_MS, | ||
| cost: 0, | ||
| tokens: 0, | ||
| requests: 0, | ||
| })); | ||
| const byModel = new Map<string, Map<number, ModelDayUsage>>(); | ||
|
|
||
| const add = (model: string | undefined, timestamp: number, cost: number, tokens: number): void => { | ||
| const index = Math.round((timestamp - firstDay) / DAY_MS); | ||
| if (index < 0 || index >= bucketCount) return; |
| // Skip a single leading whitespace after </think> for cleaner output | ||
| if (pos < buffer.length && (buffer[pos] === "\n" || buffer[pos] === "\r")) { | ||
| pos += 1; | ||
| if (pos < buffer.length && buffer[pos] === "\n") { | ||
| pos += 1; | ||
| } | ||
| } |
|
Nice one @xianhongtao, solid refactor. Cross-checked against A few things before merge:
Scope is big (87 files, +9k/-8k) but everything is behavior-preserving and tests are green (305/305). |
# Conflicts: # src/streaming.ts
Split the 1510-line goUsageTracker.ts god file into the usage domain: - usage/tracker.ts — GoUsageTracker class + types + time-window helpers - usage/history.ts — OpenCode CLI SQLite history read/aggregation (pure) - usage/pricing.ts — bundled pricing snapshot + estimateCost (pure) - usage/formatting.ts — status-bar / quick-pick formatting Move usage.ts / usageProfile.ts / goUsageSync.ts into src/usage/ and keep goUsageTracker.ts as a thin barrel re-exporting the historical public API. Behavior-preserving; all importers + tests updated.
…transport.ts Split the 1620-line streaming.ts god file into the transport domain: - core/transport.ts — StreamRequestOptions / TransportRequestSummary types - transports/chatCompletions.ts / responses.ts / anthropic.ts / google.ts — one entry per transport (OpenAI chat / OpenAI Responses / Anthropic Messages / Google GenerateContent) - transports/engine.ts — shared HTTP+SSE streaming engine + retry/backoff - transports/sse.ts — pure SSE data-line parser - transports/extractors.ts — Base/OpenAi/Anthropic response extractors - transports/extract.ts — non-stream extraction + pure delta helpers - transports/streamParts.ts — progress/thinking part emission - transports/thinkTags.ts — pure inline <think> tag stripper streaming.ts is kept as a thin barrel re-exporting the historical public API. Behavior-preserving; verified with npm run test-retry (mock server).
Move healthy single-domain modules into their final homes: - src/models/ — metadata.ts, modelLimits.ts, modelCapabilities.ts, modelNames.ts - src/core/ — routing.ts (transport types already live in core/transport.ts) Update every importer (extension.ts, request/*, thinking/*, transports/*, usage/*, tests, scripts/validate-models.ts, verify-estimate-token-count.ts). Behavior-preserving; pure path churn.
Extract ~1150 lines out of extension.ts: - src/usage/dashboard.ts — usage state, status bar, webview panel (incl. the 583-line HTML template), tooltip SVG builder, tracker/profile plumbing - src/models/metadataFetcher.ts — models.dev metadata cache state + the clear/get/refresh orchestration (owns its own in-memory cache) extension.ts keeps live references via exported getters/setters. Verified: compile + 291 tests + lint green.
…tions.ts Move the provider definition domain out of extension.ts: PROVIDERS table, ProviderDefinition / OpenCodeModel / ModelListEntry / ModelListResponse / ConvertedMessageResult / LanguageModelConfiguration types, plus the getUserAgent / isTransientFetchError helpers. extension.ts now imports from provider/definitions. Behavior-preserving; compile + 291 tests + lint green.
Split the I partition out of extension.ts: - provider/messages.ts — convertMessage (vscode parts → wire format, incl. image normalization, MiMo tool-result flattening, reasoning echo) + normalizeMessages / trimOldImagesFromHistoryInPlace / hasMessagePayload - provider/tokens.ts — messageText / estimateChatMessageTokenCount / partToTokenCount / partToText etc. Behavior-preserving; compile + 291 tests + lint green.
… headers Move the remaining request-path helpers out of extension.ts: - provider/settings.ts — modelConfigurationSchema / getSettings / modelLimits / modelCapabilities / shouldHideDeprecatedModel / resolveRawModelId / resolveVendorFromId / getConfiguredApiKey / isVisionProxyEnabled - provider/visionProxy.ts — proxyVision + showVisionProxyPicker (whole K partition) - models/pricing.ts — modelPricingFields + costCategory - request/headers.ts — buildOpenCodeRequestHeaders + stringifyInitiator etc. extension.ts is now down to ~1400 lines (from 4653). Behavior-preserving; compile + 291 tests + lint green.
…rovider.ts Move the 1100-line provider class out of extension.ts into its own module (one cohesive concern). Also extract the provider command helpers (configureUtilityModels / toggleProviderEnabled) into commands/providers.ts which both the class and activate() import. extension.ts is now ~630 lines (was 4653) and contains only wiring: activate (provider registration + command handlers), the agent-window helpers, model-picker diagnostics and the thinking-effort picker. Behavior-preserving; compile + 291 tests + test-retry + lint green.
…kers Move the remaining command helpers out of extension.ts into commands/: - commands/agentsWindow.ts — ensure/revert agent-window support + warm model-picker metadata - commands/diagnostics.ts — showModelPickerDiagnostics - commands/thinkingPicker.ts — showThinkingEffortPicker extension.ts is now ~440 lines and holds only activate() wiring + deactivate (no business logic). Behavior-preserving; compile + 291 tests + lint green.
…aths
The split is complete — update every importer to reference the canonical
modules directly and delete the two compat barrels:
- src/streaming.ts -> src/transports/* + src/core/transport.ts
- src/goUsageTracker.ts -> src/usage/{tracker,history,pricing,formatting}.ts
No behavior change; compile + 291 tests + lint green.
Add src/core/registry.ts as the single data-driven source of truth for
per-model wiring, per the provider-adapter architecture doc:
MODEL_REGISTRY: model-family rows -> { patterns, endpointKind,
sdkPackage, thinkingFamily, vendors? }
- core/routing.ts resolveModelRouting() now reads the registry (first
match wins, vendor restrictions honored) instead of an if-chain.
- thinking/provider.ts thinkingFamily() reads the registry (vendor-
agnostic) instead of a second hardcoded prefix table.
- ModelEndpointKind type moves to core/registry.ts (re-exported by
provider/definitions.ts).
Adding a new model family = adding ONE row (+ optionally a thinking
strategy class). Context limits/capabilities stay metadata-driven
(live models.dev) — not duplicated in a static table.
Behavior-preserving: identical routing/family outcomes, verified by 14
new registry tests + 305 total + test-retry 7/7 + lint.
99e4b5e to
1ad1f5f
Compare
|
Hey @xianhongtao, thanks for this. I pulled the branch and ran it locally: compile is clean, 310/310 unit tests pass (a bit more than the 305 you listed, the registry work added a few), test-retry is 7/7 on the mock server, and eslint has nothing to complain about. I also diffed the old routing and thinking logic against the registry table line by line and the behavior lines up: GPT to Responses, Claude to Messages, minimax-m2 split by vendor, Gemini only on Zen, everything else chat-completions. The thinking payloads (deepseek effort, qwen budget, minimax adaptive, the kimi-k2.7 force-on) all match what we had before. The structure is a clear win. extension.ts went from 4600+ lines to a thin entry point, transports are split per endpoint, usage is in its own domain folder, and adding a model family as one registry row is much easier to reason about than editing routing and thinking in two places. The tests cover the tricky bits too, like the minimax-m2 vs minimax ordering and the vendor restrictions. One thing I want to flag before we merge, and it's around the API key change. Previously Go and Zen shared the same The thinking refactor also drops the globalState shadow copy in favor of a single config authority. That's a behavior change (a good one, it stops one model's thinking effort leaking onto another) rather than a pure refactor, so it deserves a manual spin on a couple of models before release. I don't think it blocks the merge. One more thing on release timing: given how big this is, I don't plan to ship it right after merging. I want to sit on it for a bit, run it through a longer live session across several models and both vendors, and only cut a release once I'm confident nothing regressed. The merge itself doesn't mean it goes out the door. Merge-wise I'll keep the full commit history (merge commit, no squash) so all 24 commits stay intact. |

📝 What does this change?
Splits the three god files —
src/extension.ts(4653 lines),src/streaming.ts(1620) andsrc/goUsageTracker.ts(1510) — into domain modules (behavior-preserving), and adds a data-driven model registry so per-model wiring (transport + thinking family) lives in one table.src/usage/— Go usage domain:tracker.ts,history.ts(OpenCode CLI SQLite read/aggregation),pricing.ts,formatting.ts,dashboard.ts(status bar + usage webview + tooltip SVG), movedusage.ts/usageProfile.ts/goUsageSync.ts.src/transports/— one file per transport (chatCompletions,responses,anthropic,google) + sharedengine, puresseparser,extractors,extract,streamParts,thinkTags; contract types insrc/core/transport.ts(routing insrc/core/routing.ts).src/provider/—OpenCodeProviderclass,definitions(PROVIDERS + model types),messages/tokens(conversion + token estimation),settings,visionProxy.src/models/—metadata,modelLimits,modelCapabilities,modelNames,pricing,metadataFetcher(models.dev cache).src/commands/— provider / agent-window / diagnostics / thinking-picker handlers;src/request/headers.ts.src/core/registry.ts— data-drivenMODEL_REGISTRY: rows map model-family patterns →{ endpointKind, sdkPackage, thinkingFamily, vendors? };resolveModelRouting()(transport) andthinkingFamily()both read it, so adding a model family = one row (+ optionally a thinking strategy class).extension.tsis now a thin entry (~414 lines) that only wires activation + command registration; the compat barrels (streaming.ts,goUsageTracker.ts) are removed and every importer references canonical paths.🧪 How did you test it?
npm run compile— cleannpm test— 305/305 (incl. 14 newregistry.test.tscases: transport×vendor routing, thinking family, lookup mechanics)npm run test-retry— 7/7 (mock-server streaming/retry E2E)npm run lint— fully green (editorconfig / eslint / markdown / prettier / shell / tsc / tests)npm run package— VSIX builds (105 files, 2.67 MB)✅ Checklist
npm run compilepassesnpm run lintpassesnpm testpassesnpm run packageproduces a VSIX