index.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * OpenCode v2 Plugin: Auto-discover models from OpenAI-compatible endpoints.
  3. *
  4. * Config-driven: reads providers from opencode.jsonc (project or global).
  5. * For every provider whose `package` contains "openai-compatible" (and which
  6. * has opted in via `settings.discover_models === true`, or no opt-in flag set
  7. * for backward compat), fetches /models and registers them under that provider.
  8. * Creates the provider if it does not exist yet.
  9. */
  10. import { Plugin, Model } from "@opencode/plugin";
  11. function parseJsonc(text: string): any {
  12. try { return JSON.parse(text); } catch {}
  13. let s = text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
  14. s = s.replace(/,\s*([}\]])/g, "$1");
  15. return JSON.parse(s);
  16. }
  17. function readConfigProviders(): Record<string, any> {
  18. const fs = require("node:fs");
  19. const path = require("node:path");
  20. const os = require("node:os");
  21. const candidates = [
  22. path.join(process.cwd(), "opencode.jsonc"),
  23. path.join(process.cwd(), "opencode.json"),
  24. path.join(os.homedir(), ".config", "opencode", "opencode.jsonc"),
  25. ];
  26. const merged: Record<string, any> = {};
  27. for (const c of candidates) {
  28. try {
  29. if (!fs.existsSync(c)) continue;
  30. const cfg = parseJsonc(fs.readFileSync(c, "utf8"));
  31. const provs = cfg?.providers || cfg?.provider;
  32. if (provs && typeof provs === "object") {
  33. for (const [k, v] of Object.entries(provs)) {
  34. if (!merged[k]) merged[k] = v;
  35. }
  36. }
  37. } catch {}
  38. }
  39. return merged;
  40. }
  41. interface Target { id: string; baseURL: string; apiKey: string; }
  42. function isOpenAICompatible(pkg: unknown): boolean {
  43. return typeof pkg === "string" && pkg.toLowerCase().includes("openai-compatible");
  44. }
  45. function hasOptIn(settings: Record<string, unknown>): boolean {
  46. // backward compat: if no discover_models flag present, default to enabled
  47. if (settings.discover_models === undefined && settings.modelsDiscovery === undefined) return true;
  48. if (settings.discover_models === true) return true;
  49. const md = settings.modelsDiscovery;
  50. if (md && typeof md === "object" && (md as Record<string, unknown>).enabled === true) return true;
  51. if (settings.discover_models === false) return false;
  52. return false;
  53. }
  54. async function fetchModels(baseURL: string, apiKey: string): Promise<Model.Info[]> {
  55. const headers: Record<string, string> = { "Content-Type": "application/json" };
  56. if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
  57. const res = await fetch(`${baseURL}/models`, { headers });
  58. if (!res.ok) throw new Error(`HTTP ${res.status} from ${baseURL}/models`);
  59. const json = await res.json();
  60. const rawModels = (json.data || []) as Array<Record<string, unknown>>;
  61. return rawModels.map((m) => {
  62. const modelId = typeof m.id === "string" ? m.id : String(m.id);
  63. const contextLength = typeof m.context_length === "number" ? m.context_length : 131072;
  64. const maxTokens = typeof m.max_tokens === "number" ? m.max_tokens : 32768;
  65. const inputMods = Array.isArray(m.input_modalities) ? (m.input_modalities as string[]) : ["text"];
  66. const outputMods = Array.isArray(m.output_modalities) ? (m.output_modalities as string[]) : ["text"];
  67. return {
  68. id: modelId, modelID: modelId, providerID: "", name: modelId,
  69. capabilities: { tools: true, input: inputMods, output: outputMods },
  70. variants: [], cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
  71. limit: { context: contextLength, output: Math.min(maxTokens, contextLength) },
  72. time: { released: Math.floor(Date.now() / 1000) },
  73. status: "active" as const, enabled: true,
  74. } as unknown as Model.Info;
  75. });
  76. }
  77. export default Plugin.define({
  78. id: "opencode.openai-compatible-model-discovery",
  79. async setup(ctx) {
  80. const providers = readConfigProviders();
  81. const targets: Target[] = [];
  82. for (const [id, info] of Object.entries(providers)) {
  83. const p = info as Record<string, unknown>;
  84. if (!isOpenAICompatible(p.package)) continue;
  85. const settings = p.settings && typeof p.settings === "object" ? (p.settings as Record<string, unknown>) : {};
  86. if (!hasOptIn(settings)) continue;
  87. const baseURL = typeof settings.baseURL === "string" ? settings.baseURL : undefined;
  88. if (!baseURL) { console.warn(`[openai-discovery] Provider "${id}" has no settings.baseURL — skipping`); continue; }
  89. const apiKey = typeof settings.apiKey === "string" ? settings.apiKey : "";
  90. targets.push({ id, baseURL, apiKey });
  91. }
  92. if (targets.length === 0) {
  93. console.log("[openai-discovery] No opted-in openai-compatible providers found — nothing to do");
  94. return;
  95. }
  96. for (const target of targets) {
  97. try {
  98. const models = await fetchModels(target.baseURL, target.apiKey);
  99. for (const m of models) (m as { providerID: string }).providerID = target.id;
  100. await ctx.provider.transform((editor) => {
  101. const source = editor.get(target.id);
  102. if (source) {
  103. const existing = [...source.models.values()];
  104. const existingIDs = new Set(existing.map((model) => String(model.id)));
  105. const merged = [...existing, ...models.filter((m) => !existingIDs.has(String(m.id)))];
  106. editor.models.set(target.id, merged);
  107. } else {
  108. 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 });
  109. }
  110. });
  111. console.log(`[openai-discovery] Registered ${models.length} models in provider "${target.id}"`);
  112. } catch (error) {
  113. console.error(`[openai-discovery] Failed to discover models for "${target.id}": ${error instanceof Error ? error.message : String(error)}`);
  114. }
  115. }
  116. },
  117. });