/** * OpenCode v2 Plugin: Auto-discover models from OpenAI-compatible endpoints. * * Config-driven: reads providers from opencode.jsonc (project or global). * For every provider whose `package` contains "openai-compatible" (and which * has opted in via `settings.discover_models === true`, or no opt-in flag set * for backward compat), fetches /models and registers them under that provider. * Creates the provider if it does not exist yet. */ import { Plugin, Model } from "@opencode/plugin"; function parseJsonc(text: string): any { try { return JSON.parse(text); } catch {} let s = text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); s = s.replace(/,\s*([}\]])/g, "$1"); return JSON.parse(s); } function readConfigProviders(): Record { const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const candidates = [ path.join(process.cwd(), "opencode.jsonc"), path.join(process.cwd(), "opencode.json"), path.join(os.homedir(), ".config", "opencode", "opencode.jsonc"), ]; const merged: Record = {}; for (const c of candidates) { try { if (!fs.existsSync(c)) continue; const cfg = parseJsonc(fs.readFileSync(c, "utf8")); const provs = cfg?.providers || cfg?.provider; if (provs && typeof provs === "object") { for (const [k, v] of Object.entries(provs)) { if (!merged[k]) merged[k] = v; } } } catch {} } return merged; } 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): boolean { // backward compat: if no discover_models flag present, default to enabled if (settings.discover_models === undefined && settings.modelsDiscovery === undefined) return true; if (settings.discover_models === true) return true; const md = settings.modelsDiscovery; if (md && typeof md === "object" && (md as Record).enabled === true) return true; if (settings.discover_models === false) return false; return false; } async function fetchModels(baseURL: string, apiKey: string): Promise { const headers: Record = { "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>; 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: "", 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) { const providers = readConfigProviders(); console.error("[openai-discovery] config providers:", Object.keys(providers)); const targets: Target[] = []; for (const [id, info] of Object.entries(providers)) { const p = info as Record; if (!isOpenAICompatible(p.package)) continue; const settings = p.settings && typeof p.settings === "object" ? (p.settings as Record) : {}; if (!hasOptIn(settings)) continue; const baseURL = typeof settings.baseURL === "string" ? settings.baseURL : undefined; if (!baseURL) { console.warn(`[openai-discovery] Provider "${id}" has no settings.baseURL — skipping`); continue; } const apiKey = typeof settings.apiKey === "string" ? settings.apiKey : ""; targets.push({ id, baseURL, apiKey }); } if (targets.length === 0) { console.log("[openai-discovery] No opted-in openai-compatible providers found — nothing to do"); return; } for (const target of targets) { try { const models = await fetchModels(target.baseURL, target.apiKey); 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) { 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); } else { editor.add({ info: { id: target.id, name: target.id, activation: "enabled" as const, package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: target.baseURL, apiKey: target.apiKey } } as never, models }); } }); 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)}`); } } }, });