@mcowger/opencode-plexus 0.1.0 → 0.7.0

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 +149 -95
  2. package/dist/index.js +24 -74
  3. package/package.json +45 -34
  4. package/LICENSE +0 -21
package/README.md CHANGED
@@ -1,142 +1,196 @@
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.
43
+ Then register the path in `~/.pi/agent/settings.json`:
39
44
 
40
- ### Option C — Manual `opencode.json`
41
-
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
+ You can also pre-configure via environment variables:
120
+
121
+ ```sh
122
+ export PLEXUS_BASE_URL=https://plexus.example.com
123
+ export PLEXUS_API_KEY=your-api-key
124
+ ```
121
125
 
122
126
  ---
123
127
 
124
- ## Troubleshooting
128
+ ## Configuration files
129
+
130
+ ### pi
131
+
132
+ ```
133
+ ~/.pi/agent/extensions/plexus/
134
+ config.json # base URL and optional default model
135
+ plexus-models-cache.json # last-fetched model list (startup cache)
136
+ plexus-models-response.json # raw API response (diagnostics)
137
+ plexus.log # extension activity log
138
+ ```
139
+
140
+ The API key is stored in pi's own credential store (`auth.json`) — never in a separate file.
141
+
142
+ ### OpenCode
143
+
144
+ ```
145
+ ~/.local/share/opencode/plugins/plexus/
146
+ models-cache.json # last-fetched model list (startup cache)
147
+ models-raw.json # raw API response (diagnostics)
148
+ ```
125
149
 
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`.
150
+ The API key is stored in OpenCode's own credential store — never in a separate file.
129
151
 
130
- **Cache location**
131
- The model cache lives under OpenCode's own state directory:
152
+ ---
153
+
154
+ ## Package layout
132
155
 
133
156
  ```
