| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- /**
- * OpenCode v2 Plugin: Auto-discover models from OpenAI-compatible endpoints.
- *
- * Config-driven: iterates over the providers defined in opencode.jsonc and
- * discovers models for every provider whose `package` contains "openai-compatible"
- * AND which has opted in via `settings.discover_models === true`
- * (compat: `settings.modelsDiscovery.enabled === true`).
- *
- * The plugin never creates providers itself — it only augments existing ones.
- * Discovered models are merged into the provider's existing models (dedup by id).
- */
- import { Plugin, Model } from "@opencode/plugin";
- interface Target {
- id: string;
- baseURL: string;
- apiKey: string;
- }
- function isOpenAICompatible(pkg: unknown): boolean {
- return typeof pkg === "string" && pkg.toLowerCase().includes("openai-compatible");
- }
- function hasOptIn(settings: Record<string, unknown>): boolean {
- if (settings.discover_models === true) return true;
- const md = settings.modelsDiscovery;
- if (md && typeof md === "object" && (md as Record<string, unknown>).enabled === true) {
- return true;
- }
- return false;
- }
- async function fetchModels(
- baseURL: string,
- apiKey: string
- ): Promise<Model.Info[]> {
- const headers: Record<string, string> = { "Content-Type": "application/json" };
- if (apiKey) {
- headers["Authorization"] = `Bearer ${apiKey}`;
- }
- const res = await fetch(`${baseURL}/models`, { headers });
- if (!res.ok) {
- throw new Error(`HTTP ${res.status} from ${baseURL}/models`);
- }
- const json = await res.json();
- const rawModels = (json.data || []) as Array<Record<string, unknown>>;
- return rawModels.map((m) => {
- const modelId = typeof m.id === "string" ? m.id : String(m.id);
- const contextLength =
- typeof m.context_length === "number" ? m.context_length : 131072;
- const maxTokens = typeof m.max_tokens === "number" ? m.max_tokens : 32768;
- const inputMods = Array.isArray(m.input_modalities)
- ? (m.input_modalities as string[])
- : ["text"];
- const outputMods = Array.isArray(m.output_modalities)
- ? (m.output_modalities as string[])
- : ["text"];
- return {
- id: modelId,
- modelID: modelId,
- providerID: "", // filled in by caller
- name: modelId,
- capabilities: {
- tools: true,
- input: inputMods,
- output: outputMods,
- },
- variants: [],
- cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
- limit: {
- context: contextLength,
- output: Math.min(maxTokens, contextLength),
- },
- time: {
- released: Math.floor(Date.now() / 1000),
- },
- status: "active" as const,
- enabled: true,
- } as unknown as Model.Info;
- });
- }
- export default Plugin.define({
- id: "opencode.openai-compatible-model-discovery",
- async setup(ctx) {
- // 1. Collect discovery targets from configured providers (sync).
- const targets: Target[] = [];
- try {
- await ctx.provider.transform((editor) => {
- for (const rec of editor.list()) {
- const info = rec.provider as Record<string, unknown>;
- if (!isOpenAICompatible(info.package)) continue;
- const settings =
- info.settings && typeof info.settings === "object"
- ? (info.settings as Record<string, unknown>)
- : {};
- if (!hasOptIn(settings)) continue;
- const baseURL =
- typeof settings.baseURL === "string" ? settings.baseURL : undefined;
- if (!baseURL) {
- console.warn(
- `[openai-discovery] Provider "${info.id}" has no settings.baseURL — skipping`
- );
- continue;
- }
- const apiKey =
- typeof settings.apiKey === "string" ? settings.apiKey : "";
- targets.push({ id: String(info.id), baseURL, apiKey });
- }
- });
- } catch (error) {
- console.error(
- `[openai-discovery] Failed to read providers: ${error instanceof Error ? error.message : String(error)}`
- );
- return;
- }
- if (targets.length === 0) {
- console.log(
- "[openai-discovery] No opted-in openai-compatible providers found — nothing to do"
- );
- return;
- }
- // 2. Discover + merge per target (isolated errors).
- for (const target of targets) {
- try {
- const models = await fetchModels(target.baseURL, target.apiKey);
- // stamp providerID
- for (const m of models) {
- (m as { providerID: string }).providerID = target.id;
- }
- await ctx.provider.transform((editor) => {
- const source = editor.get(target.id);
- if (!source) {
- console.warn(
- `[openai-discovery] Provider "${target.id}" no longer exists — skipping merge`
- );
- return;
- }
- const existing = [...source.models.values()];
- const existingIDs = new Set(existing.map((model) => String(model.id)));
- const merged = [
- ...existing,
- ...models.filter((m) => !existingIDs.has(String(m.id))),
- ];
- editor.models.set(target.id, merged);
- });
- console.log(
- `[openai-discovery] Registered ${models.length} models in provider "${target.id}"`
- );
- } catch (error) {
- console.error(
- `[openai-discovery] Failed to discover models for "${target.id}": ${error instanceof Error ? error.message : String(error)}`
- );
- }
- }
- },
- });
|