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