@mcowger/opencode-plexus 0.7.0 → 0.8.2

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 +123 -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,38 @@ 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 "anthropic-messages":
42
+ return stripped.endsWith("/v1") ? stripped.slice(0, -3) : stripped;
43
+ case "google-generative-ai":
44
+ return stripped.endsWith("/v1") ? `${stripped.slice(0, -3)}/v1beta` : stripped;
45
+ default:
46
+ return stripped;
47
+ }
48
+ }
16
49
  async function fetchPlexusModels(apiKey, modelsUrl) {
17
50
  const res = await fetch(modelsUrl, {
18
51
  headers: {
@@ -138,9 +171,10 @@ function createV2Client(serverUrl, input) {
138
171
  function resolveConfig(provider) {
139
172
  const envBaseURL = process.env[ENV_BASE_URL];
140
173
  const envApiKey = process.env[ENV_API_KEY];
141
- const optBaseURL = typeof provider?.options?.baseURL === "string" ? trimURL(provider.options.baseURL) : undefined;
174
+ const optBaseURL = typeof provider?.options?.[PLEXUS_BASE_URL_OPTION] === "string" ? trimURL(provider.options[PLEXUS_BASE_URL_OPTION]) : undefined;
175
+ const legacyBaseURL = typeof provider?.options?.baseURL === "string" ? trimURL(provider.options.baseURL) : undefined;
142
176
  const optApiKey = typeof provider?.options?.apiKey === "string" ? provider.options.apiKey.trim() : undefined;
143
- const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || undefined;
177
+ const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || legacyBaseURL || undefined;
144
178
  const apiKey = (envApiKey ? envApiKey.trim() : undefined) || optApiKey || undefined;
145
179
  return { baseURL: baseURL || undefined, apiKey: apiKey || undefined };
146
180
  }
@@ -150,7 +184,7 @@ async function persistToGlobalConfig(serverUrl, client, baseURL, apiKey) {
150
184
  config: {
151
185
  provider: {
152
186
  [PLEXUS_PROVIDER_ID]: {
153
- options: { baseURL, apiKey }
187
+ options: { [PLEXUS_BASE_URL_OPTION]: baseURL, apiKey }
154
188
  }
155
189
  }
156
190
  }
@@ -172,6 +206,22 @@ function createLogger(client) {
172
206
  // src/mapper.ts
173
207
  var REASONING_PARAMS2 = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
174
208
  var DEFAULT_CONTEXT = 8192;
209
+ function resolveModelProvider(model, baseURL) {
210
+ const preferredApi = mapPreferredApi(model.preferred_api);
211
+ const api = adjustBaseUrl(baseURL, preferredApi);
212
+ switch (preferredApi) {
213
+ case "anthropic-messages":
214
+ return { npm: "@ai-sdk/anthropic", api };
215
+ case "google-generative-ai":
216
+ return { npm: "@ai-sdk/google", api };
217
+ case "openai-responses":
218
+ return { npm: "@ai-sdk/openai", api };
219
+ case "openai-completions":
220
+ return { api };
221
+ default:
222
+ return { api };
223
+ }
224
+ }
175
225
  function parsePrice(value) {
176
226
  if (!value)
177
227
  return 0;
@@ -213,7 +263,7 @@ function buildOutputModalities(model) {
213
263
  return null;
214
264
  return ["text"];
215
265
  }
216
- function buildModels(models) {
266
+ function buildModels(models, baseURL) {
217
267
  const result = {};
218
268
  for (const m of models) {
219
269
  if (!m.id)
@@ -231,9 +281,11 @@ function buildModels(models) {
231
281
  const cacheWritePrice = parsePrice(m.pricing?.input_cache_write);
232
282
  const hasCachePricing = cacheReadPrice > 0 || cacheWritePrice > 0;
233
283
  const hasNonTextInput = inputModalities.some((mod) => mod !== "text");
284
+ const provider = resolveModelProvider(m, baseURL);
234
285
  const entry = {
235
286
  id: m.id,
236
287
  name: m.name ?? m.id,
288
+ provider,
237
289
  limit: {
238
290
  context: contextLength,
239
291
  output: maxOutput
@@ -261,13 +313,54 @@ function buildModels(models) {
261
313
 
262
314
  // src/plugin.ts
263
315
  var lastRefresh = null;
264
- async function refreshModels(client, baseURL, apiKey) {
316
+ function mergeModelMaps(base, overrides) {
317
+ if (!overrides)
318
+ return base;
319
+ const merged = { ...base };
320
+ for (const [id, override] of Object.entries(overrides)) {
321
+ const existing = merged[id];
322
+ if (!existing) {
323
+ merged[id] = override;
324
+ continue;
325
+ }
326
+ merged[id] = {
327
+ ...existing,
328
+ ...override,
329
+ provider: {
330
+ ...existing.provider ?? {},
331
+ ...override.provider ?? {}
332
+ },
333
+ ...existing.cost || override.cost ? {
334
+ cost: {
335
+ ...existing.cost ?? { input: 0, output: 0 },
336
+ ...override.cost ?? {}
337
+ }
338
+ } : {},
339
+ limit: {
340
+ ...existing.limit,
341
+ ...override.limit
342
+ },
343
+ modalities: {
344
+ input: override.modalities?.input ?? existing.modalities.input,
345
+ output: override.modalities?.output ?? existing.modalities.output
346
+ }
347
+ };
348
+ }
349
+ return merged;
350
+ }
351
+ async function refreshModels(client, baseURL, log, apiKey) {
265
352
  if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
353
+ log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
266
354
  return lastRefresh.models;
267
355
  }
268
356
  const url = modelsUrl(baseURL);
269
357
  const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
270
- const built = buildModels(apiModels);
358
+ const built = buildModels(apiModels, apiBase(baseURL));
359
+ for (const [id, model] of Object.entries(built)) {
360
+ const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
361
+ const providerApi = model.provider?.api ?? "(missing)";
362
+ log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
363
+ }
271
364
  lastRefresh = { at: Date.now(), models: built };
272
365
  writeCache(client, built, raw).catch(() => {});
273
366
  return built;
@@ -282,14 +375,21 @@ var PlexusProviderPlugin = async (ctx) => {
282
375
  const existingOptions = typeof existing["options"] === "object" && existing["options"] !== null ? existing["options"] : {};
283
376
  const existingModels = typeof existing["models"] === "object" && existing["models"] !== null ? existing["models"] : null;
284
377
  const { baseURL, apiKey } = resolveConfig(existing);
378
+ log.info(`Resolved plexus config: baseURL=${baseURL ?? "(missing)"} apiKey=${apiKey ? "present" : "missing"}`);
379
+ if (typeof existingOptions["baseURL"] === "string") {
380
+ log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
381
+ }
285
382
  const cachedSync = readCachedModelsSync();
383
+ if (cachedSync) {
384
+ log.info(`Loaded sync plexus cache with ${Object.keys(cachedSync).length} models`);
385
+ }
286
386
  const merged = {
287
387
  ...existing,
288
388
  name: existing["name"] ?? PLEXUS_PROVIDER_NAME,
289
389
  npm: existing["npm"] ?? OPENAI_COMPATIBLE_NPM,
290
390
  options: {
291
391
  ...existingOptions,
292
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
392
+ ...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
293
393
  ...apiKey ? { apiKey } : {}
294
394
  },
295
395
  models: existingModels ?? cachedSync ?? {
@@ -301,21 +401,32 @@ var PlexusProviderPlugin = async (ctx) => {
301
401
  }
302
402
  }
303
403
  };
404
+ const mergedOptions = merged["options"];
405
+ delete mergedOptions["baseURL"];
304
406
  if (baseURL) {
305
407
  try {
306
- const built = await refreshModels(client, baseURL, apiKey);
307
- merged["models"] = { ...built, ...existingModels ?? {} };
408
+ const built = await refreshModels(client, baseURL, log, apiKey);
409
+ merged["models"] = mergeModelMaps(built, existingModels);
308
410
  log.info(`Loaded ${Object.keys(built).length} plexus models from ${baseURL}`);
309
411
  } catch (e) {
310
412
  log.warn(`Live model refresh failed, using cache: ${String(e)}`);
311
413
  const cached = await readCachedModels(client);
312
414
  if (cached) {
313
- merged["models"] = { ...cached, ...existingModels ?? {} };
415
+ merged["models"] = mergeModelMaps(cached, existingModels);
314
416
  }
315
417
  }
316
418
  } else {
317
419
  log.info("Plexus baseURL not configured; skipping live refresh");
318
420
  }
421
+ try {
422
+ const mergedModels = merged["models"];
423
+ for (const id of ["gemini-3.5-flash", "claude-haiku-4-5", "small-fast"]) {
424
+ const m = mergedModels?.[id];
425
+ if (!m)
426
+ continue;
427
+ log.info(`Merged model ${id}: provider.npm=${m.provider?.npm ?? "(unset)"} provider.api=${m.provider?.api ?? "(unset)"}`);
428
+ }
429
+ } catch {}
319
430
  cfg.provider[PLEXUS_PROVIDER_ID] = merged;
320
431
  },
321
432
  auth: {
@@ -324,8 +435,8 @@ var PlexusProviderPlugin = async (ctx) => {
324
435
  const auth = await getAuth();
325
436
  const { baseURL, apiKey } = resolveConfig(providerInfo);
326
437
  const key = (auth?.type === "api" ? auth.key : undefined) ?? apiKey;
438
+ log.info(`Auth loader resolved plexus config: baseURL=${baseURL ?? "(missing)"} apiKey=${key ? "present" : "missing"}`);
327
439
  return {
328
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
329
440
  ...key ? { apiKey: key } : {}
330
441
  };
331
442
  },
@@ -383,6 +494,7 @@ export {
383
494
  PLEXUS_PROVIDER_ID,
384
495
  PLEXUS_PLUGIN_ID,
385
496
  PLEXUS_LOG_SERVICE,
497
+ PLEXUS_BASE_URL_OPTION,
386
498
  PLACEHOLDER_MODEL_ID,
387
499
  OPENAI_COMPATIBLE_NPM,
388
500
  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.2",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",