opencode-cmd-provider 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 - 2026-08-21
4
+
5
+ Fix: `/connect` no longer lists Command Code — the plugin failed to load.
6
+
7
+ - The `cmd_plan_summary` tool runtime-imported `@opencode-ai/plugin`, an
8
+ optional peer dependency that `opencode plugin <pkg>` installs in
9
+ `.opencode/`, not next to the plugin. That import threw `ERR_MODULE_NOT_FOUND`
10
+ at load, silently killing the whole server plugin — no auto-registration and
11
+ no `/connect` entry (Command Code vanished from the provider list).
12
+ - The tool now builds its Zod args from a direct `zod` dependency instead of
13
+ `@opencode-ai/plugin`'s `tool()` helper, so the server bundle has zero
14
+ runtime imports of optional peers.
15
+ - Added a contract test that scans the built `dist/` and fails on any runtime
16
+ `@opencode-ai/*` import, so this class of regression can't ship again.
17
+
18
+ ## 1.2.0 - 2026-08-21
19
+
20
+ Feature: Deals intelligence — per-model pricing/allowance data in a sidebar panel and a plan-summary tool, delivered zero-step.
21
+
22
+ ### Features
23
+
24
+ - New Deals catalog (`src/deals/catalog.ts`): tier, benchmarks, deal discounts (`was`/`now` rates), peak/off-peak windows, and GOAT/Pro monthly allowances for every model, scraped from the Command Code docs.
25
+ - New `cmd_plan_summary` tool (plan-aware allowances and deal rates) and a TUI sidebar "Command Code" section for the selected model.
26
+ - Zero-step delivery: the package exports a `./tui` target (`dist/tui.js`), and `opencode plugin opencode-cmd-provider` writes both `opencode.json(c)` and `tui.json` from one command — no hand-written `tui.json` (fixes #39).
27
+ - Deals intelligence is an excisable slice (`src/deals/`); deleting it plus two lines in the plugin entry leaves Core (models, auth, streaming) unchanged.
28
+ - Visible degradation: when the Deals catalog is empty (scraping mitigated), the sidebar shows a "Deals unavailable" banner with placeholder rows and the tool reports no bundled data — Core is unaffected.
29
+ - `refresh:deals` no longer blocks a release: it falls back to fixtures or an empty catalog with a warning, and the Deals gate in the release workflow is non-blocking.
30
+
31
+ ### Fixes
32
+
33
+ - Streaming usage now reports cache-inclusive input token totals (the AI SDK convention). OpenChamber showed context usage as low as ~0.1% instead of ~5% because the converter reported only non-cached input; `inputTokens.total` now includes cache read/write tokens (issue #36).
34
+
35
+ ### Chores
36
+
37
+ - Catalog refresh to `command-code@1.31.0`, adding the free reasoning model `stealth/ox-alpha` (Ox Alpha).
38
+ - Docs: corrected the install command (`opencode plugin`, no `add`), simplified the README Install section, and added build/test/architecture guidance to `AGENTS.md`.
39
+
3
40
  ## 1.1.1 - 2026-08-20
4
41
 
5
42
  Fix: generate image-input modalities from the Command Code CLI catalog.
package/README.md CHANGED
@@ -9,7 +9,23 @@ A provider and plugin for [OpenCode](https://opencode.ai) that connects to the [
9
9
 
10
10
  ## Install
11
11
 
12
- Add the package to your OpenCode configuration:
12
+ ```sh
13
+ opencode plugin opencode-cmd-provider
14
+ ```
15
+
16
+ Update an existing install:
17
+
18
+ ```sh
19
+ opencode plugin opencode-cmd-provider --force
20
+ ```
21
+
22
+ Install globally (available in every project):
23
+
24
+ ```sh
25
+ opencode plugin opencode-cmd-provider --global
26
+ ```
27
+
28
+ Manual config also works:
13
29
 
14
30
  ```jsonc
15
31
  // opencode.json
@@ -19,28 +35,7 @@ Add the package to your OpenCode configuration:
19
35
  }
20
36
  ```
21
37
 
22
- That's it. The plugin bundles a snapshot of the Command Code model catalog and
23
- auto-registers the `commandcode` provider — npm package, name, API key env var,
24
- and every model — when OpenCode loads. No `provider.commandcode` block, no
25
- `models` map, no network access needed. All Command Code models appear in
26
- `/models` with the `[CMD]` display-name prefix (e.g. `[CMD] Claude Sonnet 5`) so
27
- they aren't confused with same-named models from other providers.
28
-
29
- You can still declare your own `provider.commandcode` entry to customize
30
- behavior; your declarations always win and the snapshot fills in only what's
31
- missing:
32
-
33
- - Declared provider-level settings (`name`, `options`, `baseURL`, `env`) are
34
- kept as you wrote them.
35
- - Declared models stay exactly as you wrote them — the snapshot never modifies
36
- or removes them, and models that left the catalog remain usable.
37
- - `whitelist`/`blacklist` on a declared entry filter the auto-registered models
38
- too, keeping the picker uncluttered.
39
-
40
- The catalog snapshot is refreshed at every release; newly published Command
41
- Code models appear after a plugin update.
42
-
43
- Start or reload OpenCode, then authenticate:
38
+ Then authenticate:
44
39
 
45
40
  ```txt
46
41
  /connect
@@ -101,12 +96,52 @@ Pick a model with `/models`, or run non-interactively:
101
96
  opencode run --model commandcode/claude-sonnet-5 "hello"
102
97
  ```
103
98
 
99
+ ## Deals intelligence
100
+
101
+ Every Command Code model ships with deal/allowance/benchmark intelligence
102
+ (bundled in `src/deals/catalog.ts`), scraped from the Command Code docs. It
103
+ surfaces in two places:
104
+
105
+ - **Sidebar** — in a session, the OpenCode sidebar (`ctrl+x b`) shows a
106
+ `Command Code` section for the selected model: tier, GOAT/Pro allowance,
107
+ benchmark (intelligence, tok/s), deal discounts (`was`/`now` rates), and
108
+ peak/off-peak windows.
109
+ - **`cmd_plan_summary` tool** — plan-aware allowances and deal rates, to
110
+ estimate monthly requests.
111
+
112
+ Delivery is zero-step: the package exports both a `server` and a `tui` target
113
+ (`exports["./tui"]` → `dist/tui.js`), and `opencode plugin
114
+ opencode-cmd-provider` writes both `opencode.json(c)` and `tui.json` from one
115
+ spec. The sidebar is a TUI plugin loaded from `tui.json` — which is why
116
+ installs made before the `./tui` export only wrote the server target and never
117
+ showed a sidebar. Re-run with `--force` to add `tui.json`.
118
+
119
+ When the Deals catalog is empty (scraping mitigated), the feature degrades
120
+ visibly rather than silently: the sidebar shows a `Deals unavailable` banner
121
+ with placeholder rows, and `cmd_plan_summary` reports that no deal data is
122
+ bundled. Core (models, auth, streaming) is unaffected.
123
+
104
124
  ## Model discovery and offline behavior
105
125
 
106
- The plugin ships a snapshot of the Command Code model catalog (`src/catalog/snapshot.ts`)
107
- and auto-registers every model into OpenCode's config at startup. Model
108
- availability changes when the package is updated: the snapshot is regenerated
109
- from the live catalog at every release via `npm run refresh:snapshot`.
126
+ The plugin ships two bundled catalogs and auto-registers every model into
127
+ OpenCode's config at startup, with the `[CMD]` display-name prefix (e.g.
128
+ `[CMD] Claude Sonnet 5`) so they aren't confused with same-named models from
129
+ other providers. Model availability changes when the package is updated. You
130
+ can still declare your own `provider.commandcode` entry; your declarations
131
+ always win and the snapshot fills in only what's missing (`whitelist`/
132
+ `blacklist` on a declared entry filter the auto-registered models too).
133
+
134
+ - Snapshot — `src/catalog/snapshot.ts` (model ids, names, context lengths) plus
135
+ `src/catalog/facts.ts` (reasoning efforts, per-1M-token rates, input
136
+ modalities). Regenerated from the live catalog via `npm run refresh:snapshot`.
137
+ - Deals — `src/deals/catalog.ts` (per-model tier, benchmarks, deal
138
+ discounts, `was`/`now` rates, peak/off-peak windows, and GOAT/Pro monthly
139
+ allowances for every model). Scraped from the Command Code docs
140
+ (`pricing-limits`, `plans/goat`, `plans/pro`; fixtures in
141
+ `tests/fixtures/*.html`) via `npm run refresh:deals` (add `-- --fixtures` to
142
+ regenerate offline from fixtures; without it, fetches live pages).
143
+
144
+ Run `npm run refresh` to regenerate both catalogs at once (offline from fixtures).
110
145
 
111
146
  Auto-registration adds no network latency to OpenCode startup — the snapshot is
112
147
  bundled, and the plugin never contacts the Command Code API to list models. The
@@ -116,11 +151,14 @@ endpoint being reachable.
116
151
  The following environment variables are intended for tests, local mocks, and
117
152
  compatible API endpoints:
118
153
 
119
- | Variable | Purpose |
120
- | ---------------------------- | -------------------------------------- |
121
- | `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
122
- | `COMMANDCODE_FACTS_URL` | Override the bundled `models.md` URL |
123
- | `COMMANDCODE_MODALITIES_URL` | Override the CLI bundle URL |
154
+ | Variable | Purpose |
155
+ | ------------------------------- | -------------------------------------- |
156
+ | `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
157
+ | `COMMANDCODE_FACTS_URL` | Override the bundled `models.md` URL |
158
+ | `COMMANDCODE_MODALITIES_URL` | Override the CLI bundle URL |
159
+ | `COMMANDCODE_DEALS_PRICING_URL` | Override the `pricing-limits` docs URL |
160
+ | `COMMANDCODE_DEALS_GOAT_URL` | Override the `plans/goat` docs URL |
161
+ | `COMMANDCODE_DEALS_PRO_URL` | Override the `plans/pro` docs URL |
124
162
 
125
163
  ### Reasoning support
126
164
 
@@ -155,6 +193,8 @@ npm test
155
193
  npm run format:check
156
194
  ```
157
195
 
196
+ `npm run build` needs [bun](https://bun.sh) on PATH — it compiles `src/deals/tui.tsx` with the solid transform (`scripts/build-tui.ts`), so the TUI panel's JSX props are reactive and the sidebar repaints on mid-session model switches.
197
+
158
198
  The headless end-to-end test runs the real OpenCode CLI against a mock Command Code server through the built package:
159
199
 
160
200
  ```sh
@@ -1,7 +1,7 @@
1
- export declare const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.28.1/dist/bundled/command-code-knowledge/reference/models.md";
2
- export declare const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.28.1/dist/cli.mjs";
3
- export declare const FACTS_PACKAGE_VERSION = "1.28.1";
4
- export declare const FACTS_LAST_REFRESHED = "2026-08-19";
1
+ export declare const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.31.0/dist/bundled/command-code-knowledge/reference/models.md";
2
+ export declare const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.31.0/dist/cli.mjs";
3
+ export declare const FACTS_PACKAGE_VERSION = "1.31.0";
4
+ export declare const FACTS_LAST_REFRESHED = "2026-08-21";
5
5
  export declare const MODEL_EFFORTS: Readonly<Record<string, readonly string[]>>;
6
6
  export declare const MODEL_COSTS: Readonly<Record<string, {
7
7
  input: number;
@@ -4,10 +4,10 @@
4
4
  // command-code npm package's bundled model catalog (models.md), plus input
5
5
  // modalities parsed from its CLI bundle (dist/cli.mjs). Regenerate
6
6
  // with `npm run refresh:snapshot`.
7
- export const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.28.1/dist/bundled/command-code-knowledge/reference/models.md";
8
- export const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.28.1/dist/cli.mjs";
9
- export const FACTS_PACKAGE_VERSION = "1.28.1";
10
- export const FACTS_LAST_REFRESHED = "2026-08-19";
7
+ export const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.31.0/dist/bundled/command-code-knowledge/reference/models.md";
8
+ export const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.31.0/dist/cli.mjs";
9
+ export const FACTS_PACKAGE_VERSION = "1.31.0";
10
+ export const FACTS_LAST_REFRESHED = "2026-08-21";
11
11
  export const MODEL_EFFORTS = {
12
12
  "deepseek/deepseek-v4-pro": ["high", "max"],
13
13
  "deepseek/deepseek-v4-flash": ["high", "max"],
@@ -69,6 +69,7 @@ export const MODEL_COSTS = {
69
69
  "thinkingmachines/inkling": { input: 1, output: 4.05, cacheRead: 0.17, cacheWrite: 0 },
70
70
  "thinkingmachines/inkling-small": { input: 0.5, output: 1.2, cacheRead: 0.1, cacheWrite: 0 },
71
71
  "poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
72
+ "stealth/ox-alpha": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
72
73
  "claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
73
74
  "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
74
75
  "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
@@ -130,6 +131,7 @@ export const MODEL_INPUT_MODALITIES = {
130
131
  "moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
131
132
  "moonshotai/Kimi-K3": ["text", "image"],
132
133
  "sakana/fugu-ultra": ["text", "image"],
134
+ "stealth/ox-alpha": ["text", "image"],
133
135
  "stepfun/Step-3.7-Flash": ["text", "image"],
134
136
  "thinkingmachines/inkling": ["text", "image"],
135
137
  "thinkingmachines/inkling-small": ["text", "image"],
@@ -14,7 +14,7 @@ export const MODEL_SNAPSHOT = [
14
14
  { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 1050000 },
15
15
  { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 1050000 },
16
16
  { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 1050000 },
17
- { id: "gpt-5.5", name: "GPT-5.5", contextLength: 200000 },
17
+ { id: "gpt-5.5", name: "GPT-5.5", contextLength: 400000 },
18
18
  { id: "gpt-5.4", name: "GPT-5.4", contextLength: 400000 },
19
19
  { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 400000 },
20
20
  { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 400000 },
@@ -54,6 +54,7 @@ export const MODEL_SNAPSHOT = [
54
54
  { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1000000 },
55
55
  { id: "thinkingmachines/inkling", name: "Inkling", contextLength: 256000 },
56
56
  { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1000000 },
57
+ { id: "stealth/ox-alpha", name: "Ox Alpha", contextLength: 1048576 },
57
58
  { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256000 },
58
59
  { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
59
60
  { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
@@ -0,0 +1,55 @@
1
+ export type PlanId = "go" | "goat" | "pro" | "max" | "max20" | "teampro" | "provider";
2
+ export interface DealRates {
3
+ input: number;
4
+ output: number;
5
+ cacheRead: number;
6
+ cacheWrite: number;
7
+ }
8
+ export interface PlanInfo {
9
+ price: number;
10
+ credits: number;
11
+ window5h: number;
12
+ windowWeek: number;
13
+ /** Human-facing plan name as shown on the Command Code pricing page. */
14
+ display: string;
15
+ }
16
+ export interface ModelDeals {
17
+ /** $/month credit allowance per plan. Absent plan = no data. */
18
+ allowance?: Partial<Record<PlanId, number>>;
19
+ /** Deal metadata. endsAt: ISO date or "while capacity lasts". */
20
+ discount?: {
21
+ pct: number;
22
+ endsAt?: string;
23
+ };
24
+ /** Pre-deal list prices (for reference). */
25
+ was?: {
26
+ input: number;
27
+ output: number;
28
+ cacheRead: number;
29
+ };
30
+ /** Discounted deal prices (what you pay now). */
31
+ now?: {
32
+ input: number;
33
+ output: number;
34
+ cacheRead: number;
35
+ };
36
+ /** Time-varying rates (DeepSeek V4 peak/off-peak). */
37
+ peakOffPeak?: {
38
+ peak: DealRates;
39
+ offPeak: DealRates;
40
+ windows: string;
41
+ };
42
+ /** Higher-context tier rates (docs: MiniMax M3 >512K). */
43
+ overContext?: DealRates;
44
+ benchmark?: {
45
+ intelligence?: number;
46
+ tokPerSec?: number;
47
+ };
48
+ tier?: "opensource" | "premium";
49
+ free: boolean;
50
+ }
51
+ export declare const MODEL_DEALS: Readonly<Record<string, ModelDeals>>;
52
+ export declare const PLAN_CATALOG: Readonly<Record<PlanId, PlanInfo>>;
53
+ export declare const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
54
+ export declare const DEAL_LAST_REFRESHED = "2026-08-21";
55
+ export declare const DEAL_PACKAGE_VERSION = "docs";
@@ -0,0 +1,76 @@
1
+ // src/deals/catalog.ts — GENERATED by scripts/refresh-deals.mjs. Do not edit.
2
+ //
3
+ // Deal/allowance/benchmark intelligence parsed from the Command Code docs
4
+ // site (pricing-limits, plans/goat, plans/pro). Bundled so the plugin can
5
+ // enrich the model picker, the sidebar panel, and the plan summary tool
6
+ // without network access at runtime. Regenerate with `npm run refresh:deals`.
7
+ export const MODEL_DEALS = {
8
+ "claude-fable-5": { tier: "premium", benchmark: { "intelligence": 62.1, "tokPerSec": 67.5 }, free: false },
9
+ "claude-haiku-4-5-20251001": { tier: "premium", benchmark: { "intelligence": 24.1, "tokPerSec": 101.1 }, free: false, allowance: { "pro": 20 } },
10
+ "claude-opus-4-7": { tier: "premium", benchmark: { "intelligence": 55 }, free: false },
11
+ "claude-opus-4-8": { tier: "premium", benchmark: { "intelligence": 57.3 }, free: false },
12
+ "claude-opus-5": { tier: "premium", benchmark: { "intelligence": 63.1, "tokPerSec": 54.4 }, free: false },
13
+ "claude-sonnet-4-6": { tier: "premium", benchmark: { "intelligence": 48.4 }, free: false, allowance: { "pro": 20 } },
14
+ "claude-sonnet-5": { tier: "premium", benchmark: { "intelligence": 55.3, "tokPerSec": 81.9 }, free: false, allowance: { "pro": 20 } },
15
+ "deepseek/deepseek-v4-flash": { tier: "opensource", benchmark: { "intelligence": 51.8, "tokPerSec": 114.6 }, peakOffPeak: { "peak": { "input": 0.44, "output": 1.32, "cacheRead": 0.014, "cacheWrite": 0 }, "offPeak": { "input": 0.22, "output": 0.66, "cacheRead": 0.007, "cacheWrite": 0 }, "windows": "01–04 & 06–10 UTC" }, free: false, allowance: { "goat": 60, "pro": 70 } },
16
+ "deepseek/deepseek-v4-pro": { tier: "opensource", benchmark: { "intelligence": 53.2, "tokPerSec": 75.3 }, peakOffPeak: { "peak": { "input": 1.32, "output": 3.96, "cacheRead": 0.044, "cacheWrite": 0 }, "offPeak": { "input": 0.66, "output": 1.98, "cacheRead": 0.022, "cacheWrite": 0 }, "windows": "01–04 & 06–10 UTC" }, free: false, allowance: { "goat": 20, "pro": 30 } },
17
+ "google/gemini-3.1-flash-lite": { tier: "premium", benchmark: { "intelligence": 25.6 }, free: false, allowance: { "pro": 20 } },
18
+ "google/gemini-3.5-flash": { tier: "premium", benchmark: { "intelligence": 52 }, free: false, allowance: { "pro": 20 } },
19
+ "google/gemini-3.5-flash-lite": { tier: "premium", benchmark: { "intelligence": 37.4, "tokPerSec": 361.2 }, free: false, allowance: { "pro": 20 } },
20
+ "google/gemini-3.6-flash": { tier: "premium", benchmark: { "intelligence": 51.6, "tokPerSec": 187.4 }, free: false, allowance: { "pro": 20 } },
21
+ "google/gemini-3.7-flash": { tier: "opensource", discount: { "pct": 50, "endsAt": "2026-12-31" }, was: { "input": 1.5, "output": 7.5, "cacheRead": 0.15 }, now: { "input": 0.75, "output": 3.75, "cacheRead": 0.075 }, benchmark: { "intelligence": 56, "tokPerSec": 339.4 }, free: false, allowance: { "goat": 40, "pro": 60 } },
22
+ "gpt-5.3-codex": { tier: "premium", benchmark: { "intelligence": 45.5, "tokPerSec": 132.3 }, free: false, allowance: { "pro": 20 } },
23
+ "gpt-5.4": { tier: "premium", benchmark: { "intelligence": 53.1 }, free: false, allowance: { "pro": 20 } },
24
+ "gpt-5.4-mini": { tier: "premium", benchmark: { "intelligence": 40.9 }, free: false, allowance: { "pro": 20 } },
25
+ "gpt-5.5": { tier: "premium", benchmark: { "intelligence": 56.3 }, free: false, allowance: { "pro": 20 } },
26
+ "gpt-5.6-luna": { tier: "opensource", benchmark: { "intelligence": 52.3, "tokPerSec": 165 }, overContext: { "input": 0.4, "output": 1.8, "cacheRead": 0.04, "cacheWrite": 0.5 }, free: false, allowance: { "goat": 20, "pro": 30 } },
27
+ "gpt-5.6-sol": { tier: "opensource", benchmark: { "intelligence": 60.9, "tokPerSec": 68.7 }, overContext: { "input": 10, "output": 45, "cacheRead": 1, "cacheWrite": 12.5 }, free: false, allowance: { "goat": 70, "pro": 80 } },
28
+ "gpt-5.6-terra": { tier: "premium", benchmark: { "intelligence": 56.6, "tokPerSec": 117.1 }, overContext: { "input": 4, "output": 18, "cacheRead": 0.4, "cacheWrite": 5 }, free: false, allowance: { "pro": 20 } },
29
+ "meta/muse-spark-1.1": { tier: "premium", benchmark: { "intelligence": 53.2 }, free: false, allowance: { "pro": 20 } },
30
+ "meta/muse-spark-1.2": { tier: "opensource", benchmark: { "intelligence": 56.8 }, free: false, allowance: { "goat": 20, "pro": 30 } },
31
+ "meta/muse-spark-1.2-contributor": { tier: "opensource", benchmark: { "intelligence": 56.8 }, free: false, allowance: { "goat": 20, "pro": 30 } },
32
+ "MiniMaxAI/MiniMax-M2.5": { tier: "opensource", benchmark: { "intelligence": 34.5 }, free: false },
33
+ "MiniMaxAI/MiniMax-M2.7": { tier: "opensource", benchmark: { "intelligence": 38.9 }, free: false },
34
+ "MiniMaxAI/MiniMax-M3": { tier: "opensource", discount: { "pct": 50 }, was: { "input": 0.6, "output": 2.4, "cacheRead": 0.12 }, now: { "input": 0.3, "output": 1.2, "cacheRead": 0.06 }, benchmark: { "intelligence": 45.4, "tokPerSec": 104.4 }, free: false, allowance: { "goat": 47, "pro": 57 } },
35
+ "moonshotai/Kimi-K2.5": { tier: "opensource", benchmark: { "intelligence": 36 }, free: false },
36
+ "moonshotai/Kimi-K2.6": { tier: "opensource", benchmark: { "intelligence": 45.1 }, free: false },
37
+ "moonshotai/Kimi-K2.7-Code": { tier: "opensource", benchmark: { "intelligence": 43, "tokPerSec": 38.2 }, free: false, allowance: { "goat": 60, "pro": 70 } },
38
+ "moonshotai/Kimi-K2.7-Code-Highspeed": { tier: "opensource", free: false, allowance: { "goat": 20, "pro": 30 } },
39
+ "moonshotai/Kimi-K3": { tier: "opensource", benchmark: { "intelligence": 59.7, "tokPerSec": 38.6 }, free: false, allowance: { "goat": 20, "pro": 30 } },
40
+ "nvidia/nemotron-3-ultra-550b-a55b": { tier: "opensource", benchmark: { "intelligence": 38.3, "tokPerSec": 135.8 }, free: false, allowance: { "goat": 20, "pro": 30 } },
41
+ "poolside/laguna-s-2.1-free": { tier: "opensource", free: true },
42
+ "Qwen/Qwen3.6-Max-Preview": { tier: "opensource", benchmark: { "intelligence": 41.1 }, free: false },
43
+ "Qwen/Qwen3.6-Plus": { tier: "opensource", benchmark: { "intelligence": 40.5 }, overContext: { "input": 2, "output": 6, "cacheRead": 0.2, "cacheWrite": 0 }, free: false, allowance: { "goat": 33, "pro": 43 } },
44
+ "Qwen/Qwen3.7-Flash": { tier: "opensource", overContext: { "input": 0.2, "output": 0.8, "cacheRead": 0.04, "cacheWrite": 0.25 }, free: false },
45
+ "Qwen/Qwen3.7-Max": { tier: "opensource", benchmark: { "intelligence": 46.7 }, free: false, allowance: { "goat": 33, "pro": 43 } },
46
+ "Qwen/Qwen3.7-Plus": { tier: "opensource", benchmark: { "intelligence": 39.4, "tokPerSec": 55.6 }, overContext: { "input": 1.2, "output": 4.8, "cacheRead": 0.24, "cacheWrite": 1.5 }, free: false, allowance: { "goat": 33, "pro": 43 } },
47
+ "Qwen/Qwen3.8-27B": { tier: "opensource", benchmark: { "intelligence": 52 }, free: false, allowance: { "goat": 70, "pro": 80 } },
48
+ "Qwen/Qwen3.8-Max": { tier: "opensource", benchmark: { "intelligence": 58.1, "tokPerSec": 46.7 }, free: false, allowance: { "goat": 20, "pro": 30 } },
49
+ "sakana/fugu-ultra": { tier: "premium", free: false },
50
+ "stepfun/Step-3.5-Flash": { tier: "opensource", benchmark: { "intelligence": 26.5 }, free: false, allowance: { "goat": 20, "pro": 30 } },
51
+ "stepfun/Step-3.7-Flash": { tier: "opensource", benchmark: { "intelligence": 30.9, "tokPerSec": 120.1 }, free: false, allowance: { "goat": 20, "pro": 30 } },
52
+ "tencent/hy3-paid": { tier: "opensource", benchmark: { "intelligence": 42.2, "tokPerSec": 74.8 }, free: false, allowance: { "goat": 70, "pro": 80 } },
53
+ "thinkingmachines/inkling": { tier: "opensource", benchmark: { "intelligence": 42.3, "tokPerSec": 69.8 }, free: false, allowance: { "goat": 20, "pro": 30 } },
54
+ "thinkingmachines/inkling-small": { tier: "opensource", benchmark: { "intelligence": 41.2, "tokPerSec": 61.3 }, free: false, allowance: { "goat": 20, "pro": 30 } },
55
+ "xai/grok-4.5": { tier: "opensource", benchmark: { "intelligence": 55.8, "tokPerSec": 51 }, free: false, allowance: { "goat": 20, "pro": 30 } },
56
+ "xai/grok-4.6": { tier: "opensource", benchmark: { "intelligence": 60.9, "tokPerSec": 66 }, overContext: { "input": 4, "output": 12, "cacheRead": 1, "cacheWrite": 0 }, free: false, allowance: { "goat": 20, "pro": 30 } },
57
+ "xiaomi/mimo-v2.5": { tier: "opensource", discount: { "pct": 98 }, was: { "input": 0.8, "output": 4, "cacheRead": 0.16 }, now: { "input": 0.14, "output": 0.28, "cacheRead": 0.0028 }, benchmark: { "intelligence": 38, "tokPerSec": 61.8 }, free: false, allowance: { "goat": 30, "pro": 40 } },
58
+ "xiaomi/mimo-v2.5-pro": { tier: "opensource", discount: { "pct": 99 }, was: { "input": 2, "output": 6, "cacheRead": 0.4 }, now: { "input": 0.435, "output": 0.87, "cacheRead": 0.0036 }, benchmark: { "intelligence": 42.9, "tokPerSec": 58.1 }, free: false, allowance: { "goat": 20, "pro": 30 } },
59
+ "zai-org/GLM-5": { tier: "opensource", benchmark: { "intelligence": 40.6 }, free: false },
60
+ "zai-org/GLM-5.1": { tier: "opensource", benchmark: { "intelligence": 41, "tokPerSec": 28.2 }, free: false },
61
+ "zai-org/GLM-5.2": { tier: "opensource", benchmark: { "intelligence": 52.6, "tokPerSec": 69 }, free: false, allowance: { "goat": 70, "pro": 80 } },
62
+ "zai-org/GLM-5.2-Fast": { tier: "opensource", free: false, allowance: { "goat": 20, "pro": 30 } },
63
+ "zai-org/GLM-5.3": { tier: "opensource", benchmark: { "intelligence": 59.5, "tokPerSec": 79.6 }, free: false, allowance: { "goat": 20, "pro": 30 } },
64
+ };
65
+ export const PLAN_CATALOG = {
66
+ go: { price: 1, credits: 10, window5h: 3, windowWeek: 6, display: "Go" },
67
+ goat: { price: 10, credits: 70, window5h: 14, windowWeek: 35, display: "GOAT" },
68
+ pro: { price: 20, credits: 80, window5h: 16, windowWeek: 40, display: "Pro" },
69
+ max: { price: 100, credits: 150, window5h: 45, windowWeek: 90, display: "Max 10×" },
70
+ max20: { price: 200, credits: 300, window5h: 90, windowWeek: 180, display: "Max 20×" },
71
+ teampro: { price: 40, credits: 40, window5h: 12, windowWeek: 24, display: "Team Pro" },
72
+ provider: { price: 15, credits: 0, window5h: 0, windowWeek: 0, display: "Provider" },
73
+ };
74
+ export const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
75
+ export const DEAL_LAST_REFRESHED = "2026-08-21";
76
+ export const DEAL_PACKAGE_VERSION = "docs";
@@ -0,0 +1,4 @@
1
+ import type { Config } from "@opencode-ai/sdk/v2";
2
+ import { type ModelDeals } from "./catalog.js";
3
+ export declare function enrichCommandCodeModels(config: Config, deals?: Readonly<Record<string, ModelDeals>>): void;
4
+ export declare function buildCmdOptions(deals: ModelDeals): Record<string, unknown>;
@@ -0,0 +1,63 @@
1
+ import { MODEL_DEALS } from "./catalog.js";
2
+ import { vendorFamilyForModel } from "./vendor.js";
3
+ export function enrichCommandCodeModels(config, deals = MODEL_DEALS) {
4
+ const provider = config.provider?.["commandcode"];
5
+ if (!provider?.models)
6
+ return;
7
+ const isEmpty = Object.keys(deals).length === 0;
8
+ for (const [modelId, model] of Object.entries(provider.models)) {
9
+ if (!model)
10
+ continue;
11
+ if (model.family === undefined) {
12
+ const family = vendorFamilyForModel(modelId);
13
+ if (family !== undefined)
14
+ model.family = family;
15
+ }
16
+ const entry = deals[modelId];
17
+ if (!entry) {
18
+ if (isEmpty && model.options?.["cmd"] === undefined) {
19
+ model.options ??= {};
20
+ model.options["cmd"] = { unavailable: true };
21
+ }
22
+ continue;
23
+ }
24
+ if (model.options?.["cmd"] === undefined) {
25
+ model.options ??= {};
26
+ model.options["cmd"] = buildCmdOptions(entry);
27
+ }
28
+ if (entry.overContext && model.cost?.["context_over_200k"] === undefined) {
29
+ const c = entry.overContext;
30
+ // Only the higher-context tier is carried here; the SDK `cost` type
31
+ // requires base input/output, so the unset branch is cast.
32
+ model.cost ??= {};
33
+ model.cost["context_over_200k"] = {
34
+ input: c.input,
35
+ output: c.output,
36
+ cache_read: c.cacheRead,
37
+ cache_write: c.cacheWrite,
38
+ };
39
+ }
40
+ }
41
+ }
42
+ export function buildCmdOptions(deals) {
43
+ const out = {};
44
+ if (deals.tier !== undefined)
45
+ out.tier = deals.tier;
46
+ if (deals.allowance !== undefined && Object.keys(deals.allowance).length > 0) {
47
+ out.allowance = deals.allowance;
48
+ }
49
+ if (deals.discount !== undefined)
50
+ out.discount = deals.discount;
51
+ if (deals.was !== undefined)
52
+ out.was = deals.was;
53
+ if (deals.now !== undefined)
54
+ out.now = deals.now;
55
+ if (deals.benchmark !== undefined)
56
+ out.benchmark = deals.benchmark;
57
+ if (deals.peakOffPeak !== undefined)
58
+ out.peakOffPeak = deals.peakOffPeak;
59
+ if (deals.overContext !== undefined)
60
+ out.overContext = deals.overContext;
61
+ out.free = deals.free;
62
+ return out;
63
+ }
@@ -0,0 +1,5 @@
1
+ export { MODEL_DEALS, PLAN_CATALOG, DEAL_SOURCE_URL, DEAL_LAST_REFRESHED, DEAL_PACKAGE_VERSION, } from "./catalog.js";
2
+ export type { ModelDeals, PlanId, PlanInfo, DealRates } from "./catalog.js";
3
+ export { vendorFamilyForModel } from "./vendor.js";
4
+ export { enrichCommandCodeModels, buildCmdOptions } from "./enrichment.js";
5
+ export { planSummaryTool, renderPlanSummary, resolvePlan, normalizePlan } from "./plan-summary.js";
@@ -0,0 +1,16 @@
1
+ // src/deals/index.ts — server-side public entry for Deals intelligence
2
+ // (catalog + enrichment + cmd_plan_summary tool). The TUI panel is a separate
3
+ // host and is reached via tui.ts → src/deals/tui.tsx, never from the server
4
+ // barrel, so the provider process does not load solid-js/@opentui. Deleting
5
+ // this folder plus the two registration lines in src/plugin/index.ts leaves
6
+ // Core (Snapshot, Auto-registration, provider/* streaming) byte-identical.
7
+ // Catalog
8
+ export { MODEL_DEALS, PLAN_CATALOG, DEAL_SOURCE_URL, DEAL_LAST_REFRESHED, DEAL_PACKAGE_VERSION, } from "./catalog.js";
9
+ // Vendor (used by enrichment, but exported for tests that import vendor directly
10
+ // from the deals deep module — keep as part of the slice so tests don't reach
11
+ // into src/plugin)
12
+ export { vendorFamilyForModel } from "./vendor.js";
13
+ // Enrichment (family + options.cmd + context_over_200k cost)
14
+ export { enrichCommandCodeModels, buildCmdOptions } from "./enrichment.js";
15
+ // Tool
16
+ export { planSummaryTool, renderPlanSummary, resolvePlan, normalizePlan } from "./plan-summary.js";
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { type ModelDeals, type PlanId, type PlanInfo } from "./catalog.js";
3
+ export declare function normalizePlan(value: unknown): PlanId | undefined;
4
+ export declare function resolvePlan(planArg: string | undefined, env?: NodeJS.ProcessEnv): Promise<PlanId>;
5
+ export declare function renderPlanSummary(plan: PlanId, deals?: Readonly<Record<string, ModelDeals>>, catalog?: Readonly<Record<PlanId, PlanInfo>>): string;
6
+ export declare function planSummaryTool(): {
7
+ description: string;
8
+ args: {
9
+ plan: z.ZodOptional<z.ZodString>;
10
+ };
11
+ execute: (args: {
12
+ plan?: string;
13
+ }) => Promise<string>;
14
+ };
@@ -0,0 +1,160 @@
1
+ // src/deals/plan-summary.ts — cmd_plan_summary tool: plan-aware allowance
2
+ // breakdown. Plan resolution: tool arg → COMMANDCODE_PLAN → live /alpha/whoami
3
+ // (when a key is present and the network works) → default "go". Rendering is a
4
+ // pure function so tests never touch the network.
5
+ import { z } from "zod";
6
+ import { MODEL_COSTS } from "../catalog/facts.js";
7
+ import { MODEL_DEALS, PLAN_CATALOG, } from "./catalog.js";
8
+ import { getApiBase } from "../env.js";
9
+ const PLAN_DISPLAY = {
10
+ go: "Go",
11
+ goat: "GOAT",
12
+ pro: "Pro",
13
+ max: "Max 10×",
14
+ max20: "Max 20×",
15
+ teampro: "Team Pro",
16
+ provider: "Provider",
17
+ };
18
+ const PLAN_ALIASES = {
19
+ go: "go",
20
+ "individual-go": "go",
21
+ goat: "goat",
22
+ "individual-goat": "goat",
23
+ pro: "pro",
24
+ "individual-pro": "pro",
25
+ "individual-pro-v1": "pro",
26
+ max: "max",
27
+ max10: "max",
28
+ "max-10x": "max",
29
+ "max 10x": "max",
30
+ "individual-max": "max",
31
+ max20: "max20",
32
+ "max-20x": "max20",
33
+ "max 20x": "max20",
34
+ "individual-ultra": "max20",
35
+ ultra: "max20",
36
+ teampro: "teampro",
37
+ "team-pro": "teampro",
38
+ "team pro": "teampro",
39
+ provider: "provider",
40
+ "individual-provider": "provider",
41
+ };
42
+ export function normalizePlan(value) {
43
+ if (typeof value !== "string")
44
+ return undefined;
45
+ return PLAN_ALIASES[value.toLowerCase()];
46
+ }
47
+ export async function resolvePlan(planArg, env = process.env) {
48
+ const fromArg = normalizePlan(planArg);
49
+ if (fromArg)
50
+ return fromArg;
51
+ const fromEnv = normalizePlan(env.COMMANDCODE_PLAN);
52
+ if (fromEnv)
53
+ return fromEnv;
54
+ if (env.COMMANDCODE_API_KEY) {
55
+ try {
56
+ const response = await fetch(`${getApiBase(env)}/alpha/whoami`, {
57
+ headers: { authorization: `Bearer ${env.COMMANDCODE_API_KEY}` },
58
+ signal: AbortSignal.timeout(5000),
59
+ });
60
+ if (response.ok) {
61
+ const body = (await response.json());
62
+ const plan = normalizePlan(body.planId ?? body.plan?.id);
63
+ if (plan)
64
+ return plan;
65
+ }
66
+ }
67
+ catch {
68
+ // offline or unreachable — fall through to the default
69
+ }
70
+ }
71
+ return "go";
72
+ }
73
+ const REQUEST_PROFILE = { input: 800, output: 200, cacheRead: 50_000 };
74
+ export function renderPlanSummary(plan, deals = MODEL_DEALS, catalog = PLAN_CATALOG) {
75
+ const info = catalog[plan];
76
+ const lines = [];
77
+ lines.push(`# Command Code plan: ${PLAN_DISPLAY[plan]}`);
78
+ if (plan === "provider") {
79
+ lines.push("pay-as-you-go at model API rates — no monthly allowances or window caps.");
80
+ }
81
+ else if (info) {
82
+ lines.push(`$${info.price}/mo buys $${info.credits} of credits; 5-hour window $${info.window5h}, weekly window $${info.windowWeek}.`);
83
+ if (plan !== "goat" && plan !== "pro") {
84
+ lines.push("This plan has no per-model allowances — active deals apply to your full credit balance at the discounted rates below.");
85
+ }
86
+ }
87
+ const rows = Object.entries(deals).filter(([, d]) => d.allowance?.[plan] !== undefined);
88
+ const freeRows = Object.entries(deals).filter(([id, d]) => d.free && !d.allowance?.[plan]);
89
+ const hasAllowances = plan === "goat" || plan === "pro";
90
+ const dealRows = hasAllowances
91
+ ? []
92
+ : Object.entries(deals).filter(([, d]) => d.discount || d.peakOffPeak);
93
+ if (rows.length === 0 && freeRows.length === 0 && dealRows.length === 0) {
94
+ lines.push("No deal data is bundled for this plan.");
95
+ lines.push("See https://commandcode.ai/docs/resources/pricing-limits for the live table.");
96
+ return lines.join("\n");
97
+ }
98
+ lines.push("");
99
+ if (hasAllowances) {
100
+ lines.push("| Model | $/mo allowance | ~requests/mo | Deal |");
101
+ lines.push("| --- | --- | --- | --- |");
102
+ }
103
+ else {
104
+ lines.push("| Model | Deal | Rates |");
105
+ lines.push("| --- | --- | --- |");
106
+ }
107
+ for (const [id, d] of [...rows, ...freeRows, ...dealRows]) {
108
+ const safeId = id.replace(/[|`]/g, " ");
109
+ const dealBits = [];
110
+ if (d.free)
111
+ dealBits.push("FREE");
112
+ if (d.discount)
113
+ dealBits.push(`${d.discount.pct}% off${d.discount.endsAt ? ` until ${d.discount.endsAt}` : ""}`);
114
+ if (d.peakOffPeak)
115
+ dealBits.push(`peak/off-peak (${d.peakOffPeak.windows})`);
116
+ const dealText = dealBits.join("; ") || "—";
117
+ if (hasAllowances) {
118
+ const allowance = d.allowance?.[plan];
119
+ const estimate = allowance && d.free === false
120
+ ? estimateMonthlyRequests(id, allowance).toLocaleString("en-US")
121
+ : "—";
122
+ lines.push(`| \`${safeId}\` | ${allowance !== undefined ? `$${allowance}` : "free"} | ${estimate} | ${dealText} |`);
123
+ }
124
+ else {
125
+ const rates = d.discount && d.was
126
+ ? `was $${d.was.input}/$${d.was.output} in/out`
127
+ : d.peakOffPeak
128
+ ? `$${d.peakOffPeak.peak.input}/$${d.peakOffPeak.peak.output} peak`
129
+ : "—";
130
+ lines.push(`| \`${safeId}\` | ${dealText} | ${rates} |`);
131
+ }
132
+ }
133
+ lines.push("");
134
+ lines.push("Estimates assume ~800 fresh input + 50K cache-read + 200 output tokens per request.");
135
+ lines.push("Source: https://commandcode.ai/docs/resources/pricing-limits");
136
+ return lines.join("\n");
137
+ }
138
+ function estimateMonthlyRequests(modelId, allowance) {
139
+ const cost = MODEL_COSTS[modelId];
140
+ // No cost row bundled (e.g. not yet in the snapshot): the allowance is the
141
+ // best estimate we have, so report it directly as a placeholder.
142
+ if (!cost)
143
+ return Math.round(allowance);
144
+ const perRequest = (REQUEST_PROFILE.input * cost.input +
145
+ REQUEST_PROFILE.output * cost.output +
146
+ REQUEST_PROFILE.cacheRead * cost.cacheRead) /
147
+ 1_000_000;
148
+ if (perRequest <= 0)
149
+ return Math.round(allowance);
150
+ return Math.floor(allowance / perRequest + 1e-9);
151
+ }
152
+ export function planSummaryTool() {
153
+ return {
154
+ description: "Show the Command Code plan's credits, usage windows, per-model monthly allowances (GOAT/Pro) or active deals (other plans), with estimated monthly request counts. Set COMMANDCODE_PLAN (go|goat|pro|max|max20|teampro|provider) to pin the plan without network access.",
155
+ args: {
156
+ plan: z.string().optional().describe("go|goat|pro|max|max20|teampro|provider"),
157
+ },
158
+ execute: async (args) => renderPlanSummary(await resolvePlan(args.plan)),
159
+ };
160
+ }
@@ -0,0 +1,10 @@
1
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ export declare function dealsRows(model: {
3
+ options?: {
4
+ cmd?: Record<string, unknown>;
5
+ };
6
+ } | undefined): Array<[string, string]>;
7
+ declare const plugin: TuiPluginModule & {
8
+ id: string;
9
+ };
10
+ export default plugin;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
2
+ /** @jsxImportSource @opentui/solid */
3
+ // src/deals/tui.tsx — TUI plugin: "Command Code" deals section in the session
4
+ // sidebar (sidebar_content slot). Renders deal details from the picked model's
5
+ // enriched options.cmd (produced by the server plugin's config hook). Renders
6
+ // nothing when the model has no deals data — zero sidebar noise.
7
+ import { For, Show, createMemo } from "solid-js";
8
+ import { DEAL_SOURCE_URL, PLAN_CATALOG } from "./catalog.js";
9
+ function planDisplay(plan) {
10
+ return PLAN_CATALOG[plan]?.display ?? plan;
11
+ }
12
+ const TIER_DISPLAY = {
13
+ opensource: "Open Source",
14
+ premium: "Premium",
15
+ };
16
+ function tierDisplay(tier) {
17
+ return TIER_DISPLAY[tier] ?? tier;
18
+ }
19
+ function rateString(rates) {
20
+ if (typeof rates.input !== "number" || typeof rates.output !== "number")
21
+ return undefined;
22
+ return `$${rates.input}/$${rates.output} in/out`;
23
+ }
24
+ export function dealsRows(model) {
25
+ const cmd = model?.options?.cmd;
26
+ if (!cmd)
27
+ return [];
28
+ if (cmd.unavailable === true) {
29
+ return [
30
+ [`Deals unavailable — ${DEAL_SOURCE_URL}`, ""],
31
+ ["Tier", "—"],
32
+ ["Intelligence", "—"],
33
+ ["Tok/s", "—"],
34
+ ];
35
+ }
36
+ const rows = [];
37
+ if (typeof cmd.tier === "string")
38
+ rows.push(["Tier", tierDisplay(cmd.tier)]);
39
+ if (cmd.free === true)
40
+ rows.push(["Status", "FREE"]);
41
+ if (cmd.allowance) {
42
+ for (const [plan, value] of Object.entries(cmd.allowance)) {
43
+ if (typeof value === "number")
44
+ rows.push([`${planDisplay(plan)} allowance`, `$${value}/mo`]);
45
+ }
46
+ }
47
+ if (cmd.discount && typeof cmd.discount.pct === "number") {
48
+ rows.push([
49
+ "Deal",
50
+ `${cmd.discount.pct}% off${typeof cmd.discount.endsAt === "string" ? ` until ${cmd.discount.endsAt}` : ""}`,
51
+ ]);
52
+ }
53
+ const was = cmd.was ? rateString(cmd.was) : undefined;
54
+ const now = cmd.now ? rateString(cmd.now) : undefined;
55
+ if (was)
56
+ rows.push(["Was", was]);
57
+ if (now)
58
+ rows.push(["Now", now]);
59
+ if (cmd.benchmark) {
60
+ rows.push([
61
+ "Intelligence",
62
+ typeof cmd.benchmark.intelligence === "number" ? String(cmd.benchmark.intelligence) : "—",
63
+ ]);
64
+ rows.push([
65
+ "Tok/s",
66
+ typeof cmd.benchmark.tokPerSec === "number" ? String(cmd.benchmark.tokPerSec) : "—",
67
+ ]);
68
+ }
69
+ if (cmd.peakOffPeak) {
70
+ rows.push([
71
+ "Rates",
72
+ `peak/off-peak${typeof cmd.peakOffPeak.windows === "string" ? ` (${cmd.peakOffPeak.windows})` : ""}`,
73
+ ]);
74
+ }
75
+ return rows;
76
+ }
77
+ const id = "commandcode.deals";
78
+ const tui = async (api) => {
79
+ api.slots.register({
80
+ order: 200,
81
+ slots: {
82
+ sidebar_content(_ctx, props) {
83
+ return _jsx(DealsPanel, { api: api, session_id: props.session_id });
84
+ },
85
+ },
86
+ });
87
+ };
88
+ function DealsPanel(props) {
89
+ const theme = () => props.api.theme.current;
90
+ // Mid-session model switches update the session record (`session.updated`
91
+ // reconciles it into the sync store), so reading `session.model` reactively
92
+ // is enough — no event subscription needed.
93
+ const model = createMemo(() => {
94
+ const current = props.api.state.session.get(props.session_id)?.model;
95
+ if (!current)
96
+ return undefined;
97
+ return props.api.state.provider.find((provider) => provider.id === current.providerID)
98
+ ?.models[current.id];
99
+ });
100
+ const rows = createMemo(() => dealsRows(model()));
101
+ return (_jsx(Show, { when: rows().length > 0, children: _jsxs("box", { children: [_jsx("text", { fg: theme().text, children: _jsx("b", { children: "Command Code" }) }), _jsx(For, { each: rows(), children: (row) => (_jsx("text", { fg: theme().textMuted, children: row[1] ? `${row[0]}: ${row[1]}` : row[0] })) })] }) }));
102
+ }
103
+ const plugin = { id, tui };
104
+ export default plugin;
@@ -0,0 +1 @@
1
+ export declare function vendorFamilyForModel(modelId: string): string | undefined;
@@ -0,0 +1,29 @@
1
+ // src/deals/vendor.ts — model id → vendor family mapping for the config hook.
2
+ // Derived from the model id namespace, never from scraped data, so it cannot
3
+ // go stale. Unknown ids map to undefined and the family field is left unset.
4
+ const VENDOR_FAMILIES = {
5
+ "claude-": "claude",
6
+ "gpt-": "gpt",
7
+ "google/": "gemini",
8
+ "deepseek/": "deepseek",
9
+ "Qwen/": "qwen",
10
+ "moonshotai/": "kimi",
11
+ "zai-org/": "glm",
12
+ "MiniMaxAI/": "minimax",
13
+ "xiaomi/": "mimo",
14
+ "stepfun/": "step",
15
+ "tencent/": "tencent",
16
+ "nvidia/": "nemotron",
17
+ "thinkingmachines/": "inkling",
18
+ "poolside/": "laguna",
19
+ "meta/": "muse",
20
+ "xai/": "grok",
21
+ "sakana/": "sakana",
22
+ };
23
+ export function vendorFamilyForModel(modelId) {
24
+ for (const [prefix, family] of Object.entries(VENDOR_FAMILIES)) {
25
+ if (modelId.startsWith(prefix))
26
+ return family;
27
+ }
28
+ return undefined;
29
+ }
@@ -2,6 +2,7 @@
2
2
  import { MODEL_SNAPSHOT } from "../catalog/snapshot.js";
3
3
  import { getApiBase } from "../env.js";
4
4
  import { augmentConfigCommandCodeModels, autoRegister } from "./models.js";
5
+ import { enrichCommandCodeModels, planSummaryTool } from "../deals/index.js";
5
6
  import { runAuthFlow } from "./auth.js";
6
7
  const server = async () => {
7
8
  return {
@@ -12,6 +13,7 @@ const server = async () => {
12
13
  baseURL: getApiBase(),
13
14
  });
14
15
  augmentConfigCommandCodeModels(config);
16
+ enrichCommandCodeModels(config);
15
17
  },
16
18
  auth: {
17
19
  provider: "commandcode",
@@ -23,6 +25,9 @@ const server = async () => {
23
25
  },
24
26
  ],
25
27
  },
28
+ tool: {
29
+ cmd_plan_summary: planSummaryTool(),
30
+ },
26
31
  };
27
32
  };
28
33
  export default { id: "commandcode", server };
@@ -9,7 +9,7 @@ import { resolveApiKey } from "./auth-key.js";
9
9
  import { messagesToCC, toolsToJson, systemPromptToText, getEnvironmentInfo, isRecord, } from "./converters.js";
10
10
  import { parseStreamEventLine, ccEventToStreamPart } from "./stream.js";
11
11
  import { redactCommandCodeErrorText, commandCodeErrorMessage } from "./redact.js";
12
- import { calculateCommandCodeCost } from "./cost.js";
12
+ import { calculateCommandCodeCost, costUsageFromAiSdkUsage } from "./cost.js";
13
13
  import { ZERO_MODEL_COST, MODEL_COSTS } from "./pricing.js";
14
14
  import { mappedReasoningEffort, resolveProviderReasoning, thinkingMetadataForModel, isReasoningModel, } from "./reasoning.js";
15
15
  import { modelSupportsImageInput } from "./modalities.js";
@@ -233,14 +233,7 @@ export class CommandCodeLanguageModel {
233
233
  for (const part of parts) {
234
234
  if (part.type === "finish") {
235
235
  finished = true;
236
- const usage = part.usage;
237
- calculateCommandCodeCost(this.costForModel(), {
238
- input: usage.inputTokens.total ?? 0,
239
- output: usage.outputTokens.total ?? 0,
240
- cacheRead: 0,
241
- cacheWrite: 0,
242
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
243
- });
236
+ calculateCommandCodeCost(this.costForModel(), costUsageFromAiSdkUsage(part.usage));
244
237
  }
245
238
  emit(part);
246
239
  }
@@ -1,3 +1,4 @@
1
+ import type { LanguageModelV3Usage } from "@ai-sdk/provider";
1
2
  import type { CommandCodeModelCost } from "./pricing.js";
2
3
  export interface CostUsage {
3
4
  input: number;
@@ -16,4 +17,10 @@ export interface CostUsage {
16
17
  export interface CostModel {
17
18
  cost: CommandCodeModelCost;
18
19
  }
20
+ /**
21
+ * Maps an emitted AI SDK v3 usage onto billable token counts: fresh input
22
+ * (`noCache`), cache read, and cache write each bill at their own rate, so
23
+ * the cache-inclusive `total` must not be billed as fresh input (issue #36).
24
+ */
25
+ export declare function costUsageFromAiSdkUsage(usage: LanguageModelV3Usage): CostUsage;
19
26
  export declare function calculateCommandCodeCost(model: CostModel, usage: CostUsage): void;
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Maps an emitted AI SDK v3 usage onto billable token counts: fresh input
3
+ * (`noCache`), cache read, and cache write each bill at their own rate, so
4
+ * the cache-inclusive `total` must not be billed as fresh input (issue #36).
5
+ */
6
+ export function costUsageFromAiSdkUsage(usage) {
7
+ return {
8
+ input: usage.inputTokens.noCache ?? 0,
9
+ output: usage.outputTokens.total ?? 0,
10
+ cacheRead: usage.inputTokens.cacheRead ?? 0,
11
+ cacheWrite: usage.inputTokens.cacheWrite ?? 0,
12
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
13
+ };
14
+ }
1
15
  export function calculateCommandCodeCost(model, usage) {
2
16
  const rates = model.cost;
3
17
  const longWrite = usage.cacheWrite1h ?? 0;
@@ -34,6 +34,7 @@ export const REASONING_MODELS = new Set([
34
34
  "moonshotai/Kimi-K3",
35
35
  "nvidia/nemotron-3-ultra-550b-a55b",
36
36
  "poolside/laguna-s-2.1-free",
37
+ "stealth/ox-alpha",
37
38
  "stepfun/Step-3.7-Flash",
38
39
  "tencent/hy3-paid",
39
40
  "thinkingmachines/inkling",
@@ -39,13 +39,13 @@ export function ccUsageToAiSdkUsage(event) {
39
39
  return undefined;
40
40
  const details = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : undefined;
41
41
  const totalInput = numberValue(usage.inputTokens) ?? 0;
42
- const noCache = numberValue(details?.noCacheTokens);
43
42
  const cacheRead = numberValue(details?.cacheReadTokens) ?? 0;
44
43
  const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0;
45
- const inputTotal = noCache ?? Math.max(0, totalInput - cacheRead - cacheWrite);
44
+ const explicitNoCache = numberValue(details?.noCacheTokens);
45
+ const noCache = explicitNoCache ?? Math.max(0, totalInput - cacheRead - cacheWrite);
46
46
  const outputTokens = numberValue(usage.outputTokens) ?? 0;
47
47
  return {
48
- inputTokens: { total: inputTotal, noCache: inputTotal, cacheRead, cacheWrite },
48
+ inputTokens: { total: totalInput, noCache, cacheRead, cacheWrite },
49
49
  outputTokens: { total: outputTokens, text: outputTokens, reasoning: 0 },
50
50
  };
51
51
  }
package/dist/tui.js ADDED
@@ -0,0 +1,180 @@
1
+ // @bun
2
+ // src/deals/tui.tsx
3
+ import { memo as _$memo } from "@opentui/solid";
4
+ import { setProp as _$setProp } from "@opentui/solid";
5
+ import { effect as _$effect } from "@opentui/solid";
6
+ import { insert as _$insert } from "@opentui/solid";
7
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
8
+ import { insertNode as _$insertNode } from "@opentui/solid";
9
+ import { createElement as _$createElement } from "@opentui/solid";
10
+ import { createComponent as _$createComponent } from "@opentui/solid";
11
+ import { For, Show, createMemo } from "solid-js";
12
+
13
+ // src/deals/catalog.ts
14
+ var PLAN_CATALOG = {
15
+ go: {
16
+ price: 1,
17
+ credits: 10,
18
+ window5h: 3,
19
+ windowWeek: 6,
20
+ display: "Go"
21
+ },
22
+ goat: {
23
+ price: 10,
24
+ credits: 70,
25
+ window5h: 14,
26
+ windowWeek: 35,
27
+ display: "GOAT"
28
+ },
29
+ pro: {
30
+ price: 20,
31
+ credits: 80,
32
+ window5h: 16,
33
+ windowWeek: 40,
34
+ display: "Pro"
35
+ },
36
+ max: {
37
+ price: 100,
38
+ credits: 150,
39
+ window5h: 45,
40
+ windowWeek: 90,
41
+ display: "Max 10\xD7"
42
+ },
43
+ max20: {
44
+ price: 200,
45
+ credits: 300,
46
+ window5h: 90,
47
+ windowWeek: 180,
48
+ display: "Max 20\xD7"
49
+ },
50
+ teampro: {
51
+ price: 40,
52
+ credits: 40,
53
+ window5h: 12,
54
+ windowWeek: 24,
55
+ display: "Team Pro"
56
+ },
57
+ provider: {
58
+ price: 15,
59
+ credits: 0,
60
+ window5h: 0,
61
+ windowWeek: 0,
62
+ display: "Provider"
63
+ }
64
+ };
65
+ var DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
66
+
67
+ // src/deals/tui.tsx
68
+ function planDisplay(plan) {
69
+ return PLAN_CATALOG[plan]?.display ?? plan;
70
+ }
71
+ var TIER_DISPLAY = {
72
+ opensource: "Open Source",
73
+ premium: "Premium"
74
+ };
75
+ function tierDisplay(tier) {
76
+ return TIER_DISPLAY[tier] ?? tier;
77
+ }
78
+ function rateString(rates) {
79
+ if (typeof rates.input !== "number" || typeof rates.output !== "number")
80
+ return;
81
+ return `$${rates.input}/$${rates.output} in/out`;
82
+ }
83
+ function dealsRows(model) {
84
+ const cmd = model?.options?.cmd;
85
+ if (!cmd)
86
+ return [];
87
+ if (cmd.unavailable === true) {
88
+ return [[`Deals unavailable \u2014 ${DEAL_SOURCE_URL}`, ""], ["Tier", "\u2014"], ["Intelligence", "\u2014"], ["Tok/s", "\u2014"]];
89
+ }
90
+ const rows = [];
91
+ if (typeof cmd.tier === "string")
92
+ rows.push(["Tier", tierDisplay(cmd.tier)]);
93
+ if (cmd.free === true)
94
+ rows.push(["Status", "FREE"]);
95
+ if (cmd.allowance) {
96
+ for (const [plan, value] of Object.entries(cmd.allowance)) {
97
+ if (typeof value === "number")
98
+ rows.push([`${planDisplay(plan)} allowance`, `$${value}/mo`]);
99
+ }
100
+ }
101
+ if (cmd.discount && typeof cmd.discount.pct === "number") {
102
+ rows.push(["Deal", `${cmd.discount.pct}% off${typeof cmd.discount.endsAt === "string" ? ` until ${cmd.discount.endsAt}` : ""}`]);
103
+ }
104
+ const was = cmd.was ? rateString(cmd.was) : undefined;
105
+ const now = cmd.now ? rateString(cmd.now) : undefined;
106
+ if (was)
107
+ rows.push(["Was", was]);
108
+ if (now)
109
+ rows.push(["Now", now]);
110
+ if (cmd.benchmark) {
111
+ rows.push(["Intelligence", typeof cmd.benchmark.intelligence === "number" ? String(cmd.benchmark.intelligence) : "\u2014"]);
112
+ rows.push(["Tok/s", typeof cmd.benchmark.tokPerSec === "number" ? String(cmd.benchmark.tokPerSec) : "\u2014"]);
113
+ }
114
+ if (cmd.peakOffPeak) {
115
+ rows.push(["Rates", `peak/off-peak${typeof cmd.peakOffPeak.windows === "string" ? ` (${cmd.peakOffPeak.windows})` : ""}`]);
116
+ }
117
+ return rows;
118
+ }
119
+ var id = "commandcode.deals";
120
+ var tui = async (api) => {
121
+ api.slots.register({
122
+ order: 200,
123
+ slots: {
124
+ sidebar_content(_ctx, props) {
125
+ return _$createComponent(DealsPanel, {
126
+ api,
127
+ get session_id() {
128
+ return props.session_id;
129
+ }
130
+ });
131
+ }
132
+ }
133
+ });
134
+ };
135
+ function DealsPanel(props) {
136
+ const theme = () => props.api.theme.current;
137
+ const model = createMemo(() => {
138
+ const current = props.api.state.session.get(props.session_id)?.model;
139
+ if (!current)
140
+ return;
141
+ return props.api.state.provider.find((provider) => provider.id === current.providerID)?.models[current.id];
142
+ });
143
+ const rows = createMemo(() => dealsRows(model()));
144
+ return _$createComponent(Show, {
145
+ get when() {
146
+ return rows().length > 0;
147
+ },
148
+ get children() {
149
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b");
150
+ _$insertNode(_el$, _el$2);
151
+ _$insertNode(_el$2, _el$3);
152
+ _$insertNode(_el$3, _$createTextNode(`Command Code`));
153
+ _$insert(_el$, _$createComponent(For, {
154
+ get each() {
155
+ return rows();
156
+ },
157
+ children: (row) => (() => {
158
+ var _el$5 = _$createElement("text");
159
+ _$insert(_el$5, (() => {
160
+ var _c$ = _$memo(() => !!row[1]);
161
+ return () => _c$() ? `${row[0]}: ${row[1]}` : row[0];
162
+ })());
163
+ _$effect((_$p) => _$setProp(_el$5, "fg", theme().textMuted, _$p));
164
+ return _el$5;
165
+ })()
166
+ }), null);
167
+ _$effect((_$p) => _$setProp(_el$2, "fg", theme().text, _$p));
168
+ return _el$;
169
+ }
170
+ });
171
+ }
172
+ var plugin = {
173
+ id,
174
+ tui
175
+ };
176
+ var tui_default = plugin;
177
+ export {
178
+ dealsRows,
179
+ tui_default as default
180
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "opencode-cmd-provider",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Command Code provider + plugin for opencode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "exports": {
8
8
  ".": "./dist/index.js",
9
- "./server": "./dist/index.js"
9
+ "./server": "./dist/index.js",
10
+ "./tui": "./dist/tui.js"
10
11
  },
11
12
  "files": [
12
13
  "dist",
@@ -15,14 +16,17 @@
15
16
  "CHANGELOG.md"
16
17
  ],
17
18
  "scripts": {
18
- "build": "tsc -p tsconfig.json",
19
+ "build": "tsc -p tsconfig.json && npm run build:tui",
19
20
  "typecheck": "tsc --noEmit",
20
21
  "refresh:snapshot": "node scripts/refresh-snapshot.mjs",
22
+ "refresh:deals": "node scripts/refresh-deals.mjs",
23
+ "refresh": "npm run refresh:snapshot && npm run refresh:deals -- --fixtures",
21
24
  "test": "npm run typecheck && npm run test:unit && npm run test:integration && npm run test:contract && npm run format:check",
22
- "test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/snapshot.test.ts && tsx tests/refresh-snapshot.test.ts && tsx tests/oauth.test.ts && tsx tests/parse-facts.test.ts && tsx tests/parse-modalities.test.ts && tsx tests/catalog-metadata.test.ts && tsx tests/plugin-models.test.ts",
25
+ "test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/snapshot.test.ts && tsx tests/refresh-snapshot.test.ts && tsx tests/oauth.test.ts && tsx tests/parse-facts.test.ts && tsx tests/parse-modalities.test.ts && tsx tests/catalog-metadata.test.ts && tsx tests/plugin-models.test.ts && tsx tests/vendor.test.ts && tsx tests/deals-enrichment.test.ts && tsx tests/plan-summary.test.ts && tsx tests/html-tables.test.ts && tsx tests/parse-docs.test.ts && tsx tests/refresh-deals.test.ts && tsx tests/tui-deals-panel.test.ts && tsx tests/plugin-install.test.ts",
23
26
  "test:integration": "tsx tests/integration-do-stream.test.ts && tsx tests/integration-do-generate.test.ts",
24
27
  "test:contract": "tsx tests/contract.test.ts",
25
28
  "test:e2e": "node tests/e2e-opencode.mjs",
29
+ "build:tui": "bun scripts/build-tui.ts",
26
30
  "format": "prettier --write '**/*.{ts,mjs,json,md}'",
27
31
  "format:check": "prettier --check '**/*.{ts,mjs,json,md}'"
28
32
  },
@@ -39,7 +43,11 @@
39
43
  "url": "git+https://github.com/rashidrazak/opencode-cmd-provider.git"
40
44
  },
41
45
  "dependencies": {
42
- "@ai-sdk/provider": "^4.0.7"
46
+ "@ai-sdk/provider": "^4.0.7",
47
+ "@opentui/core": "^0.5.4",
48
+ "@opentui/solid": "^0.5.4",
49
+ "solid-js": "1.9.12",
50
+ "zod": "^4.1.8"
43
51
  },
44
52
  "devDependencies": {
45
53
  "@opencode-ai/plugin": "*",