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.ts8.8 KB
// The config options the plugin offers, built from the live catalog only
// (`GET /config/providers`, `GET /agent`). There is no list of models,
// modes or reasoning levels anywhere in this plugin: an empty catalog is
// an error the user sees.
import { permissionMode } from "../sdk/agent.ts";
import * as z from "zod";
import type { ConfigChoice, ConfigOption } from "convergence/protocol";
import { obj } from "./map.ts";

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

export interface VariantChoice {
  value: string;
  name: string;
}

export interface ModelChoice extends ConfigChoice {
  value: string;
  name: string;
  group?: string;
  reasoningLevels?: VariantChoice[];
}

const modelSchema = z.looseObject({
  id: z.unknown().optional(),
  name: z.unknown().optional(),
  release_date: z.unknown().optional(),
  variants: z.unknown().optional(),
  limit: z.unknown().optional(),
});
const providerSchema = z.looseObject({
  id: z.unknown().optional(),
  name: z.unknown().optional(),
  models: z.unknown().optional(),
});
const agentSchema = z.looseObject({
  name: z.unknown().optional(),
  description: z.unknown().optional(),
  mode: z.unknown().optional(),
  hidden: z.unknown().optional(),
});
const catalogSchema = z.looseObject({
  providers: z.array(z.unknown()).optional(),
  default: z.unknown().optional(),
});

interface CatalogModel {
  id?: unknown;
  name?: unknown;
  release_date?: unknown;
  variants?: unknown;
  limit?: unknown;
}

export interface Providers {
  providers?: unknown;
  default?: unknown;
}

export interface AgentEntry {
  name: string;
  description: string | null;
  mode: string;
  hidden: boolean;
}

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

/// `"<provider>/<model>"` as `[provider, model]`; a model id may itself
/// contain slashes. `null` when either side is empty.
export function splitModel(value: unknown): [string, string] | null {
  if (typeof value !== "string") return null;
  const at = value.indexOf("/");
  if (at <= 0 || at === value.length - 1) return null;
  return [value.slice(0, at), value.slice(at + 1)];
}

/// A model's reasoning choices, ordered by effort.
export function variantChoices(model: { variants?: unknown }): VariantChoice[] {
  const variants = model?.variants && typeof model.variants === "object" ? Object.keys(model.variants) : [];
  const rank = (name: string): number => {
    const at = VARIANT_ORDER.indexOf(name);
    return at < 0 ? VARIANT_ORDER.length : at;
  };
  variants.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
  return variants.map((name) => ({ value: name, name }));
}

/// The reasoning option: kept in the catalog whenever any model has
/// variants (so a draft can switch to a reasoning model before a session
/// exists), with the chosen model's variants as its choices and `null`
/// ("the provider default") until the user picks one.
export function reasoningOption(
  models: readonly { value: string; reasoningLevels?: VariantChoice[] }[],
  selected: unknown,
  value: unknown,
): ConfigOption | null {
  if (!models.some((model) => model.reasoningLevels?.length)) return null;
  const chosen = models.find((model) => model.value === selected);
  return {
    id: OPTION_VARIANT,
    name: "Reasoning",
    category: "reasoning",
    kind: "select",
    value: value ?? null,
    choices: chosen?.reasoningLevels ?? [],
  };
}

const lower = (text: unknown): string => String(text ?? "").toLowerCase();

