@mcowger/oh-my-pi-plexus 1.3.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.
- package/dist/extension.js +651 -0
- package/package.json +45 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../plexus-models/src/convert.ts
|
|
3
|
+
var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
|
|
4
|
+
var NON_CHAT_PATTERN = /(?:^|[\W_])(?:embed(?:ding|dings)?|transcri(?:be[ds]?|ptions?)|whisper|speech[\W_]*to[\W_]*text|stt|text[\W_]*to[\W_]*speech|tts|image[\W_]*(?:gen(?:eration)?|\d+)|diffusion|dall[\W_]*e|stable[\W_]*diffusion|sdxl|dream)(?:$|[\W_])/i;
|
|
5
|
+
var API_DIALECT_MAP = {
|
|
6
|
+
chat_completions: "openai-completions",
|
|
7
|
+
"openai-completions": "openai-completions",
|
|
8
|
+
messages: "anthropic-messages",
|
|
9
|
+
"anthropic-messages": "anthropic-messages",
|
|
10
|
+
gemini: "google-generative-ai",
|
|
11
|
+
"google-generative-ai": "google-generative-ai",
|
|
12
|
+
responses: "openai-responses",
|
|
13
|
+
"openai-responses": "openai-responses"
|
|
14
|
+
};
|
|
15
|
+
function mapPreferredApi(raw) {
|
|
16
|
+
if (raw === undefined)
|
|
17
|
+
return "openai-completions";
|
|
18
|
+
const candidates = Array.isArray(raw) ? raw : [raw];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
const mapped = API_DIALECT_MAP[candidate];
|
|
21
|
+
if (mapped !== undefined)
|
|
22
|
+
return mapped;
|
|
23
|
+
}
|
|
24
|
+
return "openai-completions";
|
|
25
|
+
}
|
|
26
|
+
function adjustBaseUrl(baseUrl, preferredApi) {
|
|
27
|
+
const stripped = baseUrl.replace(/\/+$/, "");
|
|
28
|
+
switch (preferredApi) {
|
|
29
|
+
case "anthropic-messages":
|
|
30
|
+
return stripped.endsWith("/v1") ? stripped.slice(0, -3) : stripped;
|
|
31
|
+
case "google-generative-ai":
|
|
32
|
+
return stripped.endsWith("/v1") ? `${stripped.slice(0, -3)}/v1beta` : stripped;
|
|
33
|
+
default:
|
|
34
|
+
return stripped;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function mapInputModalities(model) {
|
|
38
|
+
const raw = model.architecture?.input_modalities;
|
|
39
|
+
if (!raw || raw.length === 0)
|
|
40
|
+
return ["text"];
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const m of raw) {
|
|
43
|
+
if (m === "text" || m === "image")
|
|
44
|
+
result.push(m);
|
|
45
|
+
}
|
|
46
|
+
return result.length > 0 ? result : ["text"];
|
|
47
|
+
}
|
|
48
|
+
function inferReasoning(model) {
|
|
49
|
+
const params = model.supported_parameters;
|
|
50
|
+
if (!params)
|
|
51
|
+
return false;
|
|
52
|
+
return params.some((p) => REASONING_PARAMS.has(p));
|
|
53
|
+
}
|
|
54
|
+
function parsePrice(raw) {
|
|
55
|
+
if (raw === undefined)
|
|
56
|
+
return 0;
|
|
57
|
+
const n = parseFloat(raw);
|
|
58
|
+
return isFinite(n) && n >= 0 ? n : 0;
|
|
59
|
+
}
|
|
60
|
+
function resolveContextWindow(model) {
|
|
61
|
+
const v = model.context_length ?? model.top_provider?.context_length ?? null;
|
|
62
|
+
return v != null && v > 0 ? v : 8192;
|
|
63
|
+
}
|
|
64
|
+
function resolveMaxTokens(model, contextWindow) {
|
|
65
|
+
const v = model.top_provider?.max_completion_tokens ?? null;
|
|
66
|
+
if (v == null || v < 100) {
|
|
67
|
+
return 32768;
|
|
68
|
+
}
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
function resolvePricingTiers(model) {
|
|
72
|
+
const pricing = model.pricing;
|
|
73
|
+
if (!pricing?.tiers)
|
|
74
|
+
return;
|
|
75
|
+
const tiers = pricing.tiers.flatMap((tier) => {
|
|
76
|
+
if (!Number.isFinite(tier.input_tokens_above) || tier.input_tokens_above < 0)
|
|
77
|
+
return [];
|
|
78
|
+
return [{
|
|
79
|
+
inputTokensAbove: tier.input_tokens_above,
|
|
80
|
+
input: parsePrice(tier.prompt ?? pricing.prompt),
|
|
81
|
+
output: parsePrice(tier.completion ?? pricing.completion),
|
|
82
|
+
cacheRead: parsePrice(tier.input_cache_read ?? pricing.input_cache_read),
|
|
83
|
+
cacheWrite: parsePrice(tier.input_cache_write ?? pricing.input_cache_write)
|
|
84
|
+
}];
|
|
85
|
+
});
|
|
86
|
+
return tiers.length > 0 ? tiers : undefined;
|
|
87
|
+
}
|
|
88
|
+
function convertToDescriptor(raw, baseUrl) {
|
|
89
|
+
const preferredApi = mapPreferredApi(raw.preferred_api);
|
|
90
|
+
const adjustedBaseUrl = adjustBaseUrl(baseUrl, preferredApi);
|
|
91
|
+
const contextWindow = resolveContextWindow(raw);
|
|
92
|
+
const maxTokens = resolveMaxTokens(raw, contextWindow);
|
|
93
|
+
const tiers = resolvePricingTiers(raw);
|
|
94
|
+
const descriptor = {
|
|
95
|
+
id: raw.id,
|
|
96
|
+
name: raw.name ?? raw.id,
|
|
97
|
+
preferredApi,
|
|
98
|
+
provider: "plexus",
|
|
99
|
+
baseUrl: adjustedBaseUrl,
|
|
100
|
+
reasoning: inferReasoning(raw),
|
|
101
|
+
input: mapInputModalities(raw),
|
|
102
|
+
cost: {
|
|
103
|
+
input: parsePrice(raw.pricing?.prompt),
|
|
104
|
+
output: parsePrice(raw.pricing?.completion),
|
|
105
|
+
cacheRead: parsePrice(raw.pricing?.input_cache_read),
|
|
106
|
+
cacheWrite: parsePrice(raw.pricing?.input_cache_write),
|
|
107
|
+
...tiers !== undefined ? { tiers } : {}
|
|
108
|
+
},
|
|
109
|
+
contextWindow,
|
|
110
|
+
maxTokens
|
|
111
|
+
};
|
|
112
|
+
if (raw.pi_provider)
|
|
113
|
+
descriptor.piProvider = raw.pi_provider;
|
|
114
|
+
if (raw.pi_model)
|
|
115
|
+
descriptor.piModel = raw.pi_model;
|
|
116
|
+
if (raw.pi_options && Object.keys(raw.pi_options).length > 0)
|
|
117
|
+
descriptor.piOptions = raw.pi_options;
|
|
118
|
+
return descriptor;
|
|
119
|
+
}
|
|
120
|
+
function isChatModel(model) {
|
|
121
|
+
if (!model.id)
|
|
122
|
+
return false;
|
|
123
|
+
const outputModalities = model.architecture?.output_modalities;
|
|
124
|
+
if (outputModalities !== undefined && !outputModalities.includes("text"))
|
|
125
|
+
return false;
|
|
126
|
+
const modality = model.architecture?.modality;
|
|
127
|
+
if (modality?.includes("->")) {
|
|
128
|
+
const output = modality.split("->").at(-1) ?? "";
|
|
129
|
+
if (!output.toLowerCase().includes("text"))
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const apiHints = Array.isArray(model.preferred_api) ? model.preferred_api.join(" ") : model.preferred_api ?? "";
|
|
133
|
+
return !NON_CHAT_PATTERN.test(`${model.id} ${model.name ?? ""} ${apiHints}`);
|
|
134
|
+
}
|
|
135
|
+
function convertDescriptors(models, baseUrl) {
|
|
136
|
+
const result = [];
|
|
137
|
+
for (const m of models) {
|
|
138
|
+
if (!isChatModel(m))
|
|
139
|
+
continue;
|
|
140
|
+
result.push(convertToDescriptor(m, baseUrl));
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
function detectOpenAICompletionsCompat(providerName, baseUrl) {
|
|
145
|
+
const name = providerName.toLowerCase();
|
|
146
|
+
let host = "";
|
|
147
|
+
try {
|
|
148
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
149
|
+
} catch {}
|
|
150
|
+
const isCerebras = name === "cerebras" || host.includes("cerebras");
|
|
151
|
+
const isChutes = name === "chutes.ai" || host.includes("chutes.ai");
|
|
152
|
+
const isXai = name === "xai" || host === "api.x.ai";
|
|
153
|
+
const isZai = name === "zai" || host === "api.zai.com" || host.includes("z.ai");
|
|
154
|
+
const isMoonshot = name === "moonshotai" || name === "moonshotai-cn" || host.includes("moonshot") || host.includes("kimi");
|
|
155
|
+
const isOpencode = name === "opencode" || host.includes("opencode");
|
|
156
|
+
const isCloudflareWorkers = host.includes("workers.cloudflare.com") || host.includes("ai.cloudflare.com");
|
|
157
|
+
const isCloudflareGateway = host.includes("gateway.ai.cloudflare.com");
|
|
158
|
+
const isCloudflare = isCloudflareWorkers || isCloudflareGateway;
|
|
159
|
+
const isDeepSeek = name === "deepseek" || host.includes("deepseek");
|
|
160
|
+
const isOpenRouter = name === "openrouter" || host.includes("openrouter.ai");
|
|
161
|
+
const isNonStandard = isCerebras || isChutes || isXai || isZai || isMoonshot || isOpencode || isCloudflare || isDeepSeek;
|
|
162
|
+
const supportsStore = !isNonStandard;
|
|
163
|
+
const supportsDeveloperRole = !isNonStandard;
|
|
164
|
+
const supportsReasoningEffort = !isXai && !isZai && !isMoonshot && !isCloudflareGateway;
|
|
165
|
+
let maxTokensField = "max_completion_tokens";
|
|
166
|
+
if (isChutes || isMoonshot || isCloudflareGateway) {
|
|
167
|
+
maxTokensField = "max_tokens";
|
|
168
|
+
}
|
|
169
|
+
let thinkingFormat = "openai";
|
|
170
|
+
if (isDeepSeek)
|
|
171
|
+
thinkingFormat = "deepseek";
|
|
172
|
+
else if (isZai)
|
|
173
|
+
thinkingFormat = "zai";
|
|
174
|
+
else if (isOpenRouter)
|
|
175
|
+
thinkingFormat = "openrouter";
|
|
176
|
+
const requiresReasoningContentOnAssistantMessages = isDeepSeek;
|
|
177
|
+
const cacheControlFormat = isOpenRouter ? "anthropic" : undefined;
|
|
178
|
+
const supportsStrictMode = !isMoonshot && !isCloudflareGateway;
|
|
179
|
+
const supportsLongCacheRetention = !isCloudflare;
|
|
180
|
+
const compat = {
|
|
181
|
+
supportsStore,
|
|
182
|
+
supportsDeveloperRole,
|
|
183
|
+
supportsReasoningEffort,
|
|
184
|
+
supportsUsageInStreaming: true,
|
|
185
|
+
maxTokensField,
|
|
186
|
+
requiresToolResultName: false,
|
|
187
|
+
requiresAssistantAfterToolResult: false,
|
|
188
|
+
requiresThinkingAsText: false,
|
|
189
|
+
requiresReasoningContentOnAssistantMessages,
|
|
190
|
+
thinkingFormat,
|
|
191
|
+
openRouterRouting: {},
|
|
192
|
+
vercelGatewayRouting: {},
|
|
193
|
+
zaiToolStream: false,
|
|
194
|
+
supportsStrictMode,
|
|
195
|
+
sendSessionAffinityHeaders: false,
|
|
196
|
+
supportsLongCacheRetention
|
|
197
|
+
};
|
|
198
|
+
if (cacheControlFormat !== undefined) {
|
|
199
|
+
compat.cacheControlFormat = cacheControlFormat;
|
|
200
|
+
}
|
|
201
|
+
return compat;
|
|
202
|
+
}
|
|
203
|
+
var DEFAULT_MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
204
|
+
async function fetchPlexusModels(apiKey, modelsUrl, timeoutMs = DEFAULT_MODELS_FETCH_TIMEOUT_MS) {
|
|
205
|
+
const controller = new AbortController;
|
|
206
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
207
|
+
try {
|
|
208
|
+
const headers = { Accept: "application/json" };
|
|
209
|
+
if (apiKey)
|
|
210
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
211
|
+
const res = await fetch(modelsUrl, {
|
|
212
|
+
headers,
|
|
213
|
+
signal: controller.signal
|
|
214
|
+
});
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
|
|
217
|
+
}
|
|
218
|
+
const raw = await res.json();
|
|
219
|
+
return { models: raw.data ?? [], raw };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
222
|
+
throw new Error(`Plexus models fetch timed out after ${timeoutMs}ms`);
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
} finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// src/config.ts
|
|
230
|
+
import { existsSync, readFileSync } from "fs";
|
|
231
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
232
|
+
import { join } from "path";
|
|
233
|
+
import { getAgentDir } from "@oh-my-pi/pi-coding-agent";
|
|
234
|
+
var getConfigDir = () => join(getAgentDir(), "extensions", "plexus");
|
|
235
|
+
var getConfigPath = () => join(getConfigDir(), "config.json");
|
|
236
|
+
var ENV_BASE_URL = "PLEXUS_BASE_URL";
|
|
237
|
+
var ENV_API_URL = "PLEXUS_API_URL";
|
|
238
|
+
var ENV_API_KEY = "PLEXUS_API_KEY";
|
|
239
|
+
var ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
240
|
+
var ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
241
|
+
var normalizeRoot = (raw) => raw.trim().replace(/\/+$/, "");
|
|
242
|
+
function resolveConfigTemplate(value) {
|
|
243
|
+
let result = "";
|
|
244
|
+
let index = 0;
|
|
245
|
+
while (index < value.length) {
|
|
246
|
+
const dollarIndex = value.indexOf("$", index);
|
|
247
|
+
if (dollarIndex < 0) {
|
|
248
|
+
result += value.slice(index);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
result += value.slice(index, dollarIndex);
|
|
252
|
+
const nextChar = value[dollarIndex + 1];
|
|
253
|
+
if (nextChar === "$" || nextChar === "!") {
|
|
254
|
+
result += nextChar;
|
|
255
|
+
index = dollarIndex + 2;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (nextChar === "{") {
|
|
259
|
+
const endIndex = value.indexOf("}", dollarIndex + 2);
|
|
260
|
+
if (endIndex < 0) {
|
|
261
|
+
result += "$";
|
|
262
|
+
index = dollarIndex + 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const name = value.slice(dollarIndex + 2, endIndex);
|
|
266
|
+
if (!ENV_VAR_NAME_RE.test(name)) {
|
|
267
|
+
result += value.slice(dollarIndex, endIndex + 1);
|
|
268
|
+
index = endIndex + 1;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const envValue = process.env[name];
|
|
272
|
+
if (envValue === undefined)
|
|
273
|
+
return;
|
|
274
|
+
result += envValue;
|
|
275
|
+
index = endIndex + 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const match = value.slice(dollarIndex + 1).match(ENV_VAR_NAME_PREFIX_RE);
|
|
279
|
+
if (match) {
|
|
280
|
+
const envValue = process.env[match[0]];
|
|
281
|
+
if (envValue === undefined)
|
|
282
|
+
return;
|
|
283
|
+
result += envValue;
|
|
284
|
+
index = dollarIndex + 1 + match[0].length;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
result += "$";
|
|
288
|
+
index = dollarIndex + 1;
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
function resolveStringOption(value) {
|
|
293
|
+
if (!value)
|
|
294
|
+
return;
|
|
295
|
+
const resolved = resolveConfigTemplate(value)?.trim();
|
|
296
|
+
return resolved || undefined;
|
|
297
|
+
}
|
|
298
|
+
var normalizeConfigBaseUrl = (raw) => {
|
|
299
|
+
const root = normalizeRoot(raw);
|
|
300
|
+
return root.endsWith("/v1") ? root.slice(0, -3) : root;
|
|
301
|
+
};
|
|
302
|
+
var normalizeApiBase = (raw) => {
|
|
303
|
+
const root = normalizeConfigBaseUrl(raw);
|
|
304
|
+
return root ? `${root}/v1` : "";
|
|
305
|
+
};
|
|
306
|
+
var cachedConfig = null;
|
|
307
|
+
function getConfigSync() {
|
|
308
|
+
if (cachedConfig)
|
|
309
|
+
return cachedConfig;
|
|
310
|
+
try {
|
|
311
|
+
if (existsSync(getConfigPath())) {
|
|
312
|
+
cachedConfig = JSON.parse(readFileSync(getConfigPath(), "utf8"));
|
|
313
|
+
return cachedConfig;
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
cachedConfig = {};
|
|
317
|
+
return cachedConfig;
|
|
318
|
+
}
|
|
319
|
+
async function saveBaseUrl(baseUrl, defaultModel) {
|
|
320
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
321
|
+
const existing = getConfigSync();
|
|
322
|
+
const config = {
|
|
323
|
+
...existing,
|
|
324
|
+
baseUrl: normalizeConfigBaseUrl(baseUrl),
|
|
325
|
+
...defaultModel !== undefined && { defaultModel }
|
|
326
|
+
};
|
|
327
|
+
await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}
|
|
328
|
+
`, "utf8");
|
|
329
|
+
cachedConfig = config;
|
|
330
|
+
}
|
|
331
|
+
async function saveDefaultModel(defaultModel) {
|
|
332
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
333
|
+
const config = { ...getConfigSync(), defaultModel };
|
|
334
|
+
await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}
|
|
335
|
+
`, "utf8");
|
|
336
|
+
cachedConfig = config;
|
|
337
|
+
}
|
|
338
|
+
function getRawBaseUrl() {
|
|
339
|
+
const config = getConfigSync();
|
|
340
|
+
return resolveStringOption(process.env[ENV_API_URL]) ?? resolveStringOption(process.env[ENV_BASE_URL]) ?? resolveStringOption(config.baseUrl) ?? null;
|
|
341
|
+
}
|
|
342
|
+
function getEnvApiKey() {
|
|
343
|
+
return resolveStringOption(process.env[ENV_API_KEY]) ?? null;
|
|
344
|
+
}
|
|
345
|
+
function getModelsUrl() {
|
|
346
|
+
const raw = getRawBaseUrl();
|
|
347
|
+
return raw ? `${normalizeApiBase(raw)}/models` : null;
|
|
348
|
+
}
|
|
349
|
+
function getBaseUrl() {
|
|
350
|
+
const raw = getRawBaseUrl();
|
|
351
|
+
return raw ? normalizeApiBase(raw) : null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/cache.ts
|
|
355
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
356
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
357
|
+
import { join as join2 } from "path";
|
|
358
|
+
import { getAgentDir as getAgentDir2 } from "@oh-my-pi/pi-coding-agent";
|
|
359
|
+
var getCacheDir = () => join2(getAgentDir2(), "extensions", "plexus");
|
|
360
|
+
var getModelsCachePath = () => join2(getCacheDir(), "plexus-models-cache.json");
|
|
361
|
+
var getRawResponsePath = () => join2(getCacheDir(), "plexus-models-response.json");
|
|
362
|
+
function parseCacheData(raw) {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(raw);
|
|
365
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
366
|
+
return null;
|
|
367
|
+
const obj = parsed;
|
|
368
|
+
if (!Array.isArray(obj["models"]))
|
|
369
|
+
return null;
|
|
370
|
+
return {
|
|
371
|
+
models: obj["models"],
|
|
372
|
+
timestamp: typeof obj["timestamp"] === "number" ? obj["timestamp"] : 0
|
|
373
|
+
};
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function readCachedModelsSync() {
|
|
379
|
+
try {
|
|
380
|
+
const p = getModelsCachePath();
|
|
381
|
+
if (!existsSync2(p))
|
|
382
|
+
return null;
|
|
383
|
+
return parseCacheData(readFileSync2(p, "utf8"));
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function writeCachedModels(models) {
|
|
389
|
+
await mkdir2(getCacheDir(), { recursive: true });
|
|
390
|
+
const payload = { models, timestamp: Date.now() };
|
|
391
|
+
await writeFile2(getModelsCachePath(), `${JSON.stringify(payload, null, 2)}
|
|
392
|
+
`, "utf8");
|
|
393
|
+
}
|
|
394
|
+
async function writeRawResponse(data) {
|
|
395
|
+
await mkdir2(getCacheDir(), { recursive: true });
|
|
396
|
+
await writeFile2(getRawResponsePath(), `${JSON.stringify(data, null, 2)}
|
|
397
|
+
`, "utf8");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/log.ts
|
|
401
|
+
import { mkdir as mkdir3, appendFile } from "fs/promises";
|
|
402
|
+
import { join as join3 } from "path";
|
|
403
|
+
import { getAgentDir as getAgentDir3 } from "@oh-my-pi/pi-coding-agent";
|
|
404
|
+
var getCacheDir2 = () => join3(getAgentDir3(), "extensions", "plexus");
|
|
405
|
+
var getLogPath = () => join3(getCacheDir2(), "plexus.log");
|
|
406
|
+
var dirEnsured = false;
|
|
407
|
+
function log(message, data) {
|
|
408
|
+
writeLogLine(message, data);
|
|
409
|
+
}
|
|
410
|
+
async function writeLogLine(message, data) {
|
|
411
|
+
try {
|
|
412
|
+
if (!dirEnsured) {
|
|
413
|
+
await mkdir3(getCacheDir2(), { recursive: true });
|
|
414
|
+
dirEnsured = true;
|
|
415
|
+
}
|
|
416
|
+
const ts = new Date().toISOString();
|
|
417
|
+
const line = data !== undefined ? `${ts} ${message} ${JSON.stringify(data)}
|
|
418
|
+
` : `${ts} ${message}
|
|
419
|
+
`;
|
|
420
|
+
await appendFile(getLogPath(), line, "utf8");
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/mapper.ts
|
|
425
|
+
import { getBundledModel } from "@oh-my-pi/pi-catalog";
|
|
426
|
+
function descriptorToOhMyPiModel(descriptor) {
|
|
427
|
+
let builtinModel;
|
|
428
|
+
if (descriptor.piProvider && descriptor.piModel) {
|
|
429
|
+
try {
|
|
430
|
+
builtinModel = getBundledModel(descriptor.piProvider, descriptor.piModel);
|
|
431
|
+
} catch {
|
|
432
|
+
builtinModel = undefined;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const cost = {
|
|
436
|
+
input: descriptor.cost.input * 1e6,
|
|
437
|
+
output: descriptor.cost.output * 1e6,
|
|
438
|
+
cacheRead: descriptor.cost.cacheRead * 1e6,
|
|
439
|
+
cacheWrite: descriptor.cost.cacheWrite * 1e6,
|
|
440
|
+
...descriptor.cost.tiers ? {
|
|
441
|
+
tiers: descriptor.cost.tiers.map((tier) => ({
|
|
442
|
+
inputTokensAbove: tier.inputTokensAbove,
|
|
443
|
+
input: tier.input * 1e6,
|
|
444
|
+
output: tier.output * 1e6,
|
|
445
|
+
cacheRead: tier.cacheRead * 1e6,
|
|
446
|
+
cacheWrite: tier.cacheWrite * 1e6
|
|
447
|
+
}))
|
|
448
|
+
} : {}
|
|
449
|
+
};
|
|
450
|
+
let compat;
|
|
451
|
+
if (descriptor.preferredApi === "openai-completions") {
|
|
452
|
+
const heuristic = detectOpenAICompletionsCompat(descriptor.piProvider ?? descriptor.provider, descriptor.baseUrl);
|
|
453
|
+
const builtinCompat = builtinModel?.compat;
|
|
454
|
+
compat = { ...heuristic, ...builtinCompat ?? {}, ...descriptor.piOptions ?? {} };
|
|
455
|
+
} else if (descriptor.piOptions) {
|
|
456
|
+
compat = descriptor.piOptions;
|
|
457
|
+
} else if (builtinModel?.compat) {
|
|
458
|
+
compat = builtinModel.compat;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
id: descriptor.id,
|
|
462
|
+
name: descriptor.name,
|
|
463
|
+
api: descriptor.preferredApi,
|
|
464
|
+
provider: descriptor.provider,
|
|
465
|
+
baseUrl: descriptor.baseUrl,
|
|
466
|
+
reasoning: descriptor.reasoning,
|
|
467
|
+
input: descriptor.input,
|
|
468
|
+
cost,
|
|
469
|
+
contextWindow: descriptor.contextWindow,
|
|
470
|
+
maxTokens: descriptor.maxTokens,
|
|
471
|
+
...builtinModel?.thinking !== undefined ? { thinking: builtinModel.thinking } : {},
|
|
472
|
+
...builtinModel?.headers !== undefined ? { headers: builtinModel.headers } : {},
|
|
473
|
+
...compat !== undefined ? { compat } : {}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/extension.ts
|
|
478
|
+
var PROVIDER_NAME = "plexus";
|
|
479
|
+
var PROVIDER_API_KEY_TEMPLATE = "${PLEXUS_API_KEY}";
|
|
480
|
+
var PLEXUS_CREDENTIAL_EXPIRES_AT = 253402300799000;
|
|
481
|
+
var currentModels = [];
|
|
482
|
+
function plexusExtension(pi) {
|
|
483
|
+
const cached = readCachedModelsSync();
|
|
484
|
+
const startupBaseUrl = getBaseUrl() ?? "http://localhost/v1";
|
|
485
|
+
const startupModels = cached?.models.map(descriptorToOhMyPiModel) ?? [];
|
|
486
|
+
log("startup", {
|
|
487
|
+
cachedModelCount: startupModels.length,
|
|
488
|
+
startupBaseUrl
|
|
489
|
+
});
|
|
490
|
+
pi.registerProvider(PROVIDER_NAME, {
|
|
491
|
+
api: "openai-completions",
|
|
492
|
+
apiKey: PROVIDER_API_KEY_TEMPLATE,
|
|
493
|
+
authHeader: true,
|
|
494
|
+
baseUrl: startupBaseUrl,
|
|
495
|
+
models: startupModels,
|
|
496
|
+
oauth: createPlexusLoginProvider(pi)
|
|
497
|
+
});
|
|
498
|
+
currentModels = startupModels;
|
|
499
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
500
|
+
const apiKey = await ctx.modelRegistry.authStorage.getApiKey(PROVIDER_NAME) ?? getEnvApiKey();
|
|
501
|
+
const baseUrl = getBaseUrl();
|
|
502
|
+
log("session_start", { hasApiKey: !!apiKey, baseUrl });
|
|
503
|
+
if (!apiKey || !baseUrl) {
|
|
504
|
+
log("session_start: no auth configured, skipping refresh");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await doRefresh(pi, apiKey, ctx);
|
|
508
|
+
});
|
|
509
|
+
pi.registerCommand("plexus", {
|
|
510
|
+
description: "Plexus provider commands: refresh, set-default-model (setup: /login plexus)",
|
|
511
|
+
getArgumentCompletions: (prefix) => {
|
|
512
|
+
const subcommands = [
|
|
513
|
+
{ value: "refresh", label: "refresh", description: "Refresh Plexus models from the API" },
|
|
514
|
+
{ value: "set-default-model", label: "set-default-model", description: "Choose the model Oh My Pi should use by default" }
|
|
515
|
+
];
|
|
516
|
+
if (!prefix.includes(" ")) {
|
|
517
|
+
return subcommands.filter((command) => command.value.startsWith(prefix));
|
|
518
|
+
}
|
|
519
|
+
const [subcommand, ...rest] = prefix.split(/\s+/);
|
|
520
|
+
if (subcommand !== "set-default-model")
|
|
521
|
+
return null;
|
|
522
|
+
const modelPrefix = rest.join(" ");
|
|
523
|
+
const choices = currentModels.map((model) => ({
|
|
524
|
+
value: model.id,
|
|
525
|
+
label: model.name === model.id ? model.id : `${model.name} (${model.id})`
|
|
526
|
+
}));
|
|
527
|
+
const filtered = choices.filter((choice) => choice.value.toLowerCase().startsWith(modelPrefix.toLowerCase()));
|
|
528
|
+
return filtered.length > 0 ? filtered : null;
|
|
529
|
+
},
|
|
530
|
+
handler: async (args, ctx) => {
|
|
531
|
+
const trimmed = args.trim();
|
|
532
|
+
const sub = trimmed.toLowerCase();
|
|
533
|
+
if (sub === "refresh" || sub === "") {
|
|
534
|
+
await handleRefresh(pi, ctx);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (sub === "set-default-model" || sub.startsWith("set-default-model ")) {
|
|
538
|
+
await handleSetDefaultModel(pi, ctx, trimmed.slice("set-default-model".length).trim());
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
ctx.ui.notify(`Unknown sub-command: "${args}". Use /login plexus, /plexus refresh, or /plexus set-default-model.`, "warning");
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function createPlexusLoginProvider(pi) {
|
|
546
|
+
return {
|
|
547
|
+
name: "Plexus",
|
|
548
|
+
async login(callbacks) {
|
|
549
|
+
const baseUrl = (await callbacks.onPrompt({
|
|
550
|
+
message: "Plexus base URL",
|
|
551
|
+
placeholder: "https://plexus.example.com"
|
|
552
|
+
})).trim();
|
|
553
|
+
if (!baseUrl)
|
|
554
|
+
throw new Error("Plexus base URL is required.");
|
|
555
|
+
const apiKey = (await callbacks.onPrompt({ message: "Plexus API key" })).trim();
|
|
556
|
+
if (!apiKey)
|
|
557
|
+
throw new Error("Plexus API key is required.");
|
|
558
|
+
await saveBaseUrl(baseUrl);
|
|
559
|
+
callbacks.onProgress?.("Refreshing Plexus models...");
|
|
560
|
+
await doRefresh(pi, apiKey, null);
|
|
561
|
+
return {
|
|
562
|
+
access: apiKey,
|
|
563
|
+
refresh: apiKey,
|
|
564
|
+
expires: PLEXUS_CREDENTIAL_EXPIRES_AT,
|
|
565
|
+
plexusBaseUrl: baseUrl
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
async refreshToken(credentials) {
|
|
569
|
+
return { ...credentials, expires: PLEXUS_CREDENTIAL_EXPIRES_AT };
|
|
570
|
+
},
|
|
571
|
+
getApiKey(credentials) {
|
|
572
|
+
return String(credentials.access || credentials.refresh || "");
|
|
573
|
+
},
|
|
574
|
+
modifyModels(models, credentials) {
|
|
575
|
+
const baseUrl = credentials.plexusBaseUrl;
|
|
576
|
+
if (!baseUrl)
|
|
577
|
+
return models;
|
|
578
|
+
const apiBase = baseUrl.trim().replace(/\/+$/, "").endsWith("/v1") ? baseUrl.trim().replace(/\/+$/, "") : `${baseUrl.trim().replace(/\/+$/, "")}/v1`;
|
|
579
|
+
return models.map((model) => model.provider === PROVIDER_NAME ? { ...model, baseUrl: adjustBaseUrl(apiBase, model.api) } : model);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function handleRefresh(pi, ctx) {
|
|
584
|
+
const apiKey = await ctx.modelRegistry.authStorage.getApiKey(PROVIDER_NAME) ?? getEnvApiKey();
|
|
585
|
+
if (!apiKey) {
|
|
586
|
+
ctx.ui.notify("No Plexus API key configured. Run /login plexus first.", "error");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
ctx.ui.notify("Refreshing Plexus models\u2026", "info");
|
|
590
|
+
await doRefresh(pi, apiKey, ctx);
|
|
591
|
+
}
|
|
592
|
+
async function handleSetDefaultModel(pi, ctx, requestedModelId) {
|
|
593
|
+
let modelId = requestedModelId;
|
|
594
|
+
if (!modelId) {
|
|
595
|
+
if (currentModels.length === 0) {
|
|
596
|
+
ctx.ui.notify("No Plexus models are available. Run /plexus refresh first.", "warning");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const choices = currentModels.map((model2) => model2.name === model2.id ? model2.id : `${model2.name} (${model2.id})`);
|
|
600
|
+
const selected = await ctx.ui.select("Select the Plexus default model:", choices);
|
|
601
|
+
if (!selected)
|
|
602
|
+
return;
|
|
603
|
+
const selectedIndex = choices.indexOf(selected);
|
|
604
|
+
modelId = currentModels[selectedIndex]?.id ?? "";
|
|
605
|
+
}
|
|
606
|
+
const model = currentModels.find((candidate) => candidate.id === modelId);
|
|
607
|
+
if (!model) {
|
|
608
|
+
ctx.ui.notify(`Plexus model not found: "${modelId}". Run /plexus refresh and choose a model from the available list.`, "error");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
await saveDefaultModel(model.id);
|
|
612
|
+
const registryModel = ctx.modelRegistry.find(PROVIDER_NAME, model.id) ?? model;
|
|
613
|
+
const active = await pi.setModel(registryModel);
|
|
614
|
+
ctx.ui.notify(active ? `Plexus model selected: ${model.id}.` : `Plexus model ${model.id} was saved but could not be selected in this session.`, active ? "info" : "warning");
|
|
615
|
+
}
|
|
616
|
+
async function doRefresh(pi, apiKey, ctx) {
|
|
617
|
+
const modelsUrl = getModelsUrl();
|
|
618
|
+
const baseUrl = getBaseUrl();
|
|
619
|
+
if (!modelsUrl || !baseUrl) {
|
|
620
|
+
if (ctx)
|
|
621
|
+
ctx.ui.notify("Plexus base URL not configured. Run /login plexus first.", "warning");
|
|
622
|
+
log("doRefresh: no base URL configured");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const { models: apiModels, raw } = await fetchPlexusModels(apiKey, modelsUrl);
|
|
627
|
+
const descriptors = convertDescriptors(apiModels, baseUrl);
|
|
628
|
+
const ohMyPiModels = descriptors.map(descriptorToOhMyPiModel);
|
|
629
|
+
await Promise.all([writeCachedModels(descriptors), writeRawResponse(raw)]);
|
|
630
|
+
currentModels = ohMyPiModels;
|
|
631
|
+
pi.registerProvider(PROVIDER_NAME, {
|
|
632
|
+
api: "openai-completions",
|
|
633
|
+
apiKey: PROVIDER_API_KEY_TEMPLATE,
|
|
634
|
+
authHeader: true,
|
|
635
|
+
baseUrl,
|
|
636
|
+
models: ohMyPiModels,
|
|
637
|
+
oauth: createPlexusLoginProvider(pi)
|
|
638
|
+
});
|
|
639
|
+
log("doRefresh: registered", { count: ohMyPiModels.length });
|
|
640
|
+
if (ctx)
|
|
641
|
+
ctx.ui.notify(`Refreshed ${ohMyPiModels.length} Plexus models`, "info");
|
|
642
|
+
} catch (error) {
|
|
643
|
+
log("doRefresh: failed", { error: String(error) });
|
|
644
|
+
if (ctx) {
|
|
645
|
+
ctx.ui.notify(`Refresh failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
export {
|
|
650
|
+
plexusExtension as default
|
|
651
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcowger/oh-my-pi-plexus",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Plexus AI proxy plugin for the Oh My Pi coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/extension.js",
|
|
8
|
+
"package.json",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"plexus",
|
|
13
|
+
"oh-my-pi",
|
|
14
|
+
"omp",
|
|
15
|
+
"ai",
|
|
16
|
+
"coding-agent",
|
|
17
|
+
"extension",
|
|
18
|
+
"plugin"
|
|
19
|
+
],
|
|
20
|
+
"omp": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"dist/extension.js"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun run build.ts"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@oh-my-pi/pi-ai": ">=16.5.0",
|
|
30
|
+
"@oh-my-pi/pi-catalog": ">=16.5.0",
|
|
31
|
+
"@oh-my-pi/pi-coding-agent": ">=16.5.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@oh-my-pi/pi-ai": "^16.5.0",
|
|
35
|
+
"@oh-my-pi/pi-catalog": "^16.5.0",
|
|
36
|
+
"@oh-my-pi/pi-coding-agent": "^16.5.0",
|
|
37
|
+
"@types/bun": "latest"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/mcowger/plexus-agent-plugins.git",
|
|
42
|
+
"directory": "packages/plexus-oh-my-pi"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|