Official

opencode

OpenCode agent provider: runs opencode serve and talks to it over HTTP and server-sent events.

The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/opencode@0.2.0

Permissions in 0.2.0

  • Read files fs.readMediumReads files in the listed places.Read, once, the servers the previous OpenCode provider keptPlaces: its own data folder
  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode server (`opencode serve`, or the binary you choose in Settings), read its catalog from the command line when the server cannot answer, upgrade it (`opencode upgrade`), and ask or tell the npm installation that owns it about a newer versionPrograms: opencodenpm${settings.binaryPath}
  • Network access netMediumConnects to the listed hosts.Talk to the OpenCode server it starts on this computer, at the port it picks for each launch, and to the external server you choose in SettingsHosts: localhost:*${settings.serverUrl}
  • Environment variables envMediumReads the listed environment variables.Expand ~ in a configured binary, and read the OpenCode settings you set in the environment: the binary to run, an external server's address, and the server's user name and passwordVariables: HOMEOPENCODE_PATHOPENCODE_SERVER_URLOPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

Files

options.test.ts4.7 KB
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildOptions, contextWindow, reasoningOption, splitModel, variantChoices } from "./options.ts";

test("model values split on the provider; a model id may hold slashes", () => {
  assert.deepEqual(splitModel("opencode/big-pickle"), ["opencode", "big-pickle"]);
  assert.deepEqual(splitModel("lmstudio/qwen/qwen3-coder-30b"), ["lmstudio", "qwen/qwen3-coder-30b"]);
  assert.equal(splitModel("nope"), null);
  assert.equal(splitModel("/x"), null);
  assert.equal(splitModel(null), null);
});

test("variants are ordered by effort, unknown names after", () => {
  const model = { variants: { high: {}, none: {}, medium: {}, low: {}, xhigh: {}, turbo: {} } };
  assert.deepEqual(
    variantChoices(model).map((choice) => choice.value),
    ["none", "low", "medium", "high", "xhigh", "turbo"],
  );
  assert.deepEqual(variantChoices({}), []);
});

test("the reasoning option survives a model without variants", () => {
  const models = [{ value: "plain" }, { value: "thinking", reasoningLevels: [{ value: "custom", name: "custom" }] }];
  const initial = reasoningOption(models, "plain", undefined);
  assert.deepEqual(initial?.choices, []);
  assert.equal(initial?.value, null);
  const selected = reasoningOption(models, "thinking", "custom");
  assert.deepEqual(selected?.choices, models[1]?.reasoningLevels);
  assert.equal(selected?.value, "custom");
  assert.equal(reasoningOption(models.slice(0, 1), "plain", undefined), null);
});

const PROVIDERS = {
  providers: [
    {
      id: "opencode",
      name: "OpenCode Zen",
      models: {
        "big-pickle": {
          id: "big-pickle",
          name: "Big Pickle",
          release_date: "2025-10-17",
          variants: {},
          limit: { context: 200000 },
        },
        "free-new": { id: "free-new", name: "Free New", release_date: "2026-02-01", variants: { high: {}, low: {} } },
      },
    },
    { id: "openai", name: "OpenAI", models: { "gpt-5": { id: "gpt-5", name: "GPT-5", variants: { medium: {} } } } },
  ],
  default: { openai: "gpt-5", opencode: "big-pickle" },
};
const AGENTS = [
  { name: "build", mode: "primary", description: "The default agent" },
  { name: "explore", mode: "subagent" },
  { name: "plan", mode: "all" },
  { name: "title", mode: "primary", hidden: true },
];

test("the options come from the live catalog: models grouped and newest first, primary modes, reasoning, permissions", () => {
  const options = buildOptions(PROVIDERS, AGENTS, {});
  assert.deepEqual(
    options.map((option) => option.id),
    ["model", "agent", "variant", "permission_mode"],
  );
  const [model, mode, reasoning, permission] = options;
  assert.deepEqual(
    model?.choices?.map((choice) => [choice.value, choice.group]),
    [
      ["opencode/free-new", "OpenCode Zen"],
      ["opencode/big-pickle", "OpenCode Zen"],
      ["openai/gpt-5", "OpenAI"],
    ],
  );
  // The first provider with a default names the initial model.
  assert.equal(model?.value, "opencode/big-pickle");
  assert.deepEqual(
    model?.choices?.[0]?.reasoningLevels?.map((level) => level.value),
    ["low", "high"],
  );
  assert.equal(model?.choices?.[1]?.reasoningLevels, undefined, "a model without variants has no reasoning control");
  assert.deepEqual(
    mode?.choices?.map((choice) => choice.value),
    ["build", "plan"],
  );
  assert.equal(mode?.value, "build");
  assert.equal(reasoning?.value, null, "the provider default until the user picks one");
  assert.deepEqual(reasoning?.choices, []);
  // Without an approval reviewer the composer must not offer Auto.
  assert.deepEqual(
    permission?.choices?.map((choice) => choice.value),
    ["supervised", "auto_edits", "full"],
  );

  const chosen = buildOptions(PROVIDERS, AGENTS, {
    model: "opencode/free-new",
    variant: "high",
    agent: "plan",
    permission_mode: "full",
  });
  assert.equal(chosen[0]?.value, "opencode/free-new");
  assert.equal(chosen[1]?.value, "plan");
  assert.equal(chosen[2]?.value, "high");
  assert.deepEqual(
    chosen[2]?.choices?.map((choice) => choice.value),
    ["low", "high"],
  );
  assert.equal(chosen[3]?.value, "full");
});

test("an empty catalog is an error, never an invented list", () => {
  assert.throws(
    () => buildOptions({ providers: [], default: {} }, AGENTS, {}),
    /no connected providers; run `opencode auth login` first/,
  );
  assert.throws(() => buildOptions(null, [], {}), /no connected providers/);
});

test("the context window comes from the catalog entry", () => {
  assert.equal(contextWindow(PROVIDERS, "opencode", "big-pickle"), 200000);
  assert.equal(contextWindow(PROVIDERS, "openai", "gpt-5"), null);
  assert.equal(contextWindow(PROVIDERS, "nobody", "x"), null);
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <394.8 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.