@mcowger/opencode-plexus 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/index.js +121 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -116,10 +116,23 @@ Run inside OpenCode:
116
116
 
117
117
  Select **Plexus** and enter your base URL and API key. Models are loaded immediately and cached for fast startup on subsequent sessions.
118
118
 
119
+ For OpenCode, enter the Plexus API base URL including the trailing `/v1`, for example:
120
+
121
+ ```text
122
+ https://plexus.example.com/v1
123
+ ```
124
+
125
+ The OpenCode plugin respects each model's `preferred_api` value and routes models through the matching SDK/API shape:
126
+
127
+ - `chat_completions` / `openai-completions` → OpenAI-compatible chat completions
128
+ - `responses` / `openai-responses` → OpenAI Responses API
129
+ - `messages` / `anthropic-messages` → Anthropic Messages API
130
+ - `gemini` / `google-generative-ai` → Google Gemini API
131
+
119
132
  You can also pre-configure via environment variables:
120
133
 
121
134
  ```sh
122
- export PLEXUS_BASE_URL=https://plexus.example.com
135
+ export PLEXUS_BASE_URL=https://plexus.example.com/v1
123
136
  export PLEXUS_API_KEY=your-api-key
124
137
  ```
125
138
 
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var PLEXUS_PROVIDER_NAME = "Plexus";
5
5
  var PLEXUS_PLUGIN_ID = "@mcowger/opencode-plexus";
6
6
  var PLEXUS_LOG_SERVICE = "opencode-plexus";
7
7
  var OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
8
+ var PLEXUS_BASE_URL_OPTION = "plexusBaseURL";
8
9
  var ENV_BASE_URL = "PLEXUS_BASE_URL";
9
10
  var ENV_API_KEY = "PLEXUS_API_KEY";
10
11
  var MODELS_FETCH_TIMEOUT_MS = 1e4;
@@ -13,6 +14,36 @@ var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
13
14
 
14
15
  // ../plexus-models/src/convert.ts
15
16
  var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
