@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.
- package/README.md +162 -95
- package/dist/index.js +145 -85
- package/package.json +45 -38
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,142 +1,209 @@
|
|
|
1
|
-
#
|
|
1
|
+
# plexus-agent-plugins
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
### pi
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
#### Option 1 — npm (recommended)
|
|
20
25
|
|
|
21
|
-
```
|
|
22
|
-
|
|
26
|
+
```sh
|
|
27
|
+
cd ~/.pi/agent/extensions
|
|
28
|
+
npm install @mcowger/pi-plexus
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
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
|
-
|
|
33
|
+
```sh
|
|
34
|
+
git clone https://github.com/mcowger/plexus-agent-plugins ~/.pi/agent/extensions/plexus-agent-plugins
|
|
35
|
+
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
#### Option 3 — git clone anywhere + settings.json
|
|
32
38
|
|
|
33
|
-
```
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
### Option C — Manual `opencode.json`
|
|
43
|
+
Then register the path in `~/.pi/agent/settings.json`:
|
|
41
44
|
|
|
42
|
-
```
|
|
45
|
+
```json
|
|
43
46
|
{
|
|
44
|
-
"
|
|
45
|
-
"plexus"
|
|
46
|
-
|
|
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
|
-
|
|
55
|
+
### OpenCode
|
|
58
56
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
73
|
+
```sh
|
|
74
|
+
git clone https://github.com/mcowger/plexus-agent-plugins ~/code/plexus-agent-plugins
|
|
75
|
+
```
|
|
91
76
|
|
|
92
|
-
|
|
77
|
+
Then reference the built artifact in `opencode.json`:
|
|
93
78
|
|
|
94
|
-
```
|
|
79
|
+
```json
|
|
95
80
|
{
|
|
96
|
-
"
|
|
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
|
-
|
|
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
|
-
##
|
|
141
|
+
## Configuration files
|
|
142
|
+
|
|
143
|
+
### pi
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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/
|
|
159
|
-
var
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
320
|
-
const
|
|
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 ? {
|
|
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"] =
|
|
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"] =
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|