Skip to content

Commit 94be56e

Browse files
fix(catalog): price free SKUs at $0 and fill remaining gaps from models.dev (#4)
* fix(catalog): price free SKUs at $0 and fill remaining gaps from models.dev Drop hardcoded FALLBACK_COSTS so billed rates come from CLI, official docs, or models.dev instead of a plugin table. Command Code free SKUs stay $0 and do not inherit paid twins. Co-authored-by: Cursor <cursoragent@cursor.com> * ci: lint and format with oxlint and oxfmt like workit Keep bun test. Add the same Oxc toolchain workit uses so CI can fail on lint and format instead of only tests and types. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 727d6f4 commit 94be56e

51 files changed

Lines changed: 3312 additions & 2554 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ jobs:
4343
- name: Typecheck
4444
run: bun run typecheck
4545

46+
lint:
47+
name: check (lint)
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v7
51+
- uses: oven-sh/setup-bun@v2
52+
with:
53+
bun-version: ${{ env.BUN_VERSION }}
54+
- name: Install dependencies
55+
run: bun install --frozen-lockfile
56+
- name: Lint
57+
run: bun run lint
58+
59+
format:
60+
name: check (format)
61+
runs-on: ubuntu-latest
62+
steps:
63+
- uses: actions/checkout@v7
64+
- uses: oven-sh/setup-bun@v2
65+
with:
66+
bun-version: ${{ env.BUN_VERSION }}
67+
- name: Install dependencies
68+
run: bun install --frozen-lockfile
69+
- name: Format
70+
run: bun run format:check
71+
4672
pack:
4773
name: check (pack)
4874
runs-on: ubuntu-latest

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ dist/
44
.DS_Store
55
opencode.local.json
66
.commandcode/
7+
8+
# workit: SDD working state (never commit)
9+
docs/*/sdd/

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This package is based on **[FanFan4204/opencode-commandcode-provider](https://gi
1616

1717
- Bundled `models.json` is the default runtime catalog (no local CLI scrape).
1818
- CLI cost extraction can fail (as on `command-code@1.38.x`) without dropping models.
19-
- Official docs fill missing costs; leftover models use a conservative fallback (`degraded` in `manifest.json`).
19+
- Official docs fill missing costs; remaining paid gaps use [models.dev](https://models.dev) as a reference. Command Code free SKUs stay `$0`.
2020
- Reasoning effort **variants** on models that declare `reasoningEfforts`.
2121
- Quiet OpenCode startup (diagnostics go to `startup.json`, not stdout).
2222

@@ -74,14 +74,14 @@ Maintainers only. OpenCode will scrape a local `command-code` install when `COMM
7474
git clone https://github.com/BrainerVirus/opencode-commandcode-provider.git
7575
cd opencode-commandcode-provider
7676
bun install
77-
bun test tests/unit/
77+
bun run check # oxlint + oxfmt + bun test + tsc (same stack as workit)
7878
```
7979

8080
```bash
8181
bun run sync -- --remote # refresh models.json + manifest.json from command-code@latest
8282
```
8383

84-
CI (`.github/workflows/catalog-sync.yml`) opens a PR every 6 hours when Command Code ships a new catalog. Merge after **check (test)**, **check (typecheck)**, and **check (pack)** are green. `.github/workflows/release.yml` then runs **semantic-release** (npm publish + GitHub Release + tag). Do not push to `main`.
84+
CI (`.github/workflows/catalog-sync.yml`) opens a PR every 6 hours when Command Code ships a new catalog. Merge after **check (test)**, **check (typecheck)**, **check (lint)**, **check (format)**, and **check (pack)** are green. `.github/workflows/release.yml` then runs **semantic-release** (npm publish + GitHub Release + tag). Do not push to `main`.
8585

8686
The GitHub Actions secret name is `NPMJS` (same as workit). It is mapped to both `NPM_TOKEN` and `NODE_AUTH_TOKEN`. Use an npm **Automation** token (bypasses 2FA). A login token from `~/.npmrc` fails CI with `EOTP`. Catalog PRs get a real CI run when `RELEASE_SYNC_TOKEN` (or `CATALOG_PUSH_TOKEN`) is a PAT; `GITHUB_TOKEN` can open the PR but GitHub will not start workflows from that event.
8787

bun.lock

Lines changed: 91 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/2026-08-28-ci-catalog/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Watch `command-code` on npm every 6 hours, refresh the bundled catalog, publish
1515
- Tag + GitHub Release **after** successful `npm publish` only. Never tag a version that is not on the registry.
1616
- Catalog auto-commit uses `GITHUB_TOKEN` (or `CATALOG_PUSH_TOKEN` if protection blocks it). Those commits do **not** trigger other workflows, so catalog-sync must publish in the **same job**.
1717
- Human merges to `main` run a separate release job that publishes only if `package.json` version is unpublished (code fixes).
18-
- Cost-only CLI failure still ships (`degraded` if fallback/unmatched costs remain). Model extract failure → no publish, `catalog-break` issue.
18+
- Cost-only CLI failure still ships (`degraded` only if unmatched placeholder costs remain). Model extract failure → no publish, `catalog-break` issue.
1919
- Runtime catalog stays bundled `models.json`. No GitHub fetch at OpenCode startup.
2020
- Hybrid OpenCode transport stays `@ai-sdk/openai-compatible` + Provider API; this package is the **plugin**, not the SDK `npm` field.
2121

docs/specs/2026-08-28-ci-catalog-automation.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Human- and CI-readable metadata about the bundled catalog. Shipped with the plug
6161
"cli": 40,
6262
"officialDocs": 25,
6363
"thirdParty": 0,
64+
"free": 0,
6465
"fallback": 0,
6566
"unmatched": 0
6667
},
@@ -71,18 +72,18 @@ Human- and CI-readable metadata about the bundled catalog. Shipped with the plug
7172
Write order: bump `package.json` version first, then write `manifest.json` with that `pluginVersion`, then one commit. Do not store `gitCommit` in the file (unknown until after commit; put SHA on the GitHub Release instead).
7273

7374
`status` rules:
74-
- `healthy`: model catalog ok **and** every model has costs from `cli`, `officialDocs`, or `thirdParty` (`fallback` = 0 and `unmatched` = 0)
75-
- `degraded`: model catalog ok, but at least one model used hardcoded fallback or has no cost
75+
- `healthy`: model catalog ok **and** every model has a sourced price (`cli`, `officialDocs`, `thirdParty` / models.dev, or `free`). `unmatched` = 0
76+
- `degraded`: model catalog ok, but at least one model still has the unmatched placeholder `{ input: 0.5, output: 2 }`
7677
- `broken`: model catalog extraction failed
7778

78-
`extraction.costCatalog` is the **best** source that contributed (`cli` | `docs` | `thirdParty` | `fallback` | `missing`).
79+
`extraction.costCatalog` is the **best** source that contributed (`cli` | `docs` | `thirdParty` | `free` | `fallback` | `missing`).
7980

8081
Status values:
8182

8283
| status | Meaning | CI action |
8384
|---|---|---|
84-
| `healthy` | models ok; costs from CLI, official docs, and/or trusted API | release |
85-
| `degraded` | models ok; some costs are hardcoded fallback or missing | release + note which models fell through |
85+
| `healthy` | models ok; costs from CLI, official docs, models.dev, and/or free SKUs | release |
86+
| `degraded` | models ok; some costs have no listed source | release + note which models fell through |
8687
| `broken` | model catalog extraction failed | no release, open issue |
8788

8889
### `_version.txt` (existing)
@@ -100,10 +101,11 @@ Waterfall, per model, first hit wins. Later steps only fill models still missing
100101
- Preferred parse target: [https://commandcode.ai/models](https://commandcode.ai/models) (per-model table: Input/M, Output/M, Cache read, Cache write).
101102
- Fallback page: [https://commandcode.ai/docs/resources/pricing-limits](https://commandcode.ai/docs/resources/pricing-limits) if `/models` fetch or parse fails.
102103
- Store the **current billed** per-1M USD rates shown on the page (deal-adjusted when the page shows an effective price). Do not invent deal math.
103-
3. **Trusted third-party API** — only if a provider is configured (`COST_ENRICHMENT_API_URL` secret/env). Default: **none**. Skip this step until one is added. Do not use OpenRouter (or similar) unless explicitly configured; their list prices are not Command Code’s billed rates.
104-
4. **Hardcoded fallback** (`FALLBACK_COSTS` + existing default `{ input: 0.5, output: 2 }`) — last resort so the catalog still ships.
104+
3. **Free SKUs** — catalog id/name matching `\bfree\b` or id ending `-free``{ input: 0, output: 0 }`. Runs **before** models.dev so Command Code free SKUs stay $0 even when models.dev lists a paid twin (e.g. Tencent Hy3).
105+
4. **models.dev**`GET https://models.dev/api.json` for remaining **paid** gaps. Exact id (case-insensitive), then last path segment as id, then exact display name. Reference prices, not Command Code billed rates. Do not apply to free SKUs.
106+
5. **Unmatched placeholder** `{ input: 0.5, output: 2 }` — last resort so the catalog still ships. These are the only models that mark the catalog `degraded`.
105107

106-
Match docs/API rows to catalog models with **exact** id (case-insensitive) then **exact** display name (case-insensitive). No fuzzy matching. Unmatched models go to the next step.
108+
Match docs/API rows to catalog models with **exact** id (case-insensitive) then **exact** display name (case-insensitive). models.dev also tries the last `/` segment of the catalog id as an exact id. No fuzzy matching. Unmatched models go to the next step.
107109

108110
Never fail the sync because costs are incomplete. Model catalog remains the hard requirement.
109111

@@ -126,8 +128,8 @@ Steps:
126128
3. Download tarball (reuse logic from `scripts/sync-models.ts --remote`).
127129
4. Extract models (required) then run the **cost waterfall**:
128130
- model catalog fail → status `broken`, stop (no commit, no publish).
129-
- CLI costs fail or partial → continue; fill gaps from official docs, then optional trusted API, then hardcoded fallback.
130-
- Record `costSources` on the manifest. `degraded` only if any model still used fallback/unmatched.
131+
- CLI costs fail or partial → continue; fill gaps from official docs, then free SKUs ($0), then models.dev, then unmatched placeholder.
132+
- Record `costSources` on the manifest. `degraded` only if any model is still unmatched.
131133
5. Sanity floor: `modelCount >= max(20, floor(lastSuccessfulModelCount * 0.5))`. `lastSuccessfulModelCount` is `modelCount` from the last committed manifest with `status` `healthy` or `degraded`. Fail as `broken` if below. If no prior manifest, use `20`.
132134
6. Write `models.json`, `_version.txt`, bump patch in `package.json`, write `manifest.json`.
133135
7. Run unit tests (including 1.38 costless fixture).
@@ -171,7 +173,7 @@ When a subsequent sync succeeds after manual fix:
171173
| Event | Version bump | Publish |
172174
|---|---|---|
173175
| New command-code catalog (healthy) | patch | yes |
174-
| New command-code catalog (degraded: some fallback costs) | patch | yes |
176+
| New command-code catalog (degraded: unmatched placeholder costs) | patch | yes |
175177
| Model extraction broken | none | no |
176178
| Plugin code fix (manual PR) | patch/minor per semver | yes (manual or on merge) |
177179

@@ -255,15 +257,14 @@ Commit **extracted snapshots** (model JSON + expected counts/errors), not the pr
255257
| `NPM_TOKEN` | publish plugin package (`brainervirus`) |
256258
| `GITHUB_TOKEN` | commit, release, issues |
257259
| `CATALOG_PUSH_TOKEN` | optional; only if branch protection blocks `GITHUB_TOKEN` |
258-
| `COST_ENRICHMENT_API_URL` | optional; skip third-party cost step when unset |
259260

260261
If `main` is protected against `GITHUB_TOKEN`, set `CATALOG_PUSH_TOKEN`. One push path only.
261262

262263
## Acceptance Criteria
263264

264265
- User can run OpenCode with **no** global/local `command-code` install and get current models from the installed plugin (npm package, or a `file://` checkout that has been synced).
265266
- Within 6 hours of a new `command-code` npm release, CI either commits a catalog update (and publishes a plugin patch if `NPM_TOKEN` is set) or opens/updates a `catalog-break` issue.
266-
- Cost-only CLI regressions (like 1.38) still ship a catalog. Costs come from official docs when the CLI map fails; `degraded` only if docs/API also miss models.
267+
- Cost-only CLI regressions (like 1.38) still ship a catalog. Costs come from official docs, then free SKUs ($0), then models.dev; `degraded` only if models remain unmatched.
267268
- Successful sync never requires local `bun run sync` from the user.
268269
- Failed model extraction never publishes a misleading npm release.
269270

index.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
import { resolveApiKey } from "./src/auth.js"
2-
import { CommandCodeLanguageModel } from "./src/model.js"
1+
import { resolveApiKey } from "./src/auth.js";
2+
import { CommandCodeLanguageModel } from "./src/model.js";
33

44
export interface CommandCodeProviderOptions {
5-
name?: string
6-
apiKey?: string
7-
baseURL?: string
8-
headers?: Record<string, string>
9-
authPaths?: string[]
5+
name?: string;
6+
apiKey?: string;
7+
baseURL?: string;
8+
headers?: Record<string, string>;
9+
authPaths?: string[];
1010
}
1111

1212
export function createCommandCode(options: CommandCodeProviderOptions = {}) {
13-
const apiKey = resolveApiKey({ apiKey: options.apiKey, authPaths: options.authPaths })
13+
const apiKey = resolveApiKey({ apiKey: options.apiKey, authPaths: options.authPaths });
1414
if (!apiKey) {
1515
throw new Error(
1616
"Command Code API key not found. Set COMMANDCODE_API_KEY env var, create ~/.commandcode/auth.json, or pass apiKey option.",
17-
)
17+
);
1818
}
1919

2020
return {
2121
languageModel(modelId: string): CommandCodeLanguageModel {
2222
return new CommandCodeLanguageModel(modelId, {
2323
apiKey,
2424
baseURL: typeof options.baseURL === "string" ? options.baseURL : undefined,
25-
headers: typeof options.headers === "object" && options.headers !== null ? options.headers as Record<string, string> : undefined,
26-
})
25+
headers:
26+
typeof options.headers === "object" && options.headers !== null
27+
? (options.headers as Record<string, string>)
28+
: undefined,
29+
});
2730
},
28-
}
31+
};
2932
}

manifest.json

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
3-
"generatedAt": "2026-08-28T19:09:12.385Z",
3+
"generatedAt": "2026-08-28T19:50:57.386Z",
44
"pluginVersion": "0.5.0",
55
"commandCodeVersion": "1.38.1",
66
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.38.1.tgz",
@@ -13,10 +13,22 @@
1313
},
1414
"costSources": {
1515
"cli": 0,
16-
"officialDocs": 53,
16+
"officialDocs": 60,
1717
"thirdParty": 0,
18-
"fallback": 7,
19-
"unmatched": 5
18+
"free": 5,
19+
"fallback": 0,
20+
"unmatched": 0
2021
},
21-
"status": "degraded"
22+
"review": {
23+
"thirdParty": [],
24+
"free": [
25+
"inclusionai/ling-3.0-flash-free",
26+
"minimax/minimax-m2.7-free",
27+
"MiniMaxAI/MiniMax-M3-Free",
28+
"minimax/minimax-m3-free",
29+
"tencent/Hy3"
30+
],
31+
"unmatched": []
32+
},
33+
"status": "healthy"
2234
}

models.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -793,8 +793,8 @@
793793
"reasoning": true,
794794
"tool_call": true,
795795
"cost": {
796-
"input": 0.5,
797-
"output": 2
796+
"input": 0,
797+
"output": 0
798798
},
799799
"limit": {
800800
"context": 256000,
@@ -872,8 +872,8 @@
872872
"reasoning": false,
873873
"tool_call": true,
874874
"cost": {
875-
"input": 0.5,
876-
"output": 2
875+
"input": 0,
876+
"output": 0
877877
},
878878
"limit": {
879879
"context": 197000,
@@ -903,8 +903,8 @@
903903
"reasoning": true,
904904
"tool_call": true,
905905
"cost": {
906-
"input": 0.5,
907-
"output": 2
906+
"input": 0,
907+
"output": 0
908908
},
909909
"limit": {
910910
"context": 1000000,
@@ -918,8 +918,8 @@
918918
"reasoning": true,
919919
"tool_call": true,
920920
"cost": {
921-
"input": 0.5,
922-
"output": 2
921+
"input": 0,
922+
"output": 0
923923
},
924924
"limit": {
925925
"context": 1000000,
@@ -1193,8 +1193,8 @@
11931193
"reasoning": true,
11941194
"tool_call": true,
11951195
"cost": {
1196-
"input": 0.5,
1197-
"output": 2
1196+
"input": 0,
1197+
"output": 0
11981198
},
11991199
"limit": {
12001200
"context": 262144,

0 commit comments

Comments
 (0)