/// Every option for a workspace: model (grouped by provider, newest
/// first), mode (the primary agents), reasoning, and the permission mode.
/// `providers` is `GET /config/providers`, `agents` is `GET /agent`,
/// `current` the values a chat chose.
export function buildOptions(
  providers: unknown,
  agents: unknown,
  current: Record<string, unknown> = {},
): ConfigOption[] {
  const parsedCatalog = catalogSchema.safeParse(providers);
  const catalog = parsedCatalog.success ? parsedCatalog.data : {};
  const modelChoices: ModelChoice[] = [];
  let defaultModel: string | null = null;
  const defaults = catalog.default && typeof catalog.default === "object" ? catalog.default : {};
  for (const raw of Array.isArray(catalog.providers) ? catalog.providers : []) {
    const parsedProvider = providerSchema.safeParse(raw);
    if (!parsedProvider.success || typeof parsedProvider.data.id !== "string") continue;
    const provider = parsedProvider.data;
    const providerId = parsedProvider.data.id;
    const models: CatalogModel[] = [];
    for (const entry of Object.values(provider.models && typeof provider.models === "object" ? provider.models : {})) {
      const parsedModel = modelSchema.safeParse(entry);
      if (parsedModel.success && typeof parsedModel.data.id === "string") models.push(parsedModel.data);
    }
    // Newest first, then by name; the catalog itself is unordered.
    models.sort((a, b) => {
      const left = typeof a.release_date === "string" ? a.release_date : "";
      const right = typeof b.release_date === "string" ? b.release_date : "";
      if (left !== right) return left < right ? 1 : -1;
      const an = lower(a.name ?? a.id);
      const bn = lower(b.name ?? b.id);
      return an < bn ? -1 : an > bn ? 1 : 0;
    });
    const own = obj(defaults)[providerId];
    const providerModels = obj(provider.models);
    if (defaultModel === null && typeof own === "string" && providerModels[own]) defaultModel = `${providerId}/${own}`;
    for (const model of models) {
      if (typeof model.id !== "string") continue;
      const modelId = model.id;
      const choice: ModelChoice = {
        value: `${providerId}/${modelId}`,
        name: typeof model.name === "string" && model.name ? model.name : modelId,
        group: typeof provider.name === "string" && provider.name ? provider.name : providerId,
      };
      const levels = variantChoices(model);
      if (levels.length) choice.reasoningLevels = levels;
      modelChoices.push(choice);
    }
  }
  if (!modelChoices.length) throw new Error("opencode has no connected providers; run `opencode auth login` first");

  const options: ConfigOption[] = [];
  const first = modelChoices[0];
  if (!first) throw new Error("opencode has no connected providers; run `opencode auth login` first");
  const currentModel = current[OPTION_MODEL];
  const modelValue = typeof currentModel === "string" ? currentModel : (defaultModel ?? first.value);
  options.push({
    id: OPTION_MODEL,
    name: "Model",
    category: "model",
    kind: "select",
    value: modelValue,
    choices: modelChoices,
  });

  const agentChoices: ConfigChoice[] = [];
  for (const raw of Array.isArray(agents) ? agents : []) {
    const parsed = agentSchema.safeParse(raw);
    if (!parsed.success) continue;
    const agent = parsed.data;
    if (typeof agent.name !== "string" || agent.hidden) continue;
    if (agent.mode !== "primary" && agent.mode !== "all") continue;
    const choice: ConfigChoice = { value: agent.name, name: agent.name };
    if (typeof agent.description === "string" && agent.description) choice.description = agent.description;
    agentChoices.push(choice);
  }
  if (agentChoices.length) {
    const currentAgent = current[OPTION_AGENT];
    const head = agentChoices[0];
    const value = typeof currentAgent === "string" ? currentAgent : head ? head.value : "";
    options.push({ id: OPTION_AGENT, name: "Mode", category: "mode", kind: "select", value, choices: agentChoices });
  }

  const reasoning = reasoningOption(modelChoices, modelValue, current[OPTION_VARIANT]);
  if (reasoning) options.push(reasoning);

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

/// The context window of `provider/model` in a catalog, when it names one.
export function contextWindow(providers: unknown, providerId: string, modelId: string): number | null {
  const parsedCatalog = catalogSchema.safeParse(providers);
  const entries =
    parsedCatalog.success && Array.isArray(parsedCatalog.data.providers) ? parsedCatalog.data.providers : [];
  let models: unknown = null;
  for (const raw of entries) {
    const parsed = providerSchema.safeParse(raw);
    if (parsed.success && parsed.data.id === providerId) {
      models = parsed.data.models;
      break;
    }
  }
  const model = obj(models)[modelId];
  const parsedModel = modelSchema.safeParse(model);
  if (!parsedModel.success) return null;
  const context = obj(parsedModel.data.limit)["context"];
  return typeof context === "number" && context > 0 ? context : null;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.