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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ For published plugin versions, the six-hour CI schedule extracts the latest `com
- **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 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.
- **Version-specific registration** — V1 uses the `plugin` config key, direct AI SDK packages, the `server()` provider map, and its auth callback. V2 uses `plugins`, OpenCode's native provider runtimes, 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.

## Quick Start
Expand Down Expand Up @@ -64,7 +64,7 @@ The bare package name is unpinned and resolves npm's `latest` release when OpenC

### 2. No provider block needed

On OpenCode V2 the plugin registers the `commandcode` provider, its models, and its API base URL through the V2 provider API. On V1 the `server` hook fills `provider.commandcode` defaults — `npm: "@ai-sdk/openai-compatible"` plus the Provider API `baseURL`; the plugin package itself is never the SDK `npm` field. Only add a manual provider entry if you need non-default transport options.
On OpenCode V2 the plugin registers the `commandcode` provider, its models, the native OpenCode runtime, and its API base URL through the V2 provider API. On V1 the `server` hook fills `provider.commandcode` defaults — `npm: "@ai-sdk/openai-compatible"` plus the Provider API `baseURL`; the plugin package itself is never the SDK `npm` field. Only add a manual provider entry if you need non-default transport options.

### 3. Connect

Expand Down
31 changes: 22 additions & 9 deletions docs/specs/2026-09-28-v2-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ remain for future models that advertise Responses without another route, but
the current catalog has no such entry. Other models' Responses tool-call streams
were not individually live-tested.
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
uses OpenCode's native `@opencode/ai/providers/openai-compatible`,
`@opencode/ai/providers/openai/responses`, and
`@opencode/ai/providers/anthropic` runtimes. The legacy `aisdk:` adapter did
not reliably carry provider settings into model requests: image requests and
an existing image-bearing session returned `Invalid input` until a manual
provider base URL was supplied. The native runtime preserves the plugin's base
URL and requires no manual provider block. If an older catalog
lacks route metadata, Claude retains the Messages fallback and other models
retain Chat Completions. The current 84-model catalog has 66 models advertising
both Chat Completions and Responses, 10 Messages-only, and 8 Chat-only; it has
Expand Down Expand Up @@ -142,10 +146,11 @@ auth and metadata behavior.
`/api/integration` exposed the `key` and `env` methods. Both images returned
`OK` for a GOAT-eligible DeepSeek V4.1 Flash smoke prompt; the current V2
local pin also returned `OK` for DeepSeek V4 Flash.
- Responses routing passed both official Docker images against a temporary
local Responses stream: V1 sent `POST /provider/v1/responses` via
`@ai-sdk/openai`; V2 sent the same route via `aisdk:@ai-sdk/openai`.
No live Command Code inference was made for this check.
- Earlier Responses routing checks passed both official Docker images against a
temporary local stream through the legacy adapters. The current mapping unit
test selects OpenCode's native Responses runtime for future Responses-only
models; the current catalog has no such model, so no live inference exercises
that route.
- Claude plan-denial and disconnected-state results are described above; no
higher-plan model request was made.

Expand All @@ -162,5 +167,13 @@ allowed only a `printf*` shell command. The same `printf TOOL_CALL_SMOKE_OK`
tool request succeeded on the installed local V2.0.18 binary and in the
official V1.18.30 and V2.0.18 Docker images. These runs verify the formerly
failing model/variant/tool-call path through the new Chat route; they do not
validate every model's Responses stream. The user's active config, session, and
auth files were not changed.
validate every model's Responses stream.

The native V2 runtime fix was then verified on local OpenCode 2.0.19 with a
synthetic PNG and an exported clone of the original 121-message failing session;
both completed with the image still present. Official Docker V2.0.18 returned
`OK` through the same local plugin, official Docker V1.18.30 still returned
`OK` through its unchanged V1 runtime, and the Claude Messages route reached
the provider's expected `MODEL_NOT_IN_PLAN` response. The active config was
temporarily pinned to the local checkout for these checks; its backup matched
before the swap, and no real session or auth record was modified.
14 changes: 7 additions & 7 deletions plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { ManifestSchema, ModelEntrySchema, PluginFileConfigSchema } from "./src/

const { models: MODELS_PATH, manifest: MANIFEST_PATH, version: VERSION_PATH } = catalogPaths();

// Transport decision (docs/2026-08-28-ci-catalog/spec.md:20): OpenAI-compatible AI SDK +
// Provider API; this package is the plugin, never the SDK `npm` field. V1 takes the bare
// npm name; the `aisdk:` prefix is V2-only.
const PROVIDER_SDK_NPM = "@ai-sdk/openai-compatible";
// V1 loads the AI SDK package directly. V2 uses OpenCode's native runtime so provider
// settings such as baseURL survive registry materialization.
const V1_PROVIDER_SDK_NPM = "@ai-sdk/openai-compatible";
const V2_PROVIDER_PACKAGE = "@opencode/ai/providers/openai-compatible";
const PROVIDER_API_BASE_URL = "https://api.commandcode.ai/provider/v1";

type PluginFileConfig = z.infer<typeof PluginFileConfigSchema>;
Expand Down Expand Up @@ -239,13 +239,13 @@ export async function server() {
const pluginCfg = loadPluginConfig();
const debug = pluginCfg.debugStartupLogs === true;

if (!cc.npm) cc.npm = PROVIDER_SDK_NPM;
if (!cc.npm) cc.npm = V1_PROVIDER_SDK_NPM;
if (!cc.name) cc.name = "Command Code";
if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"];
// V1 resolves `npm` through BunProc.install and calls the first `create*` export, so
// the SDK package must be the OpenAI-compatible one; give it the Provider API URL
// unless the user already configured another baseURL (custom npm stays untouched).
if (cc.npm === PROVIDER_SDK_NPM) {
if (cc.npm === V1_PROVIDER_SDK_NPM) {
if (cc.options === undefined) cc.options = {};
if (typeof cc.options === "object" && cc.options !== null) {
const options = cc.options as Record<string, unknown>;
Expand Down Expand Up @@ -307,7 +307,7 @@ async function setup(ctx: any): Promise<void> {
activation: "auto",
integrationID: "commandcode",
env: ["COMMANDCODE_API_KEY"],
package: `aisdk:${PROVIDER_SDK_NPM}`,
package: V2_PROVIDER_PACKAGE,
settings: {
baseURL: PROVIDER_API_BASE_URL,
},
Expand Down
4 changes: 2 additions & 2 deletions src/v2models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export function toV2Model(entry: ModelEntry): V2Model {
},
};
const api = modelApi(entry);
if (api === "responses") model.package = "aisdk:@ai-sdk/openai";
else if (api === "messages") model.package = "aisdk:@ai-sdk/anthropic";
if (api === "responses") model.package = "@opencode/ai/providers/openai/responses";
else if (api === "messages") model.package = "@opencode/ai/providers/anthropic";
return model;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/plugin-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ test("V2 setup adds provider with models when missing", async () => {
expect(calls.map((call) => call.name)).toEqual(["provider.transform", "integration.transform"]);
expect(added.info.id).toBe("commandcode");
expect(added.info.activation).toBe("auto");
expect(added.info.package).toBe("aisdk:@ai-sdk/openai-compatible");
expect(added.info.package).toBe("@opencode/ai/providers/openai-compatible");
expect(added.info.integrationID).toBe("commandcode");
expect(added.info.env).toEqual(["COMMANDCODE_API_KEY"]);
expect(added.info.settings.apiKey).toBeUndefined();
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/v2models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ test("Claude catalog models use the Anthropic Messages package", () => {
"claude-haiku-4-5-20251001",
];
const models = generateV2Models(ids.map((id) => ({ ...base, id })));
expect(models.map((model) => model.package)).toEqual(ids.map(() => "aisdk:@ai-sdk/anthropic"));
expect(models.map((model) => model.package)).toEqual(
ids.map(() => "@opencode/ai/providers/anthropic"),
);
});

test("V2 maps advertised Responses and Messages packages", () => {
Expand All @@ -63,7 +65,7 @@ test("V2 maps advertised Responses and Messages packages", () => {
id: "deepseek/deepseek-v4-flash",
supported_endpoints: ["/provider/v1/responses"],
}).package,
).toBe("aisdk:@ai-sdk/openai");
).toBe("@opencode/ai/providers/openai/responses");
expect(
toV2Model({
...base,
Expand All @@ -77,7 +79,7 @@ test("V2 maps advertised Responses and Messages packages", () => {
id: "vendor/messages-model",
supported_endpoints: ["/v1/messages"],
}).package,
).toBe("aisdk:@ai-sdk/anthropic");
).toBe("@opencode/ai/providers/anthropic");
});

test("V2 maps models.dev release and catalog metadata", () => {
Expand Down
Loading