diff --git a/README.md b/README.md index fd0704f..a7be88a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This package is based on **[FanFan4204/opencode-commandcode-provider](https://gi - Vision vs text-only comes from the Command Code CLI catalog (`inputModalities` on every SKU). [models.dev](https://models.dev) only adds extra inputs (video/audio/pdf) when it matches. - Reasoning effort **variants** on models that declare `reasoningEfforts`. - Release date, family, input limits, model status, vendor context limits, and matched context-price tiers flow through the bundled catalog. V2 gets native release, family, input, status, and tier fields; V1 keeps its supported fields and base prices, with `context_over_200k` where representable. -- The provider's `supported_endpoints` metadata chooses each model's API route. Responses-capable models use `@ai-sdk/openai` on V1 and `aisdk:@ai-sdk/openai` on V2; Claude falls back to Anthropic Messages when endpoint metadata is absent. The V1 and V2 Responses mappings passed official Docker checks against a local mock stream, and a GOAT-key DeepSeek control request succeeded in OpenCode V2. Claude Sonnet 4.6 returned `MODEL_NOT_IN_PLAN` with the message “available in Pro and above plans or extra on-demand usage”; this confirms the account restriction, not successful Anthropic routing. Entitled Claude access has not been live-verified. If a model fails on an account entitled to use it, please [open an issue](https://github.com/BrainerVirus/opencode-commandcode/issues) with the model ID and OpenCode version, or submit a PR with a reproducible fix. +- The provider's `supported_endpoints` metadata chooses each model's API route. When a model advertises Messages, it uses Anthropic Messages; otherwise Chat Completions is preferred when available, and Responses is used when it is the only advertised route. This avoids malformed annotation events seen on the provider's Responses stream while preserving Responses-only models. V1 maps Responses to `@ai-sdk/openai`; V2 maps it to `aisdk:@ai-sdk/openai`. Claude Sonnet 4.6 returned `MODEL_NOT_IN_PLAN` with the message “available in Pro and above plans or extra on-demand usage”; entitled Claude access has not been live-verified. If a model fails on an account entitled to use it, please [open an issue](https://github.com/BrainerVirus/opencode-commandcode/issues) with the model ID and OpenCode version, or submit a PR with a reproducible fix. - Quiet OpenCode startup (diagnostics go to `startup.json`, not stdout). ## How it works @@ -33,7 +33,7 @@ This package is based on **[FanFan4204/opencode-commandcode-provider](https://gi For published plugin versions, the six-hour CI schedule extracts the latest `command-code` npm bundle and refreshes callable models plus endpoint metadata, even when the CLI version is unchanged. It opens a catalog PR only when generated artifacts change; at startup the plugin loads those artifacts and registers them with OpenCode — never the other way around. - **Extract + filter** — model entries (ids, names, reasoning, `inputModalities`, limits) are evaluated out of the minified CLI bundle (`src/catalog.ts`), then intersected with the callable IDs reported by the provider API. -- **Metadata + costs merge** — vendor context length tightens only a fallback context limit, while `supported_endpoints` selects Chat Completions, Responses, or Messages per model; missing endpoint metadata preserves the last known route data. models.dev contributes release date, family, input limit, status, modalities, and cost tiers when present. Tier rows are accepted only when their base prices match this Command Code catalog. Base costs use CLI bundle → official Command Code docs → free SKUs (`$0`) → [models.dev](https://models.dev) reference prices → unmatched placeholder. Anything still unmatched marks the catalog `degraded`. This runs at sync time only; runtime never fetches metadata or prices. +- **Metadata + costs merge** — vendor context length tightens only a fallback context limit, while `supported_endpoints` selects Messages, Chat Completions, or Responses per model in that preference order; missing endpoint metadata preserves the last known route data. models.dev contributes release date, family, input limit, status, modalities, and cost tiers when present. Tier rows are accepted only when their base prices match this Command Code catalog. Base costs use CLI bundle → official Command Code docs → free SKUs (`$0`) → [models.dev](https://models.dev) reference prices → unmatched placeholder. Anything still unmatched marks the catalog `degraded`. This runs at sync time only; runtime never fetches metadata or prices. - **Artifacts** — `models.json` (the catalog), `_version.txt` (upstream version), `manifest.json` (counts, per-source cost stats, `healthy`/`degraded`/`broken` status). - **Version-specific registration** — V1 uses the `plugin` config key, `server()` provider map, and V1 auth callback. V2 uses `plugins`, provider/model transforms, and a `commandcode` integration with key and `COMMANDCODE_API_KEY` environment methods. Both retain the Command Code wire model ID. V2 exposes tiered context pricing; V1 emits its supported `context_over_200k` field and keeps flat pricing for other tiers. - **Degraded/cache fallbacks** — a `degraded`/`broken` manifest sets the degraded flag with a reason; an unreadable bundled `models.json` falls back to the last-good cache; auth/connect still registers even with an empty catalog. @@ -46,7 +46,7 @@ OpenCode V2: ```json { - "plugins": ["@brainervirus/opencode-commandcode@latest"] + "plugins": ["@brainervirus/opencode-commandcode"] } ``` @@ -54,11 +54,11 @@ OpenCode V1: ```json { - "plugin": ["@brainervirus/opencode-commandcode@latest"] + "plugin": ["@brainervirus/opencode-commandcode"] } ``` -Pin a version instead of `@latest` if you do not want automatic catalog patches. +The bare package name is unpinned and resolves npm's `latest` release when OpenCode installs or updates it; adding `@latest` is unnecessary. OpenCode V2 checks for updates at startup but keeps an existing cached package. Apply a newer package with OpenCode's plugin-update action, then restart to load it. OpenCode V1 can refresh the global package with `opencode plugin @brainervirus/opencode-commandcode --global --force`. `file://` checkouts are **not** updated by npm; `git pull` after CI commits, or switch to the npm plugin line. @@ -76,7 +76,7 @@ Set `COMMANDCODE_API_KEY`, or connect interactively. OpenCode V1 provides **Comm /models ``` -Catalog patches arrive as plugin updates: `@latest` refreshes in the background and takes effect on the next OpenCode restart. If a new model is missing after an announced sync, restart opencode once. +Catalog patches ship in new npm releases. OpenCode loads the version in its package cache; an update must be applied through the host before the new catalog appears. If a new model is missing after an announced sync, check the installed package version, apply the update, and restart OpenCode. ## Plugin config file diff --git a/docs/specs/2026-08-28-ci-catalog-automation.md b/docs/specs/2026-08-28-ci-catalog-automation.md index 8d7a5aa..1d01f4b 100644 --- a/docs/specs/2026-08-28-ci-catalog-automation.md +++ b/docs/specs/2026-08-28-ci-catalog-automation.md @@ -180,12 +180,12 @@ When a subsequent sync succeeds after manual fix: Published npm name (locked, same account as workit: `brainervirus`): ```json -"plugin": ["@brainervirus/opencode-commandcode@latest"] +{ "plugins": ["@brainervirus/opencode-commandcode"] } ``` `package.json` `name` is `@brainervirus/opencode-commandcode` with `publishConfig.access: "public"` (published as `@brainervirus/commandcode-go-opencode-provider` before the 0.6.0 rename). This stays its own repo; it is not folded into `workflow-toolkit`. -`file://` installs are **not** auto-updated by CI; catalog changes require `git pull` of this repo. npm installs update through `@latest`. +OpenCode V1 uses the singular `plugin` config key with the same bare package name. The package name without a suffix is unpinned and resolves npm's latest dist-tag when installed or updated; `@latest` is redundant. OpenCode V2 checks for unpinned updates at startup but keeps an existing package cache until the host's update action is applied. `file://` installs are not updated by npm; catalog changes require `git pull` of this repo. ## Runtime Plugin Changes @@ -294,7 +294,7 @@ Same sequence as the identity spec. Phase 1 (A) must refresh `models.json` befor 1. Rename package to `@brainervirus/commandcode-go-opencode-provider` (shipped at 0.5.0; renamed to `@brainervirus/opencode-commandcode` at 0.6.0). 2. Store `NPM_TOKEN` (npm user `brainervirus`) as a GitHub Actions secret; `publishConfig.access: public`. 3. Enable publish + GitHub Release in the workflow. -4. Document `"plugin": ["@brainervirus/opencode-commandcode@latest"]` vs pin. +4. Document the bare unpinned package name and explicit-version pinning. ## Historical Plan Decomposition diff --git a/docs/specs/2026-09-20-catalog-freshness.md b/docs/specs/2026-09-20-catalog-freshness.md index b766dc2..08b1728 100644 --- a/docs/specs/2026-09-20-catalog-freshness.md +++ b/docs/specs/2026-09-20-catalog-freshness.md @@ -1,6 +1,6 @@ # End-to-End Command Code Catalog Freshness -Status: provider steps shipped (0.7.72, 2026-09-20); upstream OpenCode refresh pending +Status: provider steps shipped (0.7.72, 2026-09-20); automatic warm-cache refresh remains an upstream OpenCode limitation ## Goal @@ -10,7 +10,7 @@ Freshness has three independent boundaries: 1. Command Code must identify which extracted models are currently callable. 2. This provider must publish only that callable catalog with internally consistent metadata. -3. OpenCode must refresh mutable plugin package references without discarding a working cache. +3. OpenCode must make it clear when a mutable plugin package is stale and how to update it without losing a working cache. ## Current Failure @@ -20,7 +20,7 @@ Separately, OpenCode can retain an older installation of `@brainervirus/opencode The release pipeline also updated `manifest.json` after npm publication. Consequently, the manifest inside a newly published tarball could report the previous plugin version even though the repository was corrected by a later synchronization PR. -Provider-side resolution (2026-09-20): the availability filter shipped in `fix(catalog): exclude unavailable models` and the packed-manifest alignment shipped in `fix(release): align published manifest version`. The OpenCode-side mutable-package refresh (step 3 below) is still pending upstream. +Provider-side resolution (2026-09-20): the availability filter shipped in `fix(catalog): exclude unavailable models` and the packed-manifest alignment shipped in `fix(release): align published manifest version`. The original request for automatic warm-cache replacement is not implemented by OpenCode V2.0.18; startup loads the cached package and does not replace it. The plugin cannot update the package code that is already running. ## Decisions @@ -86,19 +86,9 @@ The unavailable list is sorted by ID so repeated synchronization is deterministi ### Mutable OpenCode plugin packages -The upstream OpenCode implementation will rework pull request [anomalyco/opencode#49485](https://github.com/anomalyco/opencode/pull/49485). +OpenCode V2.0.18 loads a warm cached package at startup and does not silently replace it. An isolated local startup with a cached Command Code 0.9.1 package loaded 0.9.1 even though npm `latest` was 0.10.1. A clean cache does install the latest package. OpenCode's current V2 plugin documentation describes startup update checks without changing the installed package; the host's update action must be applied before a restart loads the new code. V1.18.30's `opencode plugin --global --force` explicitly refreshes a global package. -For npm plugin references: - -- exact versions are immutable and use the cache without refresh; -- bare package names, dist-tags such as `@latest`, and version ranges are mutable; -- a mutable reference with no cached installation blocks on installation; a failure reports the normal plugin installation error and leaves no partial cache; -- a mutable reference with a cached installation loads that cache immediately and starts at most one background refresh per OpenCode process; -- a successful background refresh becomes active on the next OpenCode restart; plugins are not hot-swapped in a running process; -- a failed background refresh leaves the cached installation untouched and does not block startup; and -- equivalent bare and explicit-latest references share one canonical cache location. - -Concurrent refresh attempts for the same canonical package are deduplicated. Installation remains atomic so interruption cannot replace a working cache with a partial package. +The bare package name is the normal unpinned reference; adding `@latest` is redundant. Exact versions remain pinned. The plugin reads only its bundled catalog and cannot refresh its own npm package or hot-swap its running code. Git, file, and workspace plugin references are outside this behavior change. @@ -114,7 +104,7 @@ Release verification must inspect the packed artifact and fail before publicatio The installed plugin reads only its bundled `models.json` and `manifest.json` for normal model registration. It does not call the availability endpoint, npm registry, or GitHub to decide which models to expose. -This preserves deterministic startup and offline use. Freshness is delivered by catalog automation plus OpenCode's mutable-package refresh behavior. +This preserves deterministic startup and offline use. Freshness is delivered by catalog automation plus the host's explicit package-update flow; a warm npm cache can remain stale until that update is applied. ## Non-Goals @@ -140,13 +130,11 @@ This preserves deterministic startup and offline use. Freshness is delivered by ### OpenCode package refresh -- A warm mutable cache starts OpenCode without waiting for the registry. -- A cold mutable install failure reports an installation error and leaves no cache entry. -- One background refresh is attempted per canonical mutable package per process. -- Successful refresh output is used after restart, not during the current process. -- Offline or failed refresh preserves and continues using the prior cache. -- Exact-version references perform no background refresh. -- Bare and `@latest` references cannot maintain divergent cache roots. +- A clean cache with the bare package entry installs the current npm `latest` release. +- A warm OpenCode V2.0.18 cache loads its installed version at startup without replacing it. +- Applying a host plugin update and restarting loads the new package. +- The V1 force-install command refreshes its configured global package. +- Exact-version references remain pinned; `@latest` is not required for an unpinned package. ### Release metadata @@ -156,7 +144,7 @@ This preserves deterministic startup and offline use. Freshness is delivered by ### End-to-end -From a clean OpenCode cache, installing the mutable Command Code plugin and restarting after a successful refresh exposes every exact-ID match between extractor candidates and the current API list, including newly added matches, and exposes none of the API-absent retired or unreleased entries. +From a clean OpenCode cache, the bare Command Code package installs the current npm release and exposes the catalog in that tarball. With a warm stale cache, users must apply the host's package update and restart before the newer catalog appears. In both cases the catalog exposes only the exact-ID matches produced by the provider sync. ## Coordination Plan @@ -178,17 +166,17 @@ From a clean OpenCode cache, installing the mutable Command Code plugin and rest - Owner: OpenCode contributor. - Files: the package installation/cache path and tests in the upstream OpenCode repository. - Dependency: rework pull request `#49485`; independent of provider steps 1 and 2. - - Evidence: cold, warm, offline, exact-version, canonical-cache, deduplication, and next-restart tests in upstream CI. + - Evidence: upstream docs and local warm-cache tests show that V2 checks but does not silently replace a cached package; automatic replacement still requires upstream support. - Commit: follow OpenCode repository convention. - - Status: pending upstream. + - Status: pending upstream; see [OpenCode plugin update behavior](https://opencode.ai/v2/docs/plugins). 4. **Rollout verification** - Owner: provider maintainer. - Dependency: provider release and an OpenCode build containing step 3. - - Evidence: compare the installed tarball manifest to its package version, launch from a clean cache, restart after refresh, and compare displayed provider IDs with the public availability response. + - Evidence: compare the installed tarball manifest to its package version, launch from a clean cache, apply a host update to a warm cache, restart, and compare displayed provider IDs with the public availability response. - Commit: none unless verification finds a defect. - Status: pending upstream. -Provider steps 1 and 2 shipped on 2026-09-20 before the upstream change. Until OpenCode releases step 3, users on stale mutable caches may still need one manual cache refresh; that temporary operational workaround is not part of the target behavior. +Provider steps 1 and 2 shipped on 2026-09-20. Until OpenCode adds automatic package installation for warm caches, users may need to apply the host update action manually; do not clear the cache as a workaround because it discards a working install. ## References diff --git a/docs/specs/2026-09-28-v2-parity.md b/docs/specs/2026-09-28-v2-parity.md index 2c090e9..479722c 100644 --- a/docs/specs/2026-09-28-v2-parity.md +++ b/docs/specs/2026-09-28-v2-parity.md @@ -34,15 +34,18 @@ and [Provider API](https://commandcode.ai/docs/provider) documentation. ## A — Endpoint-aware model routing The provider's live `/models` metadata is the route authority when -`supported_endpoints` is present. Models advertising Responses use the -Responses route per model; otherwise models advertising Messages use the -Anthropic route; models that advertise only Chat Completions use the existing -compatible route. V1 uses `@ai-sdk/openai` for Responses and the Anthropic SDK -for Messages. V2 uses `aisdk:@ai-sdk/openai` for Responses and -`aisdk:@ai-sdk/anthropic` for Messages. OpenCode 2.0.18 accepts the AI SDK -package route; its newer native `@opencode/ai` package is not present in that -tested image. If an older catalog lacks route metadata, Claude retains the -Messages fallback and other models retain Chat Completions. +`supported_endpoints` is present. Prefer Anthropic Messages when advertised, +then Chat Completions, and use Responses when it is the only advertised route. +The order avoids malformed `response.output_text.annotation.added` chunks seen +on the provider's Responses stream when a model also supports Chat Completions. +V1 uses `@ai-sdk/openai` for Responses and the Anthropic SDK for Messages. V2 +uses `aisdk:@ai-sdk/openai` for Responses and `aisdk:@ai-sdk/anthropic` for +Messages. OpenCode 2.0.18 accepts the AI SDK package route; its newer native +`@opencode/ai` package is not present in that tested image. If an older catalog +lacks route metadata, Claude retains the Messages fallback and other models +retain Chat Completions. The 0.10.1 catalog has 66 models advertising both Chat +Completions and Responses, 10 Messages-only, and 8 Chat-only; it has no +Responses-only model. The isolated GOAT-key probes for Claude Sonnet and Haiku returned `MODEL_NOT_IN_PLAN`. Claude Sonnet 4.6 reported that it is available on Pro and @@ -74,7 +77,7 @@ The strict catalog pipeline carries: 4. Existing modalities and costs without clearing known values when a metadata source is partial. -The current 82-model catalog has release dates for 81 entries, family for 77, +The current 84-model catalog has release dates for 82 entries, family for 78, input limits for 13, one beta model, and one deprecated model. These are generated-data counts, not fixed schema expectations. @@ -109,10 +112,12 @@ model schema cannot express. ## E — Documentation and current usage -README examples now use `plugin` for V1 and `plugins` for V2, describe both -auth surfaces and their distinct cost support, and state the plan/test limits. -The stable-model-identity spec describes the 0.9.1 baseline historically and -links here for current V2 auth and metadata behavior. +README examples use the bare unpinned npm package with `plugin` for V1 and +`plugins` for V2. They explain that V2 checks for package updates at startup but +does not silently replace its cache, describe both auth surfaces and their +distinct cost support, and state the plan/test limits. The stable-model-identity +spec describes the 0.9.1 baseline historically and links here for current V2 +auth and metadata behavior. ## Verification @@ -136,6 +141,15 @@ links here for current V2 auth and metadata behavior. - Claude plan-denial and disconnected-state results are described above; no higher-plan model request was made. -All live checks used temporary Docker homes and config files. The user's active -OpenCode setup, global configuration, auth files, and `@latest` resolution were -not changed. +Fresh isolated OpenCode V1.18.30 and V2.0.18 starts fetched npm `latest` +0.10.1 from the bare package entry. A separate isolated cache containing 0.9.1 +stayed on 0.9.1 at V2 startup, confirming that the warm-cache update still needs +the host's update action. The locally installed V2.0.18 binary loaded the +working-tree plugin through a temporary `file://` config and returned `OK` for a +GOAT-key DeepSeek V4.1 Flash request in standalone mode; that model now takes +Chat Completions when both Chat and Responses are advertised. The simple call +did not reproduce the reported malformed annotation event, so this verifies the +Chat route and local host integration rather than that exact stream failure. +The same working-tree plugin returned `OK` in the official OpenCode V1.18.30 +and V2.0.18 Docker images. The user's active config, session, and auth files +were not changed. diff --git a/src/catalog.ts b/src/catalog.ts index a8cc9c3..7d4de5c 100644 --- a/src/catalog.ts +++ b/src/catalog.ts @@ -797,9 +797,9 @@ export function modelApi(entry: Pick): return path === route || path.endsWith(`/${route}`); }) ?? false; - if (supports("responses")) return "responses"; if (supports("messages")) return "messages"; if (supports("chat/completions")) return "chat"; + if (supports("responses")) return "responses"; return usesAnthropicMessagesApi(entry.id) ? "messages" : "chat"; } diff --git a/tests/unit/catalog.test.ts b/tests/unit/catalog.test.ts index f1679dc..c1c338e 100644 --- a/tests/unit/catalog.test.ts +++ b/tests/unit/catalog.test.ts @@ -296,7 +296,7 @@ describe("generateOpencodeModels", () => { } }); - test("routes models through Responses when the availability API advertises it", () => { + test("uses Responses only when no Chat Completions route is advertised", () => { const models = generateOpencodeModels([ { id: "deepseek/deepseek-v4-flash", @@ -339,9 +339,7 @@ describe("generateOpencodeModels", () => { supported_endpoints: ["/v1/chat/completions"], }, ]); - expect((models["deepseek-v4-flash"] as Record).provider).toEqual({ - npm: "@ai-sdk/openai", - }); + expect((models["deepseek-v4-flash"] as Record).provider).toBeUndefined(); expect((models["gpt-5.5"] as Record).provider).toEqual({ npm: "@ai-sdk/openai", }); @@ -351,10 +349,19 @@ describe("generateOpencodeModels", () => { expect((models["gemini-3.5-flash"] as Record).provider).toBeUndefined(); }); - test("prefers advertised API routes and keeps the legacy Claude fallback", () => { + test("prefers Messages, then Chat Completions, then Responses and keeps the Claude fallback", () => { expect( modelApi({ id: "vendor/model", supported_endpoints: ["/v1/messages", "/v1/responses"] }), - ).toBe("responses"); + ).toBe("messages"); + expect( + modelApi({ + id: "deepseek/model", + supported_endpoints: ["/v1/chat/completions", "/v1/responses"], + }), + ).toBe("chat"); + expect(modelApi({ id: "openai/model", supported_endpoints: ["/v1/responses"] })).toBe( + "responses", + ); expect(modelApi({ id: "vendor/model", supported_endpoints: ["/v1/messages"] })).toBe( "messages", ); diff --git a/tests/unit/v2models.test.ts b/tests/unit/v2models.test.ts index 0eccace..ed773da 100644 --- a/tests/unit/v2models.test.ts +++ b/tests/unit/v2models.test.ts @@ -64,6 +64,13 @@ test("V2 maps advertised Responses and Messages packages", () => { supported_endpoints: ["/provider/v1/responses"], }).package, ).toBe("aisdk:@ai-sdk/openai"); + expect( + toV2Model({ + ...base, + id: "deepseek/deepseek-v4.1-flash", + supported_endpoints: ["/provider/v1/chat/completions", "/provider/v1/responses"], + }).package, + ).toBeUndefined(); expect( toV2Model({ ...base,