Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ jobs:
- name: Typecheck
run: bun run typecheck

lint:
name: check (lint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint
run: bun run lint

format:
name: check (format)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Format
run: bun run format:check

pack:
name: check (pack)
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ dist/
.DS_Store
opencode.local.json
.commandcode/

# workit: SDD working state (never commit)
docs/*/sdd/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This package is based on **[FanFan4204/opencode-commandcode-provider](https://gi

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

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

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

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`.
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`.

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.

Expand Down
92 changes: 91 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/2026-08-28-ci-catalog/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Watch `command-code` on npm every 6 hours, refresh the bundled catalog, publish
- Tag + GitHub Release **after** successful `npm publish` only. Never tag a version that is not on the registry.
- 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**.
- Human merges to `main` run a separate release job that publishes only if `package.json` version is unpublished (code fixes).
- Cost-only CLI failure still ships (`degraded` if fallback/unmatched costs remain). Model extract failure → no publish, `catalog-break` issue.
- Cost-only CLI failure still ships (`degraded` only if unmatched placeholder costs remain). Model extract failure → no publish, `catalog-break` issue.
- Runtime catalog stays bundled `models.json`. No GitHub fetch at OpenCode startup.
- Hybrid OpenCode transport stays `@ai-sdk/openai-compatible` + Provider API; this package is the **plugin**, not the SDK `npm` field.

Expand Down
27 changes: 14 additions & 13 deletions docs/specs/2026-08-28-ci-catalog-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Human- and CI-readable metadata about the bundled catalog. Shipped with the plug
"cli": 40,
"officialDocs": 25,
"thirdParty": 0,
"free": 0,
"fallback": 0,
"unmatched": 0
},
Expand All @@ -71,18 +72,18 @@ Human- and CI-readable metadata about the bundled catalog. Shipped with the plug
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).

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

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

Status values:

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

### `_version.txt` (existing)
Expand All @@ -100,10 +101,11 @@ Waterfall, per model, first hit wins. Later steps only fill models still missing
- Preferred parse target: [https://commandcode.ai/models](https://commandcode.ai/models) (per-model table: Input/M, Output/M, Cache read, Cache write).
- Fallback page: [https://commandcode.ai/docs/resources/pricing-limits](https://commandcode.ai/docs/resources/pricing-limits) if `/models` fetch or parse fails.
- 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.
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.
4. **Hardcoded fallback** (`FALLBACK_COSTS` + existing default `{ input: 0.5, output: 2 }`) — last resort so the catalog still ships.
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).
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.
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`.

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.
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.

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

Expand All @@ -126,8 +128,8 @@ Steps:
3. Download tarball (reuse logic from `scripts/sync-models.ts --remote`).
4. Extract models (required) then run the **cost waterfall**:
- model catalog fail → status `broken`, stop (no commit, no publish).
- CLI costs fail or partial → continue; fill gaps from official docs, then optional trusted API, then hardcoded fallback.
- Record `costSources` on the manifest. `degraded` only if any model still used fallback/unmatched.
- CLI costs fail or partial → continue; fill gaps from official docs, then free SKUs ($0), then models.dev, then unmatched placeholder.
- Record `costSources` on the manifest. `degraded` only if any model is still unmatched.
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`.
6. Write `models.json`, `_version.txt`, bump patch in `package.json`, write `manifest.json`.
7. Run unit tests (including 1.38 costless fixture).
Expand Down Expand Up @@ -171,7 +173,7 @@ When a subsequent sync succeeds after manual fix:
| Event | Version bump | Publish |
|---|---|---|
| New command-code catalog (healthy) | patch | yes |
| New command-code catalog (degraded: some fallback costs) | patch | yes |
| New command-code catalog (degraded: unmatched placeholder costs) | patch | yes |
| Model extraction broken | none | no |
| Plugin code fix (manual PR) | patch/minor per semver | yes (manual or on merge) |

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

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

## Acceptance Criteria

- 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).
- 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.
- 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.
- 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.
- Successful sync never requires local `bun run sync` from the user.
- Failed model extraction never publishes a misleading npm release.

Expand Down
27 changes: 15 additions & 12 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { resolveApiKey } from "./src/auth.js"
import { CommandCodeLanguageModel } from "./src/model.js"
import { resolveApiKey } from "./src/auth.js";
import { CommandCodeLanguageModel } from "./src/model.js";

export interface CommandCodeProviderOptions {
name?: string
apiKey?: string
baseURL?: string
headers?: Record<string, string>
authPaths?: string[]
name?: string;
apiKey?: string;
baseURL?: string;
headers?: Record<string, string>;
authPaths?: string[];
}

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

return {
languageModel(modelId: string): CommandCodeLanguageModel {
return new CommandCodeLanguageModel(modelId, {
apiKey,
baseURL: typeof options.baseURL === "string" ? options.baseURL : undefined,
headers: typeof options.headers === "object" && options.headers !== null ? options.headers as Record<string, string> : undefined,
})
headers:
typeof options.headers === "object" && options.headers !== null
? (options.headers as Record<string, string>)
: undefined,
});
},
}
};
}
22 changes: 17 additions & 5 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"generatedAt": "2026-08-28T19:09:12.385Z",
"generatedAt": "2026-08-28T19:50:57.386Z",
"pluginVersion": "0.5.0",
"commandCodeVersion": "1.38.1",
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.38.1.tgz",
Expand All @@ -13,10 +13,22 @@
},
"costSources": {
"cli": 0,
"officialDocs": 53,
"officialDocs": 60,
"thirdParty": 0,
"fallback": 7,
"unmatched": 5
"free": 5,
"fallback": 0,
"unmatched": 0
},
"status": "degraded"
"review": {
"thirdParty": [],
"free": [
"inclusionai/ling-3.0-flash-free",
"minimax/minimax-m2.7-free",
"MiniMaxAI/MiniMax-M3-Free",
"minimax/minimax-m3-free",
"tencent/Hy3"
],
"unmatched": []
},
"status": "healthy"
}
20 changes: 10 additions & 10 deletions models.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,8 +793,8 @@
"reasoning": true,
"tool_call": true,
"cost": {
"input": 0.5,
"output": 2
"input": 0,
"output": 0
},
"limit": {
"context": 256000,
Expand Down Expand Up @@ -872,8 +872,8 @@
"reasoning": false,
"tool_call": true,
"cost": {
"input": 0.5,
"output": 2
"input": 0,
"output": 0
},
"limit": {
"context": 197000,
Expand Down Expand Up @@ -903,8 +903,8 @@
"reasoning": true,
"tool_call": true,
"cost": {
"input": 0.5,
"output": 2
"input": 0,
"output": 0
},
"limit": {
"context": 1000000,
Expand All @@ -918,8 +918,8 @@
"reasoning": true,
"tool_call": true,
"cost": {
"input": 0.5,
"output": 2
"input": 0,
"output": 0
},
"limit": {
"context": 1000000,
Expand Down Expand Up @@ -1193,8 +1193,8 @@
"reasoning": true,
"tool_call": true,
"cost": {
"input": 0.5,
"output": 2
"input": 0,
"output": 0
},
"limit": {
"context": 262144,
Expand Down
Loading