17
+ var API_DIALECT_MAP = {
18
+ chat_completions: "openai-completions",
19
+ "openai-completions": "openai-completions",
20
+ messages: "anthropic-messages",
21
+ "anthropic-messages": "anthropic-messages",
22
+ gemini: "google-generative-ai",
23
+ "google-generative-ai": "google-generative-ai",
24
+ responses: "openai-responses",
25
+ "openai-responses": "openai-responses"
26
+ };
27
+ function mapPreferredApi(raw) {
28
+ if (raw === undefined)
29
+ return "openai-completions";
30
+ const candidates = Array.isArray(raw) ? raw : [raw];
31
+ for (const candidate of candidates) {
32
+ const mapped = API_DIALECT_MAP[candidate];
33
+ if (mapped !== undefined)
34
+ return mapped;
35
+ }
36
+ return "openai-completions";
37
+ }
38
+ function adjustBaseUrl(baseUrl, preferredApi) {
39
+ const stripped = baseUrl.replace(/\/+$/, "");
40
+ switch (preferredApi) {
41
+ case "google-generative-ai":
42
+ return stripped.endsWith("/v1") ? `${stripped.slice(0, -3)}/v1beta` : stripped;
43
+ default:
44
+ return stripped;
45
+ }
46
+ }
16
47
  async function fetchPlexusModels(apiKey, modelsUrl) {
17
48
  const res = await fetch(modelsUrl, {
18
49
  headers: {
@@ -138,9 +169,10 @@ function createV2Client(serverUrl, input) {
138
169
  function resolveConfig(provider) {
139
170
  const envBaseURL = process.env[ENV_BASE_URL];
140
171
  const envApiKey = process.env[ENV_API_KEY];
141
- const optBaseURL = typeof provider?.options?.baseURL === "string" ? trimURL(provider.options.baseURL) : undefined;
172
+ const optBaseURL = typeof provider?.options?.[PLEXUS_BASE_URL_OPTION] === "string" ? trimURL(provider.options[PLEXUS_BASE_URL_OPTION]) : undefined;
173
+ const legacyBaseURL = typeof provider?.options?.baseURL === "string" ? trimURL(provider.options.baseURL) : undefined;
142
174
  const optApiKey = typeof provider?.options?.apiKey === "string" ? provider.options.apiKey.trim() : undefined;
143
- const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || undefined;
175
+ const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || legacyBaseURL || undefined;
144
176
  const apiKey = (envApiKey ? envApiKey.trim() : undefined) || optApiKey || undefined;
145
177
  return { baseURL: baseURL || undefined, apiKey: apiKey || undefined };
146
178
  }
@@ -150,7 +182,7 @@ async function persistToGlobalConfig(serverUrl, client, baseURL, apiKey) {
150
182
  config: {
151
183
  provider: {
152
184
  [PLEXUS_PROVIDER_ID]: {
153
- options: { baseURL, apiKey }
185
+ options: { [PLEXUS_BASE_URL_OPTION]: baseURL, apiKey }
154
186
  }
155
187
  }
156
188
  }
@@ -172,6 +204,22 @@ function createLogger(client) {
172
204
  // src/mapper.ts
173
205
  var REASONING_PARAMS2 = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
174
206
  var DEFAULT_CONTEXT = 8192;
207
+ function resolveModelProvider(model, baseURL) {
208
+ const preferredApi = mapPreferredApi(model.preferred_api);
209
+ const api = adjustBaseUrl(baseURL, preferredApi);
210
+ switch (preferredApi) {
211
+ case "anthropic-messages":
212
+ return { npm: "@ai-sdk/anthropic", api };
213
+ case "google-generative-ai":
214
+ return { npm: "@ai-sdk/google", api };
215
+ case "openai-responses":
216
+ return { npm: "@ai-sdk/openai", api };
217
+ case "openai-completions":
218
+ return { api };
219
+ default:
220
+ return { api };
221
+ }
222
+ }
175
223
  function parsePrice(value) {
176
224
  if (!value)
177
225
  return 0;
@@ -213,7 +261,7 @@ function buildOutputModalities(model) {
213
261
  return null;
214
262
  return ["text"];
215
263
  }
216
- function buildModels(models) {
264
+ function buildModels(models, baseURL) {
217
265
  const result = {};
218
266
  for (const m of models) {
219
267
  if (!m.id)
@@ -231,9 +279,11 @@ function buildModels(models) {
231
279
  const cacheWritePrice = parsePrice(m.pricing?.input_cache_write);
232
280
  const hasCachePricing = cacheReadPrice > 0 || cacheWritePrice > 0;
233
281
  const hasNonTextInput = inputModalities.some((mod) => mod !== "text");
282
+ const provider = resolveModelProvider(m, baseURL);
234
283
  const entry = {
235
284
  id: m.id,
236
285
  name: m.name ?? m.id,
286
+ provider,
237
287
  limit: {
238
288
  context: contextLength,
239
289
  output: maxOutput
@@ -261,13 +311,54 @@ function buildModels(models) {
261
311
 
262
312
  // src/plugin.ts
263
313
  var lastRefresh = null;
264
- async function refreshModels(client, baseURL, apiKey) {
314
+ function mergeModelMaps(base, overrides) {
315
+ if (!overrides)
316
+ return base;
317
+ const merged = { ...base };
318
+ for (const [id, override] of Object.entries(overrides)) {
319
+ const existing = merged[id];
320
+ if (!existing) {
321
+ merged[id] = override;
322
+ continue;
323
+ }
324
+ merged[id] = {
325
+ ...existing,
326
+ ...override,
327
+ provider: {
328
+ ...existing.provider ?? {},
329
+ ...override.provider ?? {}
330
+ },
331
+ ...existing.cost || override.cost ? {
332
+ cost: {
333
+ ...existing.cost ?? { input: 0, output: 0 },
334
+ ...override.cost ?? {}
335
+ }
336
+ } : {},
337
+ limit: {
338
+ ...existing.limit,
339
+ ...override.limit
340
+ },
341
+ modalities: {
342
+ input: override.modalities?.input ?? existing.modalities.input,
343
+ output: override.modalities?.output ?? existing.modalities.output
344
+ }
345
+ };
346
+ }
347
+ return merged;
348
+ }
349
+ async function refreshModels(client, baseURL, log, apiKey) {
265
350
  if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
351
+ log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
266
352
  return lastRefresh.models;
267
353
  }
268
354
  const url = modelsUrl(baseURL);
269
355
  const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
270
- const built = buildModels(apiModels);
356
+ const built = buildModels(apiModels, apiBase(baseURL));
357
+ for (const [id, model] of Object.entries(built)) {
358
+ const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
359
+ const providerApi = model.provider?.api ?? "(missing)";
360
+ log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
361
+ }
271
362
  lastRefresh = { at: Date.now(), models: built };
272
363
  writeCache(client, built, raw).catch(() => {});
273
364
  return built;
@@ -282,14 +373,21 @@ var PlexusProviderPlugin = async (ctx) => {
282
373
  const existingOptions = typeof existing["options"] === "object" && existing["options"] !== null ? existing["options"] : {};
283
374
  const existingModels = typeof existing["models"] === "object" && existing["models"] !== null ? existing["models"] : null;
284
375
  const { baseURL, apiKey } = resolveConfig(existing);
376
+ log.info(`Resolved plexus config: baseURL=${baseURL ?? "(missing)"} apiKey=${apiKey ? "present" : "missing"}`);
377
+ if (typeof existingOptions["baseURL"] === "string") {
378
+ log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
379
+ }
285
380
  const cachedSync = readCachedModelsSync();
381
+ if (cachedSync) {
382
+ log.info(`Loaded sync plexus cache with ${Object.keys(cachedSync).length} models`);
383
+ }
286
384
  const merged = {
287
385
  ...existing,
288
386
  name: existing["name"] ?? PLEXUS_PROVIDER_NAME,
289
387
  npm: existing["npm"] ?? OPENAI_COMPATIBLE_NPM,
290
388
  options: {
291
389
  ...existingOptions,
292
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
390
+ ...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
293
391
  ...apiKey ? { apiKey } : {}
294
392
  },
295
393
  models: existingModels ?? cachedSync ?? {
@@ -301,21 +399,32 @@ var PlexusProviderPlugin = async (ctx) => {
301
399
  }
302
400
  }
303
401
  };
402
+ const mergedOptions = merged["options"];
403
+ delete mergedOptions["baseURL"];
304
404
  if (baseURL) {
305
405
  try {
306
- const built = await refreshModels(client, baseURL, apiKey);
307
- merged["models"] = { ...built, ...existingModels ?? {} };
406
+ const built = await refreshModels(client, baseURL, log, apiKey);
407
+ merged["models"] = mergeModelMaps(built, existingModels);
308
408
  log.info(`Loaded ${Object.keys(built).length} plexus models from ${baseURL}`);
309
409
  } catch (e) {
310
410
  log.warn(`Live model refresh failed, using cache: ${String(e)}`);
311
411
  const cached = await readCachedModels(client);
312
412
  if (cached) {
313
- merged["models"] = { ...cached, ...existingModels ?? {} };
413
+ merged["models"] = mergeModelMaps(cached, existingModels);
314
414
  }
315
415
  }
316
416
  } else {
317
417
  log.info("Plexus baseURL not configured; skipping live refresh");
318
418
  }
419
+ try {
420
+ const mergedModels = merged["models"];
421
+ for (const id of ["gemini-3.5-flash", "claude-haiku-4-5", "small-fast"]) {
422
+ const m = mergedModels?.[id];
423
+ if (!m)
424
+ continue;
425
+ log.info(`Merged model ${id}: provider.npm=${m.provider?.npm ?? "(unset)"} provider.api=${m.provider?.api ?? "(unset)"}`);
426
+ }
427
+ } catch {}
319
428
  cfg.provider[PLEXUS_PROVIDER_ID] = merged;
320
429
  },
321
430
  auth: {
@@ -324,8 +433,8 @@ var PlexusProviderPlugin = async (ctx) => {
324
433
  const auth = await getAuth();
325
434
  const { baseURL, apiKey } = resolveConfig(providerInfo);
326
435
  const key = (auth?.type === "api" ? auth.key : undefined) ?? apiKey;
436
+ log.info(`Auth loader resolved plexus config: baseURL=${baseURL ?? "(missing)"} apiKey=${key ? "present" : "missing"}`);
327
437
  return {
328
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
329
438
  ...key ? { apiKey: key } : {}
330
439
  };
331
440
  },
@@ -383,6 +492,7 @@ export {
383
492
  PLEXUS_PROVIDER_ID,
384
493
  PLEXUS_PLUGIN_ID,
385
494
  PLEXUS_LOG_SERVICE,
495
+ PLEXUS_BASE_URL_OPTION,
386
496
  PLACEHOLDER_MODEL_ID,
387
497
  OPENAI_COMPATIBLE_NPM,
388
498
  MODELS_FETCH_TIMEOUT_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",