134
- ~/.local/share/opencode/plugins/plexus/models-cache.json
135
- ~/.local/share/opencode/plugins/plexus/models-raw.json
157
+ packages/
158
+ plexus-models/ # host-agnostic data layer
159
+ src/
160
+ types.ts # wire types (PlexusApiModel, PlexusModelDescriptor, etc.)
161
+ convert.ts # model fetching, conversion, compat detection
162
+ index.ts # barrel export
163
+ plexus-pi/ # pi host adapter
164
+ src/
165
+ extension.ts # entry point: commands, session refresh, auth flow
166
+ mapper.ts # PlexusModelDescriptor → pi ProviderModelConfig
167
+ config.ts # base URL / default model config I/O
168
+ cache.ts # model cache I/O
169
+ log.ts # append-only log
170
+ package.json # declares pi.extensions entry point
171
+ plexus-opencode/ # OpenCode plugin adapter
172
+ src/
173
+ plugin.ts # Plugin export: config hook, auth handler
174
+ mapper.ts # PlexusApiModel → OpenCode ConfigModel
175
+ cache.ts # model cache I/O
176
+ config-store.ts # resolveConfig, persistToGlobalConfig
177
+ log.ts # logger via OpenCode SDK
178
+ constants.ts # provider ID, env var names, timeouts
179
+ url.ts # URL helpers (trimURL, apiBase, modelsUrl)
180
+ index.ts # barrel export
181
+ package.json # npm package manifest
182
+ ```
183
+
184
+ `plexus-models` has zero imports from any agent framework. Each host adapter imports it via a relative path.
185
+
186
+ ## Development
187
+
188
+ After cloning, install dependencies to set up the pre-commit hook:
189
+
190
+ ```sh
191
+ bun install
136
192
  ```
137
193
 
138
- Deleting OpenCode's state directory (the documented reset path) also clears this cache.
194
+ The pre-commit hook (via lefthook) rebuilds both dist artifacts automatically whenever source files change. After committing, reload/restart your agent.
139
195
 
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.
196
+ To add support for a new host agent, see [AGENTS.md](AGENTS.md).
package/dist/index.js CHANGED
@@ -9,9 +9,23 @@ var ENV_BASE_URL = "PLEXUS_BASE_URL";
9
9
  var ENV_API_KEY = "PLEXUS_API_KEY";
10
10
  var MODELS_FETCH_TIMEOUT_MS = 1e4;
11
11
  var REFRESH_TTL_MS = 60000;
12
- var DEFAULT_CONTEXT = 8192;
13
12
  var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
14
13
 
14
+ // ../plexus-models/src/convert.ts
15
+ var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
16
+ async function fetchPlexusModels(apiKey, modelsUrl) {
17
+ const res = await fetch(modelsUrl, {
18
+ headers: {
19
+ Authorization: `Bearer ${apiKey}`,
20
+ Accept: "application/json"
21
+ }
22
+ });
23
+ if (!res.ok) {
24
+ throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
25
+ }
26
+ const raw = await res.json();
27
+ return { models: raw.data ?? [], raw };
28
+ }
15
29
  // src/cache.ts
16
30
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
17
31
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -155,8 +169,9 @@ function createLogger(client) {
155
169
  };
156
170
  }
157
171
 
158
- // src/models.ts
159
- var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
172
+ // src/mapper.ts
173
+ var REASONING_PARAMS2 = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
174
+ var DEFAULT_CONTEXT = 8192;
160
175
  function parsePrice(value) {
161
176
  if (!value)
162
177
  return 0;
@@ -235,7 +250,7 @@ function buildModels(models) {
235
250
  }
236
251
  } : {},
237
252
  ...params.includes("tools") ? { tool_call: true } : {},
238
- ...params.some((p) => REASONING_PARAMS.has(p)) ? { reasoning: true } : {},
253
+ ...params.some((p) => REASONING_PARAMS2.has(p)) ? { reasoning: true } : {},
239
254
  ...params.includes("temperature") ? { temperature: true } : {},
240
255
  ...hasNonTextInput ? { attachment: true } : {}
241
256
  };
@@ -244,79 +259,14 @@ function buildModels(models) {
244
259
  return result;
245
260
  }
246
261
 
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
262
  // src/plugin.ts
314
263
  var lastRefresh = null;
315
264
  async function refreshModels(client, baseURL, apiKey) {
316
265
  if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
317
266
  return lastRefresh.models;
318
267
  }
319
- const { models: apiModels, raw } = await fetchPlexusModels(baseURL, apiKey);
268
+ const url = modelsUrl(baseURL);
269
+ const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
320
270
  const built = buildModels(apiModels);
321
271
  lastRefresh = { at: Date.now(), models: built };
322
272
  writeCache(client, built, raw).catch(() => {});
@@ -398,7 +348,8 @@ var PlexusProviderPlugin = async (ctx) => {
398
348
  if (!baseURL || !apiKey)
399
349
  return { type: "failed" };
400
350
  try {
401
- await fetchPlexusModels(baseURL);
351
+ const url = modelsUrl(baseURL);
352
+ await fetchPlexusModels("", url);
402
353
  } catch (e) {
403
354
  log.error(`Plexus URL probe failed at ${baseURL}: ${String(e)}`);
404
355
  return { type: "failed" };
@@ -436,6 +387,5 @@ export {
436
387
  OPENAI_COMPATIBLE_NPM,
437
388
  MODELS_FETCH_TIMEOUT_MS,
438
389
  ENV_BASE_URL,
439
- ENV_API_KEY,
440
- DEFAULT_CONTEXT
390
+ ENV_API_KEY
441
391
  };
package/package.json CHANGED
@@ -1,36 +1,47 @@
1
1
  {
2
- "name": "@mcowger/opencode-plexus",
3
- "version": "0.1.0",
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
- "publishConfig": {
15
- "access": "public"
16
- },
17
- "scripts": {
18
- "build": "bun build src/index.ts --outdir=dist --target=bun --format=esm --packages=external",
19
- "watch": "bun build src/index.ts --outdir=dist --target=bun --format=esm --packages=external --watch",
20
- "typecheck": "tsc --noEmit",
21
- "test": "bun test ./tests",
22
- "prepublishOnly": "bun run build"
23
- },
24
- "dependencies": {
25
- "@opencode-ai/plugin": "^1.15.7",
26
- "@opencode-ai/sdk": "^1.15.7",
27
- "zod": "^4.4.3"
28
- },
29
- "peerDependencies": {
30
- "typescript": "^6"
31
- },
32
- "devDependencies": {
33
- "@types/bun": "^1.3.14",
34
- "typescript": "^6.0.3"
35
- }
2
+ "name": "@mcowger/opencode-plexus",
3
+ "version": "0.7.0",
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"
36
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.