Official

opencode-v2

OpenCode 2 agent provider: runs opencode serve (2.x) and talks to its /api 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-v2@0.1.0

Permissions in 0.1.0

  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode 2 agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode 2 server (`opencode serve`, or the binary you choose in Settings) and read its versionPrograms: opencode${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.Read the user name and password of the OpenCode serverVariables: OPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

Files

options.ts6.2 KB
// The config options the plugin offers, built from the live catalog only
// (`GET /api/provider`, `/api/model`, `/api/model/default`, `/api/agent`).
// There is no list of models, modes or reasoning levels in this plugin: an
// empty catalog is an error the user sees.
import type { ConfigChoice, ConfigOption } from "convergence/protocol";
import { permissionMode } from "../sdk/agent.ts";
import type { Agent, Model, ModelRef, Provider } from "./api.ts";

/// Option ids; also the keys of a session's stored options.
export const OPTION_MODEL = "model";
export const OPTION_AGENT = "agent";
export const OPTION_VARIANT = "variant";

/// Everything the options are built from, read once per workspace.
export interface Catalog {
  providers: readonly Provider[];
  models: readonly Model[];
  defaultModel: ModelRef | null;
  agents: readonly Agent[];
}

/// Display order of reasoning variants, least effort first. Only the order:
/// the names always come from the API. Unknown names follow, alphabetically.
const VARIANT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];

/// The option value of a model: `<providerID>/<id>`. The id is the
/// catalog entry's own (`gpt-6.1-sol-fast`), not the provider's model name,
/// because two entries can share one provider model.
export const modelValue = (ref: { providerID: string; id: string }) => `${ref.providerID}/${ref.id}`;

/// `<providerID>/<id>` as a model reference; a model id may contain slashes.
export function splitModel(value: unknown): { providerID: string; id: string } | null {
  if (typeof value !== "string") return null;
  const at = value.indexOf("/");
  if (at <= 0 || at === value.length - 1) return null;
  return { providerID: value.slice(0, at), id: value.slice(at + 1) };
}

/// A model's reasoning variants, ordered by effort.
export function variantChoices(model: Model): ConfigChoice[] {
  const rank = (name: string) => {
    const at = VARIANT_ORDER.indexOf(name);
    return at < 0 ? VARIANT_ORDER.length : at;
  };
  return (model.variants ?? [])
    .map((variant) => variant.id)
    .sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))
    .map((id) => ({ value: id, name: id }));
}

/// The models a chat can pick: enabled ones of the providers the server
/// has connected, in the server's order.
export function usableModels(catalog: Catalog): Model[] {
  const connected = new Set(catalog.providers.map((provider) => provider.id));
  return catalog.models.filter((model) => model.enabled !== false && connected.has(model.providerID));
}

/// The catalog entry a stored model value names.
export function findModel(catalog: Catalog, value: unknown): Model | null {
  const ref = splitModel(value);
  if (!ref) return null;
  return catalog.models.find((model) => model.providerID === ref.providerID && model.id === ref.id) ?? null;
}

/// Every option for a workspace: model (grouped by provider), mode (the
/// primary agents), reasoning, and the permission mode. `current` holds the
/// values a chat chose.
export function buildOptions(catalog: Catalog, current: Record<string, unknown> = {}): ConfigOption[] {
  const providerNames = new Map(catalog.providers.map((provider) => [provider.id, provider.name]));
  const models = usableModels(catalog);
  const first = models[0];
  if (!first)
    throw new Error("OpenCode has no connected provider with an enabled model; connect one with `opencode auth login`");
  const choices: ConfigChoice[] = models.map((model) => {
    const levels = variantChoices(model);
    return {
      value: modelValue(model),
      name: model.name,
      group: providerNames.get(model.providerID) ?? model.providerID,
      ...(levels.length ? { reasoningLevels: levels } : {}),
    };
  });
  const stored = current[OPTION_MODEL];
  const fallback = catalog.defaultModel ? modelValue(catalog.defaultModel) : modelValue(first);
  const selected = typeof stored === "string" ? stored : fallback;
  const options: ConfigOption[] = [
    { id: OPTION_MODEL, name: "Model", category: "model", kind: "select", value: selected, choices },
  ];

  const modes: ConfigChoice[] = catalog.agents
    .filter((agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"))
    .map((agent) => ({
      value: agent.id,
      name: agent.name,
      ...(agent.description ? { description: agent.description } : {}),
    }));
  const firstMode = modes[0];
  if (firstMode) {
    const mode = current[OPTION_AGENT];
    options.push({
      id: OPTION_AGENT,
      name: "Mode",
      category: "mode",
      kind: "select",
      value: typeof mode === "string" ? mode : firstMode.value,
      choices: modes,
    });
  }

  // Kept whenever any model has variants, so a draft can switch to a
  // reasoning model before a session exists; `null` is the provider
  // default until the user picks one.
  if (choices.some((choice) => choice.reasoningLevels?.length)) {
    const chosen = choices.find((choice) => choice.value === selected);
    options.push({
      id: OPTION_VARIANT,
      name: "Reasoning",
      category: "reasoning",
      kind: "select",
      value: current[OPTION_VARIANT] ?? null,
      choices: chosen?.reasoningLevels ?? [],
    });
  }

  // OpenCode has no approval reviewer, so Auto is left out rather than
  // offered as another name for Supervised.
  options.push(permissionMode.option(permissionMode.selected(current), false));
  return options;
}

/// The model reference a chat's options ask for, `null` for the server's
/// default. A variant the chosen model does not have is dropped.
export function chosenModel(catalog: Catalog, options: Record<string, unknown>): ModelRef | null {
  const model = findModel(catalog, options[OPTION_MODEL]);
  if (!model) return null;
  const variant = options[OPTION_VARIANT];
  const known = typeof variant === "string" && (model.variants ?? []).some((entry) => entry.id === variant);
  return { providerID: model.providerID, id: model.id, ...(known ? { variant } : {}) };
}

/// The context window of a model, when the catalog names one.
export function contextWindow(catalog: Catalog, ref: { providerID: string; id: string }): number | null {
  const model = catalog.models.find((entry) => entry.providerID === ref.providerID && entry.id === ref.id);
  const context = model?.limit?.context;
  return context && context > 0 ? context : null;
}

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.1.0latestOct 5, 2026>=2 <345.1 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.