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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.1.0latest | Oct 5, 2026 | >=2 <3 | 45.1 KB | 4 permissions | Listed |
No comments yet.