@mcowger/opencode-plexus 0.1.2 → 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 (4) hide show
  1. package/README.md +162 -95
  2. package/dist/index.js +145 -85
  3. package/package.json +45 -38
  4. package/LICENSE +0 -21
package/README.md CHANGED
@@ -1,142 +1,209 @@
1
- # @mcowger/opencode-plexus
1
+ # plexus-agent-plugins
2
2
 
3
- An [OpenCode](https://opencode.ai) plugin that exposes a self-hosted [Plexus](https://github.com/mcowger/plexus) instance as a first-class `plexus` provider with **dynamic model discovery**.
3
+ Exposes models from a self-hosted [Plexus](https://github.com/mcowger/plexus) AI proxy as a first-class provider inside AI coding agents. Models appear in the agent's model picker with correct wire-protocol behavior, as if they were natively supported providers.
4
4
 
5
- Models are fetched live from your Plexus instance's `/v1/models` endpoint on every startup, and cached on-disk so OpenCode starts cleanly even when the network is unavailable.
5
+ ## Supported agents
6
6
 
7
- ---
7
+ | Package | Agent | npm |
8
+ |---|---|---|
9
+ | `plexus-pi` | [pi](https://github.com/earendil-works/pi) | `@mcowger/pi-plexus` |
10
+ | `plexus-opencode` | [OpenCode](https://opencode.ai) | `@mcowger/opencode-plexus` |
11
+
12
+ ## Prerequisites
13
+
14
+ - A running Plexus instance
8
15
 
9
16
  ## Installation
10
17
 
11
- ```bash
12
- opencode plugin --global @mcowger/opencode-plexus
13
- ```
18
+ The built dist artifact is committed to the repo, so no build step is needed for any install method.
14
19
 
15
20
  ---
16
21
 
17
- ## Setup
22
+ ### pi
18
23
 
19
- ### Option AInteractive auth (recommended)
24
+ #### Option 1npm (recommended)
20
25
 
21
- ```bash
22
- opencode auth login --provider plexus
26
+ ```sh
27
+ cd ~/.pi/agent/extensions
28
+ npm install @mcowger/pi-plexus
23
29
  ```
24
30
 
25
- You will be prompted for:
26
- - **Plexus base URL** — e.g. `https://plexus.example.com`
27
- - **API key** — the key used to authenticate chat-completion requests
31
+ #### Option 2 git clone into the extensions directory
28
32
 
29
- OpenCode will probe the URL, then persist both values in its global config so you only need to do this once.
33
+ ```sh
34
+ git clone https://github.com/mcowger/plexus-agent-plugins ~/.pi/agent/extensions/plexus-agent-plugins
35
+ ```
30
36
 
31
- ### Option BEnvironment variables
37
+ #### Option 3git clone anywhere + settings.json
32
38
 
33
- ```bash
34
- export PLEXUS_BASE_URL=https://plexus.example.com
35
- export PLEXUS_API_KEY=sk-...
39
+ ```sh
40
+ git clone https://github.com/mcowger/plexus-agent-plugins ~/code/plexus-agent-plugins
36
41
  ```
37
42
 
38
- Environment variables take precedence over the stored config.
39
-
40
- ### Option C — Manual `opencode.json`
43
+ Then register the path in `~/.pi/agent/settings.json`:
41
44
 
42
- ```jsonc
45
+ ```json
43
46
  {
44
- "provider": {
45
- "plexus": {
46
- "options": {
47
- "baseURL": "https://plexus.example.com",
48
- "apiKey": "sk-..."
49
- }
50
- }
51
- }
47
+ "extensions": [
48
+ "~/code/plexus-agent-plugins/packages/plexus-pi"
49
+ ]
52
50
  }
53
51
  ```
54
52
 
55
53
  ---
56
54
 
57
- ## How it works
55
+ ### OpenCode
58
56
 
59
- 1. On startup the plugin's `config` hook fires.
60
- 2. It reads `PLEXUS_BASE_URL` / `PLEXUS_API_KEY` (or the stored options) and calls `/v1/models` on your Plexus instance.
61
- 3. The response is transformed into OpenCode's model schema and registered under the `plexus` provider.
62
- 4. The transformed list is cached in OpenCode's state directory (`~/.local/share/opencode/plugins/plexus/`).
63
- 5. On the next startup the cache is loaded synchronously before the live refresh completes, so the model picker is always populated.
57
+ #### Option 1 npm (recommended)
64
58
 
65
- > **Note:** `/v1/models` does not require an API key. The API key is only required for chat-completion requests. You can run the plugin without an API key if you only want to browse models.
59
+ ```sh
60
+ npm install -g @mcowger/opencode-plexus
61
+ ```
66
62
 
67
- ---
63
+ Then add the plugin to your `opencode.json`:
68
64
 
69
- ## Model discovery details
70
-
71
- | Plexus field | OpenCode model field |
72
- |---|---|
73
- | `id` | `id` (also dict key) |
74
- | `name` | `name` |
75
- | `context_length` / `top_provider.context_length` | `limit.context` |
76
- | `top_provider.max_completion_tokens` | `limit.output` (fallback: `ceil(context × 0.2)`) |
77
- | `pricing.prompt` / `.completion` | `cost.input` / `.output` (per-million tokens) |
78
- | `pricing.input_cache_read` / `.input_cache_write` | `cost.cache_read` / `.cache_write` |
79
- | `architecture.input_modalities` | `modalities.input` (`file` → `pdf`) |
80
- | `architecture.output_modalities` | `modalities.output` |
81
- | `output_modalities` present but does not include `text` | **model skipped** (filters image-generation, embedding, TTS, and other non-chat output types) |
82
- | no `architecture` field and id matches `embedding`, `tts`, `whisper`, `image-*`, `dream`, etc. | **model skipped** (bare-stub non-chat models with no metadata) |
83
- | `supported_parameters` includes `tools` | `tool_call: true` |
84
- | `supported_parameters` includes `reasoning` / `include_reasoning` / `reasoning_effort` | `reasoning: true` |
85
- | `supported_parameters` includes `temperature` | `temperature: true` |
86
- | any non-text input modality | `attachment: true` |
65
+ ```json
66
+ {
67
+ "plugins": ["@mcowger/opencode-plexus"]
68
+ }
69
+ ```
87
70
 
88
- ---
71
+ #### Option 2 — path reference
89
72
 
90
- ## Multi-API escape hatch
73
+ ```sh
74
+ git clone https://github.com/mcowger/plexus-agent-plugins ~/code/plexus-agent-plugins
75
+ ```
91
76
 
92
- By default this plugin uses `@ai-sdk/openai-compatible` (the OpenAI-compatible route) for all models. For Plexus-proxied models that use a different API wire format (e.g. Anthropic messages), add a **sibling provider** manually in your `opencode.json`:
77
+ Then reference the built artifact in `opencode.json`:
93
78
 
94
- ```jsonc
79
+ ```json
95
80
  {
96
- "provider": {
97
- "plexus": {
98
- // managed by this plugin — do not edit models here
99
- },
100
- "plexus-anthropic": {
101
- "npm": "@ai-sdk/anthropic",
102
- "options": {
103
- "baseURL": "https://plexus.example.com"
104
- },
105
- "models": {
106
- "claude-sonnet-4-6": {
107
- "name": "Claude Sonnet 4.6 (via Plexus, Anthropic wire)",
108
- "attachment": true,
109
- "reasoning": true,
110
- "tool_call": true,
111
- "cost": { "input": 3.0, "output": 15.0 },
112
- "limit": { "context": 1000000, "output": 128000 }
113
- }
114
- }
115
- }
116
- }
81
+ "plugins": ["~/code/plexus-agent-plugins/packages/plexus-opencode/dist/index.js"]
117
82
  }
118
83
  ```
119
84
 
120
- The `plexus` provider itself only manages the OpenAI-compatible route.
85
+ ---
86
+
87
+ ## First-time setup
88
+
89
+ ### pi
90
+
91
+ Run inside pi:
92
+
93
+ ```
94
+ /plexus login
95
+ ```
96
+
97
+ You will be prompted for:
98
+
99
+ - **Plexus base URL** — e.g. `https://plexus.example.com`
100
+ - **Plexus API key**
101
+ - **Default model** (optional)
102
+
103
+ To force a model refresh:
104
+
105
+ ```
106
+ /plexus refresh
107
+ ```
108
+
109
+ ### OpenCode
110
+
111
+ Run inside OpenCode:
112
+
113
+ ```
114
+ /connect
115
+ ```
116
+
117
+ Select **Plexus** and enter your base URL and API key. Models are loaded immediately and cached for fast startup on subsequent sessions.
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
+
132
+ You can also pre-configure via environment variables:
133
+
134
+ ```sh
135
+ export PLEXUS_BASE_URL=https://plexus.example.com/v1
136
+ export PLEXUS_API_KEY=your-api-key
137
+ ```
121
138
 
122
139
  ---
123
140
 
124
- ## Troubleshooting
141
+ ## Configuration files
142
+
143
+ ### pi
125
144
 
126
- **Models don't appear after setup**
127
- - Check that `PLEXUS_BASE_URL` is set or that you completed `opencode auth login --provider plexus`.
128
- - Verify reachability: `curl https://plexus.example.com/v1/models`.
145
+ ```
146
+ ~/.pi/agent/extensions/plexus/
147
+ config.json # base URL and optional default model
148
+ plexus-models-cache.json # last-fetched model list (startup cache)
149
+ plexus-models-response.json # raw API response (diagnostics)
150
+ plexus.log # extension activity log
151
+ ```
129
152
 
130
- **Cache location**
131
- The model cache lives under OpenCode's own state directory:
153
+ The API key is stored in pi's own credential store (`auth.json`) — never in a separate file.
132
154
 
155
+ ### OpenCode
156
+
157
+ ```
158
+ ~/.local/share/opencode/plugins/plexus/
159
+ models-cache.json # last-fetched model list (startup cache)
160
+ models-raw.json # raw API response (diagnostics)
133
161
  ```
134
- ~/.local/share/opencode/plugins/plexus/models-cache.json
135
- ~/.local/share/opencode/plugins/plexus/models-raw.json
162
+
163
+ The API key is stored in OpenCode's own credential store — never in a separate file.
164
+
165
+ ---
166
+
167
+ ## Package layout
168
+
169
+ ```
170
+ packages/
171
+ plexus-models/ # host-agnostic data layer
172
+ src/
173
+ types.ts # wire types (PlexusApiModel, PlexusModelDescriptor, etc.)
174
+ convert.ts # model fetching, conversion, compat detection
175
+ index.ts # barrel export
176
+ plexus-pi/ # pi host adapter
177
+ src/
178
+ extension.ts # entry point: commands, session refresh, auth flow
179
+ mapper.ts # PlexusModelDescriptor → pi ProviderModelConfig
180
+ config.ts # base URL / default model config I/O
181
+ cache.ts # model cache I/O
182
+ log.ts # append-only log
183
+ package.json # declares pi.extensions entry point
184
+ plexus-opencode/ # OpenCode plugin adapter
185
+ src/
186
+ plugin.ts # Plugin export: config hook, auth handler
187
+ mapper.ts # PlexusApiModel → OpenCode ConfigModel
188
+ cache.ts # model cache I/O
189
+ config-store.ts # resolveConfig, persistToGlobalConfig
190
+ log.ts # logger via OpenCode SDK
191
+ constants.ts # provider ID, env var names, timeouts
192
+ url.ts # URL helpers (trimURL, apiBase, modelsUrl)
193
+ index.ts # barrel export
194
+ package.json # npm package manifest
195
+ ```
196
+
197
+ `plexus-models` has zero imports from any agent framework. Each host adapter imports it via a relative path.
198
+
199
+ ## Development
200
+
201
+ After cloning, install dependencies to set up the pre-commit hook:
202
+
203
+ ```sh
204
+ bun install
136
205
  ```
137
206
 
138
- Deleting OpenCode's state directory (the documented reset path) also clears this cache.
207
+ The pre-commit hook (via lefthook) rebuilds both dist artifacts automatically whenever source files change. After committing, reload/restart your agent.
139
208
 
140
- **API key vs base URL**
141
- - `PLEXUS_API_KEY` (and the stored key) is used **only** for chat-completion requests.
142
- - `/v1/models` is fetched without authentication, so the model picker works even if you haven't set an API key yet.
209
+ To add support for a new host agent, see [AGENTS.md](AGENTS.md).
package/dist/index.js CHANGED
@@ -5,13 +5,58 @@ 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;
11
12
  var REFRESH_TTL_MS = 60000;
12
- var DEFAULT_CONTEXT = 8192;
13
13
  var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
14
14
 
15
+ // ../plexus-models/src/convert.ts
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
+ }
47
+ async function fetchPlexusModels(apiKey, modelsUrl) {
48
+ const res = await fetch(modelsUrl, {
49
+ headers: {
50
+ Authorization: `Bearer ${apiKey}`,
51
+ Accept: "application/json"
52
+ }
53
+ });
54
+ if (!res.ok) {
55
+ throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
56
+ }
57
+ const raw = await res.json();
58
+ return { models: raw.data ?? [], raw };
59
+ }
15
60
  // src/cache.ts
16
61
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
17
62
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -124,9 +169,10 @@ function createV2Client(serverUrl, input) {
124
169
  function resolveConfig(provider) {
125
170
  const envBaseURL = process.env[ENV_BASE_URL];
126
171
  const envApiKey = process.env[ENV_API_KEY];
127
- 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;
128
174
  const optApiKey = typeof provider?.options?.apiKey === "string" ? provider.options.apiKey.trim() : undefined;
129
- const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || undefined;
175
+ const baseURL = (envBaseURL ? trimURL(envBaseURL) : undefined) || optBaseURL || legacyBaseURL || undefined;
130
176
  const apiKey = (envApiKey ? envApiKey.trim() : undefined) || optApiKey || undefined;
131
177
  return { baseURL: baseURL || undefined, apiKey: apiKey || undefined };
132
178
  }
@@ -136,7 +182,7 @@ async function persistToGlobalConfig(serverUrl, client, baseURL, apiKey) {
136
182
  config: {
137
183
  provider: {
138
184
  [PLEXUS_PROVIDER_ID]: {
139
- options: { baseURL, apiKey }
185
+ options: { [PLEXUS_BASE_URL_OPTION]: baseURL, apiKey }
140
186
  }
141
187
  }
142
188
  }
@@ -155,8 +201,25 @@ function createLogger(client) {
155
201
  };
156
202
  }
157
203
 
158
- // src/models.ts
159
- var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
204
+ // src/mapper.ts
205
+ var REASONING_PARAMS2 = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
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
+ }
160
223
  function parsePrice(value) {
161
224
  if (!value)
162
225
  return 0;
@@ -198,7 +261,7 @@ function buildOutputModalities(model) {
198
261
  return null;
199
262
  return ["text"];
200
263
  }
201
- function buildModels(models) {
264
+ function buildModels(models, baseURL) {
202
265
  const result = {};
203
266
  for (const m of models) {
204
267
  if (!m.id)
@@ -216,9 +279,11 @@ function buildModels(models) {
216
279
  const cacheWritePrice = parsePrice(m.pricing?.input_cache_write);
217
280
  const hasCachePricing = cacheReadPrice > 0 || cacheWritePrice > 0;
218
281
  const hasNonTextInput = inputModalities.some((mod) => mod !== "text");
282
+ const provider = resolveModelProvider(m, baseURL);
219
283
  const entry = {
220
284
  id: m.id,
221
285
  name: m.name ?? m.id,
286
+ provider,
222
287
  limit: {
223
288
  context: contextLength,
224
289
  output: maxOutput
@@ -235,7 +300,7 @@ function buildModels(models) {
235
300
  }
236
301
  } : {},
237
302
  ...params.includes("tools") ? { tool_call: true } : {},
238
- ...params.some((p) => REASONING_PARAMS.has(p)) ? { reasoning: true } : {},
303
+ ...params.some((p) => REASONING_PARAMS2.has(p)) ? { reasoning: true } : {},
239
304
  ...params.includes("temperature") ? { temperature: true } : {},
240
305
  ...hasNonTextInput ? { attachment: true } : {}
241
306
  };
@@ -244,80 +309,56 @@ function buildModels(models) {
244
309
  return result;
245
310
  }
246
311
 
247
- // src/plexus-client.ts
248
- import { z } from "zod";
249
- var PlexusModelArchitectureSchema = z.object({
250
- modality: z.string().optional(),
251
- input_modalities: z.array(z.string()).optional(),
252
- output_modalities: z.array(z.string()).optional(),
253
- tokenizer: z.string().optional(),
254
- instruct_type: z.string().nullable().optional()
255
- }).passthrough();
256
- var PlexusModelPricingSchema = z.object({
257
- prompt: z.string().optional(),
258
- completion: z.string().optional(),
259
- input_cache_read: z.string().optional(),
260
- input_cache_write: z.string().optional()
261
- }).passthrough();
262
- var PlexusTopProviderSchema = z.object({
263
- context_length: z.number().nullable().optional(),
264
- max_completion_tokens: z.number().nullable().optional(),
265
- is_moderated: z.boolean().optional()
266
- }).passthrough();
267
- var PlexusApiModelSchema = z.object({
268
- id: z.string(),
269
- object: z.string().optional(),
270
- created: z.number().optional(),
271
- owned_by: z.string().optional(),
272
- preferred_api: z.union([z.string(), z.array(z.string())]).optional(),
273
- name: z.string().optional(),
274
- description: z.string().optional(),
275
- context_length: z.number().nullable().optional(),
276
- architecture: PlexusModelArchitectureSchema.optional(),
277
- pricing: PlexusModelPricingSchema.optional(),
278
- supported_parameters: z.array(z.string()).optional(),
279
- top_provider: PlexusTopProviderSchema.optional(),
280
- pi_provider: z.string().optional(),
281
- pi_model: z.string().optional()
282
- }).passthrough();
283
- var PlexusApiResponseSchema = z.object({
284
- object: z.string(),
285
- data: z.array(PlexusApiModelSchema)
286
- });
287
- async function fetchPlexusModels(baseURL, apiKey) {
288
- const url = modelsUrl(baseURL);
289
- if (!url)
290
- throw new Error("Plexus: cannot build models URL from an empty baseURL");
291
- const headers = {
292
- Accept: "application/json"
293
- };
294
- if (apiKey) {
295
- headers["Authorization"] = `Bearer ${apiKey}`;
296
- }
297
- const response = await fetch(url, {
298
- headers,
299
- signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
300
- });
301
- if (!response.ok) {
302
- throw new Error(`Plexus models fetch failed: HTTP ${response.status} ${response.statusText} (${url})`);
303
- }
304
- const json = await response.json();
305
- const parsed = PlexusApiResponseSchema.safeParse(json);
306
- if (!parsed.success) {
307
- throw new Error(`Plexus models response did not match expected schema: ${parsed.error.message}`);
308
- }
309
- const raw = parsed.data;
310
- return { models: raw.data ?? [], raw };
311
- }
312
-
313
312
  // src/plugin.ts
314
313
  var lastRefresh = null;
315
- 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) {
316
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)`);
317
352
  return lastRefresh.models;
318
353
  }
319
- const { models: apiModels, raw } = await fetchPlexusModels(baseURL, apiKey);
320
- const built = buildModels(apiModels);
354
+ const url = modelsUrl(baseURL);
355
+ const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
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
+ }
321
362
  lastRefresh = { at: Date.now(), models: built };
322
363
  writeCache(client, built, raw).catch(() => {});
323
364
  return built;
@@ -332,14 +373,21 @@ var PlexusProviderPlugin = async (ctx) => {
332
373
  const existingOptions = typeof existing["options"] === "object" && existing["options"] !== null ? existing["options"] : {};
333
374
  const existingModels = typeof existing["models"] === "object" && existing["models"] !== null ? existing["models"] : null;
334
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
+ }
335
380
  const cachedSync = readCachedModelsSync();
381
+ if (cachedSync) {
382
+ log.info(`Loaded sync plexus cache with ${Object.keys(cachedSync).length} models`);
383
+ }
336
384
  const merged = {
337
385
  ...existing,
338
386
  name: existing["name"] ?? PLEXUS_PROVIDER_NAME,
339
387
  npm: existing["npm"] ?? OPENAI_COMPATIBLE_NPM,
340
388
  options: {
341
389
  ...existingOptions,
342
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
390
+ ...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
343
391
  ...apiKey ? { apiKey } : {}
344
392
  },
345
393
  models: existingModels ?? cachedSync ?? {
@@ -351,21 +399,32 @@ var PlexusProviderPlugin = async (ctx) => {
351
399
  }
352
400
  }
353
401
  };
402
+ const mergedOptions = merged["options"];
403
+ delete mergedOptions["baseURL"];
354
404
  if (baseURL) {
355
405
  try {
356
- const built = await refreshModels(client, baseURL, apiKey);
357
- merged["models"] = { ...built, ...existingModels ?? {} };
406
+ const built = await refreshModels(client, baseURL, log, apiKey);
407
+ merged["models"] = mergeModelMaps(built, existingModels);
358
408
  log.info(`Loaded ${Object.keys(built).length} plexus models from ${baseURL}`);
359
409
  } catch (e) {
360
410
  log.warn(`Live model refresh failed, using cache: ${String(e)}`);
361
411
  const cached = await readCachedModels(client);
362
412
  if (cached) {
363
- merged["models"] = { ...cached, ...existingModels ?? {} };
413
+ merged["models"] = mergeModelMaps(cached, existingModels);
364
414
  }
365
415
  }
366
416
  } else {
367
417
  log.info("Plexus baseURL not configured; skipping live refresh");
368
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 {}
369
428
  cfg.provider[PLEXUS_PROVIDER_ID] = merged;
370
429
  },
371
430
  auth: {
@@ -374,8 +433,8 @@ var PlexusProviderPlugin = async (ctx) => {
374
433
  const auth = await getAuth();
375
434
  const { baseURL, apiKey } = resolveConfig(providerInfo);
376
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"}`);
377
437
  return {
378
- ...baseURL ? { baseURL: apiBase(baseURL) } : {},
379
438
  ...key ? { apiKey: key } : {}
380
439
  };
381
440
  },
@@ -398,7 +457,8 @@ var PlexusProviderPlugin = async (ctx) => {
398
457
  if (!baseURL || !apiKey)
399
458
  return { type: "failed" };
400
459
  try {
401
- await fetchPlexusModels(baseURL);
460
+ const url = modelsUrl(baseURL);
461
+ await fetchPlexusModels("", url);
402
462
  } catch (e) {
403
463
  log.error(`Plexus URL probe failed at ${baseURL}: ${String(e)}`);
404
464
  return { type: "failed" };
@@ -432,10 +492,10 @@ export {
432
492
  PLEXUS_PROVIDER_ID,
433
493
  PLEXUS_PLUGIN_ID,
434
494
  PLEXUS_LOG_SERVICE,
495
+ PLEXUS_BASE_URL_OPTION,
435
496
  PLACEHOLDER_MODEL_ID,
436
497
  OPENAI_COMPATIBLE_NPM,
437
498
  MODELS_FETCH_TIMEOUT_MS,
438
499
  ENV_BASE_URL,
439
- ENV_API_KEY,
440
- DEFAULT_CONTEXT
500
+ ENV_API_KEY
441
501
  };
package/package.json CHANGED
@@ -1,40 +1,47 @@
1
1
  {
2
- "name": "@mcowger/opencode-plexus",
3
- "version": "0.1.2",
4
- "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
- "type": "module",
6
- "module": "./dist/index.js",
7
- "main": "./dist/index.js",
8
- "exports": {
9
- ".": "./dist/index.js",
10
- "./server": "./dist/index.js"
11
- },
12
- "files": ["dist", "README.md", "LICENSE"],
13
- "license": "MIT",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/mcowger/opencode-plexus.git"
17
- },
18
- "publishConfig": {
19
- "access": "public"
20
- },
21
- "scripts": {
22
- "build": "bun build src/index.ts --outdir=dist --target=bun --format=esm --packages=external",
23
- "watch": "bun build src/index.ts --outdir=dist --target=bun --format=esm --packages=external --watch",
24
- "typecheck": "tsc --noEmit",
25
- "test": "bun test ./tests",
26
- "prepublishOnly": "bun run build"
27
- },
28
- "dependencies": {
29
- "@opencode-ai/plugin": "^1.15.7",
30
- "@opencode-ai/sdk": "^1.15.7",
31
- "zod": "^4.4.3"
32
- },
33
- "peerDependencies": {
34
- "typescript": "^6"
35
- },
36
- "devDependencies": {
37
- "@types/bun": "^1.3.14",
38
- "typescript": "^6.0.3"
39
- }
2
+ "name": "@mcowger/opencode-plexus",
3
+ "version": "0.8.1",
4
+ "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
+ "type": "module",
6
+ "module": "./dist/index.js",
7
+ "main": "./dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./server": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist/index.js",
14
+ "package.json",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "plexus",
19
+ "opencode",
20
+ "ai",
21
+ "coding-agent",
22
+ "extension",
23
+ "plugin"
24
+ ],
25
+ "scripts": {
26
+ "build": "bun run build.ts"
27
+ },
28
+ "dependencies": {
29
+ "@opencode-ai/plugin": "^1.15.7",
30
+ "@opencode-ai/sdk": "^1.15.7"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^5"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bun": "latest"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/mcowger/plexus-agent-plugins.git",
44
+ "directory": "packages/plexus-opencode"
45
+ },
46
+ "license": "MIT"
40
47
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Matt Cowger
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.