@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.
- package/README.md +14 -1
- package/dist/index.js +123 -11
- 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?.
|
|
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
|
-
|
|
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 ? {
|
|
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"] =
|
|
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"] =
|
|
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,
|