Przeglądaj źródła

feat: read endpoint/key from config with fallback to defaults

Paul Klumpp 1 tydzień temu
rodzic
commit
9c7bc1310f

+ 68 - 144
.opencode/plugins/opencode-openai-compatible-model-discovery/index.ts

@@ -1,167 +1,91 @@
 /**
  * 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).
+ * Reads endpoint/key from opencode.jsonc (project or global) when available,
+ * falling back to the built-in defaults. Creates the "OmniRoute" provider itself.
  */
 
 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 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 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;
+function readConfig(): { baseURL: string; apiKey: string } {
+  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"),
+  ];
+  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") continue;
+      for (const info of Object.values(provs) as any[]) {
+        const p = info as Record<string, unknown>;
+        if (typeof p.package === "string" && p.package.includes("openai-compatible")) {
+          const settings = (p.settings && typeof p.settings === "object") ? (p.settings as Record<string, unknown>) : {};
+          const baseURL = typeof settings.baseURL === "string" ? settings.baseURL : undefined;
+          const apiKey = typeof settings.apiKey === "string" ? settings.apiKey : "";
+          if (baseURL) return { baseURL, apiKey };
+        }
+      }
+    } catch {}
   }
-  return false;
+  return { baseURL: "http://localhost:20128/v1", apiKey: "sk-e9ac58c0b78cd2bc-f17d3e-f6beea67" };
 }
 
-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;
-  });
-}
+const { baseURL: BASE_URL, apiKey: API_KEY } = readConfig();
+const PROVIDER_ID = "OmniRoute";
+const PROVIDER_NAME = "OmniRoute";
 
 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 });
-        }
+      const headers: Record<string, string> = { "Content-Type": "application/json" };
+      if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
+      const res = await fetch(`${BASE_URL}/models`, { headers });
+      if (!res.ok) throw new Error(`HTTP ${res.status} from ${BASE_URL}/models`);
+      const json = await res.json();
+      const rawModels = (json.data || []) as Array<Record<string, unknown>>;
+      const models = 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: PROVIDER_ID, 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;
       });
-    } 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;
-          }
+      ctx.provider.transform((editor) => {
+        const source = editor.get(PROVIDER_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);
-        });
-
-        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)}`
-        );
-      }
+          const merged = [...existing, ...models.filter((m) => !existingIDs.has(String(m.id)))];
+          editor.models.set(PROVIDER_ID, merged);
+        } else {
+          editor.add({ info: { id: PROVIDER_ID, name: PROVIDER_NAME, activation: "enabled" as const, package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: BASE_URL, apiKey: API_KEY } } as never, models });
+        }
+      });
+      console.log(`[openai-discovery] Registered ${models.length} models in provider "${PROVIDER_ID}"`);
+    } catch (error) {
+      console.error(`[openai-discovery] Failed to discover models: ${error instanceof Error ? error.message : String(error)}`);
     }
   },
 });