@vtstech/pi-api 1.1.8 → 1.2.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 (2) hide show
  1. package/api.js +155 -111
  2. package/package.json +2 -2
package/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // .build-npm/api/api.temp.ts
2
2
  import { section, ok, info, warn } from "@vtstech/pi-shared/format";
3
- import { readModelsJson, writeModelsJson, getOllamaBaseUrl, BUILTIN_PROVIDERS, EXTENSION_VERSION } from "@vtstech/pi-shared/ollama";
3
+ import { readModelsJson, readModifyWriteModelsJson, getOllamaBaseUrl, BUILTIN_PROVIDERS, EXTENSION_VERSION, isLocalProvider } from "@vtstech/pi-shared/ollama";
4
4
  import { SETTINGS_PATH, readSettings, writeSettings } from "@vtstech/pi-shared/config-io";
5
5
  var API_MODES = {
6
6
  "anthropic-messages": "Anthropic Claude API and compatibles",
@@ -38,19 +38,31 @@ var COMPAT_FLAGS = {
38
38
  };
39
39
  function getLocalProvider(config) {
40
40
  for (const [name, provider] of Object.entries(config.providers)) {
41
- const url = provider.baseUrl || "";
42
- if (url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0") || name === "ollama") {
41
+ if (isLocalProvider(provider.baseUrl || "", name)) {
43
42
  return { name, isLocal: true };
44
43
  }
45
44
  }
46
45
  const first = Object.keys(config.providers)[0];
47
46
  return { name: first || "ollama", isLocal: false };
48
47
  }
49
- function resolveProvider(config, explicit) {
50
- const target = explicit || getLocalProvider(config).name;
51
- const provider = config.providers[target];
48
+ function findProvider(providerName) {
49
+ const config = readModelsJson();
50
+ const provider = config.providers[providerName];
51
+ return provider || null;
52
+ }
53
+ function resolveProvider(config, explicit, currentProvider) {
54
+ if (explicit) {
55
+ const provider2 = config.providers[explicit];
56
+ if (!provider2) return null;
57
+ return { name: explicit, config: provider2 };
58
+ }
59
+ if (currentProvider && config.providers[currentProvider]) {
60
+ return { name: currentProvider, config: config.providers[currentProvider] };
61
+ }
62
+ const local = getLocalProvider(config);
63
+ const provider = config.providers[local.name];
52
64
  if (!provider) return null;
53
- return { name: target, config: provider };
65
+ return { name: local.name, config: provider };
54
66
  }
55
67
  function api_temp_default(pi) {
56
68
  const branding = [
@@ -61,50 +73,52 @@ function api_temp_default(pi) {
61
73
  ].join("\n");
62
74
  pi.registerCommand("api", {
63
75
  description: "View and switch API modes, base URLs, thinking, and compat flags",
64
- handler: async (args, ctx) => {
76
+ handler: async (args, ctx2) => {
65
77
  const parts = args.trim().split(/\s+/);
66
78
  const sub = parts[0]?.toLowerCase() || "";
67
79
  const rest = parts.slice(1).join(" ");
68
80
  const config = readModelsJson();
69
81
  if (sub !== "provider" && sub !== "providers") {
70
- const provider = resolveProvider(config);
82
+ const currentProvider = getCurrentSessionProvider(ctx2);
83
+ const provider = resolveProvider(config, void 0, currentProvider);
71
84
  if (!provider) {
72
- ctx.ui.notify("No providers found in models.json", "error");
85
+ ctx2.ui.notify("No providers found in models.json", "error");
73
86
  return;
74
87
  }
75
88
  switch (sub) {
76
89
  case "":
77
90
  case "show":
78
- return showConfig(provider);
91
+ return showConfig(provider, currentProvider);
79
92
  case "mode":
80
- return setMode(ctx, provider.name, rest);
93
+ return setMode(ctx2, provider.name, rest);
81
94
  case "url":
82
- return setUrl(ctx, provider.name, rest);
95
+ return setUrl(ctx2, provider.name, rest);
83
96
  case "think":
84
- return setThink(ctx, provider.name, rest);
97
+ return setThink(ctx2, provider.name, rest);
85
98
  case "compat":
86
- return handleCompat(ctx, provider.name, rest);
99
+ return handleCompat(ctx2, provider.name, rest);
87
100
  case "reload":
88
- return reloadConfig(ctx);
101
+ return reloadConfig(ctx2);
89
102
  case "modes":
90
103
  return listModes();
91
104
  default:
92
- ctx.ui.notify(`Unknown sub-command: "${sub}". Use: mode, url, think, compat, reload, modes, provider, providers`, "error");
105
+ ctx2.ui.notify(`Unknown sub-command: "${sub}". Use: mode, url, think, compat, reload, modes, provider, providers`, "error");
93
106
  }
94
107
  }
95
108
  if (sub === "provider" || sub === "providers") {
96
- return handleProvider(ctx, config, rest);
109
+ return handleProvider(ctx2, config, rest);
97
110
  }
98
111
  }
99
112
  });
100
- function showConfig(provider) {
113
+ function showConfig(provider, currentProvider) {
101
114
  const p = provider.config;
102
115
  const compat = p.compat || {};
103
116
  const modelCount = p.models?.length || 0;
104
117
  const firstModel = p.models?.[0]?.id || "none";
118
+ const isCurrent = currentProvider === provider.name;
105
119
  const lines = [branding];
106
120
  lines.push(section("CURRENT PROVIDER CONFIG"));
107
- lines.push(info(`Provider: ${provider.name}`));
121
+ lines.push(info(`Provider: ${provider.name}${isCurrent ? " \u25C0 current" : ""}`));
108
122
  lines.push(info(`API mode: ${p.api || "(not set)"}`));
109
123
  lines.push(info(`Base URL: ${p.baseUrl || "(not set)"}`));
110
124
  lines.push(info(`API key: ${p.apiKey ? "\u2022\u2022\u2022\u2022" + String(p.apiKey).slice(-4) : "(not set)"}`));
@@ -119,15 +133,20 @@ function api_temp_default(pi) {
119
133
  lines.push(section("RESOLVED"));
120
134
  lines.push(info(`Ollama base: ${ollamaBase}`));
121
135
  lines.push(info(`(strip /v1 \u2192 ${ollamaBase})`));
136
+ if (currentProvider && currentProvider !== provider.name) {
137
+ lines.push(section("NOTE"));
138
+ lines.push(info(`Current session is using: ${currentProvider}`));
139
+ lines.push(info(`Use /api provider set ${provider.name} to switch to this provider`));
140
+ }
122
141
  pi.sendMessage({
123
142
  customType: "api-config",
124
143
  content: lines.join("\n"),
125
144
  display: { type: "content", content: lines.join("\n") }
126
145
  });
127
146
  }
128
- function setMode(ctx, providerName, mode) {
147
+ async function setMode(ctx2, providerName, mode) {
129
148
  if (!mode) {
130
- ctx.ui.notify("Usage: /api mode <mode>. Use /api modes to list available modes.", "error");
149
+ ctx2.ui.notify("Usage: /api mode <mode>. Use /api modes to list available modes.", "error");
131
150
  return;
132
151
  }
133
152
  const modeLower = mode.toLowerCase();
@@ -136,18 +155,21 @@ function api_temp_default(pi) {
136
155
  matched = Object.keys(API_MODES).find((m) => m.includes(modeLower));
137
156
  }
138
157
  if (!matched) {
139
- ctx.ui.notify(`Unknown API mode: "${mode}". Use /api modes to list available modes.`, "error");
158
+ ctx2.ui.notify(`Unknown API mode: "${mode}". Use /api modes to list available modes.`, "error");
140
159
  return;
141
160
  }
142
- const config = readModelsJson();
143
- const provider = config.providers[providerName];
144
- if (!provider) {
145
- ctx.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
161
+ let oldMode = "(not set)";
162
+ const written = await readModifyWriteModelsJson((config) => {
163
+ const provider = config.providers[providerName];
164
+ if (!provider) return null;
165
+ oldMode = provider.api || "(not set)";
166
+ provider.api = matched;
167
+ return config;
168
+ });
169
+ if (!written) {
170
+ ctx2.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
146
171
  return;
147
172
  }
148
- const oldMode = provider.api || "(not set)";
149
- provider.api = matched;
150
- writeModelsJson(config);
151
173
  const lines = [branding];
152
174
  lines.push(section("API MODE CHANGED"));
153
175
  lines.push(ok(`Provider: ${providerName}`));
@@ -160,11 +182,11 @@ function api_temp_default(pi) {
160
182
  content: lines.join("\n"),
161
183
  display: { type: "content", content: lines.join("\n") }
162
184
  });
163
- ctx.ui.notify(`API mode set to ${matched}`, "success");
185
+ ctx2.ui.notify(`API mode set to ${matched}`, "success");
164
186
  }
165
- function setUrl(ctx, providerName, url) {
187
+ async function setUrl(ctx2, providerName, url) {
166
188
  if (!url) {
167
- ctx.ui.notify("Usage: /api url <base-url>", "error");
189
+ ctx2.ui.notify("Usage: /api url <base-url>", "error");
168
190
  return;
169
191
  }
170
192
  let normalizedUrl = url.trim();
@@ -174,22 +196,25 @@ function api_temp_default(pi) {
174
196
  try {
175
197
  new URL(normalizedUrl);
176
198
  } catch {
177
- ctx.ui.notify(`Invalid URL: "${url}"`, "error");
199
+ ctx2.ui.notify(`Invalid URL: "${url}"`, "error");
178
200
  return;
179
201
  }
180
- const config = readModelsJson();
181
- const provider = config.providers[providerName];
182
- if (!provider) {
183
- ctx.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
202
+ let oldUrl = "(not set)";
203
+ const written = await readModifyWriteModelsJson((config) => {
204
+ const provider = config.providers[providerName];
205
+ if (!provider) return null;
206
+ const apiMode = provider.api || "";
207
+ if (apiMode.includes("openai") && !normalizedUrl.endsWith("/v1")) {
208
+ normalizedUrl = normalizedUrl.replace(/\/+$/, "") + "/v1";
209
+ }
210
+ oldUrl = provider.baseUrl || "(not set)";
211
+ provider.baseUrl = normalizedUrl;
212
+ return config;
213
+ });
214
+ if (!written) {
215
+ ctx2.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
184
216
  return;
185
217
  }
186
- const apiMode = provider.api || "";
187
- if (apiMode.includes("openai") && !normalizedUrl.endsWith("/v1")) {
188
- normalizedUrl = normalizedUrl.replace(/\/+$/, "") + "/v1";
189
- }
190
- const oldUrl = provider.baseUrl || "(not set)";
191
- provider.baseUrl = normalizedUrl;
192
- writeModelsJson(config);
193
218
  const lines = [branding];
194
219
  lines.push(section("BASE URL CHANGED"));
195
220
  lines.push(ok(`Provider: ${providerName}`));
@@ -201,46 +226,49 @@ function api_temp_default(pi) {
201
226
  content: lines.join("\n"),
202
227
  display: { type: "content", content: lines.join("\n") }
203
228
  });
204
- ctx.ui.notify(`Base URL set to ${normalizedUrl}`, "success");
229
+ ctx2.ui.notify(`Base URL set to ${normalizedUrl}`, "success");
205
230
  }
206
- function setThink(ctx, providerName, value) {
231
+ async function setThink(ctx2, providerName, value) {
207
232
  if (!value) {
208
- ctx.ui.notify("Usage: /api think <on|off|auto>", "error");
209
- return;
210
- }
211
- const config = readModelsJson();
212
- const provider = config.providers[providerName];
213
- if (!provider) {
214
- ctx.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
215
- return;
216
- }
217
- const models = provider.models || [];
218
- if (models.length === 0) {
219
- ctx.ui.notify(`No models found in provider "${providerName}"`, "error");
233
+ ctx2.ui.notify("Usage: /api think <on|off|auto>", "error");
220
234
  return;
221
235
  }
222
236
  const valLower = value.toLowerCase();
223
- const setAll = (state) => {
224
- for (const model of models) {
225
- if (state === null) {
226
- const name = (model.id || "").toLowerCase();
227
- model.reasoning = name.includes("deepseek-r1") || name.includes("qwq") || name.includes("o1") || name.includes("o3") || name.includes("think") || name.includes("qwen3");
228
- } else {
229
- model.reasoning = state;
237
+ let models = [];
238
+ const written = await readModifyWriteModelsJson((config) => {
239
+ const provider = config.providers[providerName];
240
+ if (!provider) return null;
241
+ models = provider.models || [];
242
+ if (models.length === 0) return null;
243
+ const setAll = (state) => {
244
+ for (const model of models) {
245
+ if (state === null) {
246
+ const name = (model.id || "").toLowerCase();
247
+ model.reasoning = name.includes("deepseek-r1") || name.includes("qwq") || name.includes("o1") || name.includes("o3") || name.includes("think") || name.includes("qwen3") || name.includes("glm-4");
248
+ } else {
249
+ model.reasoning = state;
250
+ }
230
251
  }
252
+ };
253
+ if (valLower === "on" || valLower === "true" || valLower === "1") {
254
+ setAll(true);
255
+ } else if (valLower === "off" || valLower === "false" || valLower === "0") {
256
+ setAll(false);
257
+ } else if (valLower === "auto") {
258
+ setAll(null);
259
+ } else {
260
+ return null;
231
261
  }
232
- };
233
- if (valLower === "on" || valLower === "true" || valLower === "1") {
234
- setAll(true);
235
- } else if (valLower === "off" || valLower === "false" || valLower === "0") {
236
- setAll(false);
237
- } else if (valLower === "auto") {
238
- setAll(null);
239
- } else {
240
- ctx.ui.notify("Invalid value. Use: on, off, or auto", "error");
262
+ return config;
263
+ });
264
+ if (!written) {
265
+ ctx2.ui.notify(`Provider "${providerName}" not found in models.json`, "error");
266
+ return;
267
+ }
268
+ if (models.length === 0) {
269
+ ctx2.ui.notify(`No models found in provider "${providerName}"`, "error");
241
270
  return;
242
271
  }
243
- writeModelsJson(config);
244
272
  const lines = [branding];
245
273
  lines.push(section("THINKING MODE"));
246
274
  lines.push(info(`Provider: ${providerName}`));
@@ -255,20 +283,19 @@ function api_temp_default(pi) {
255
283
  content: lines.join("\n"),
256
284
  display: { type: "content", content: lines.join("\n") }
257
285
  });
258
- ctx.ui.notify(`Thinking set to ${valLower} for ${models.length} model(s)`, "success");
286
+ ctx2.ui.notify(`Thinking set to ${valLower} for ${models.length} model(s)`, "success");
259
287
  }
260
- function handleCompat(ctx, providerName, args) {
288
+ async function handleCompat(ctx2, providerName, args) {
261
289
  const parts = args.split(/\s+/);
262
290
  const key = parts[0];
263
291
  const value = parts.slice(1).join(" ");
264
292
  if (!key) {
265
- const config2 = readModelsJson();
266
- const provider2 = config2.providers[providerName];
267
- if (!provider2) {
268
- ctx.ui.notify(`Provider "${providerName}" not found`, "error");
293
+ const provider = findProvider(providerName);
294
+ if (!provider) {
295
+ ctx2.ui.notify(`Provider "${providerName}" not found`, "error");
269
296
  return;
270
297
  }
271
- const compat = provider2.compat || {};
298
+ const compat = provider.compat || {};
272
299
  const lines2 = [branding];
273
300
  lines2.push(section("COMPAT FLAGS"));
274
301
  if (Object.keys(compat).length === 0) {
@@ -292,23 +319,15 @@ function api_temp_default(pi) {
292
319
  return;
293
320
  }
294
321
  if (!value) {
295
- const config2 = readModelsJson();
296
- const provider2 = config2.providers[providerName];
297
- if (!provider2) {
298
- ctx.ui.notify(`Provider "${providerName}" not found`, "error");
322
+ const provider = findProvider(providerName);
323
+ if (!provider) {
324
+ ctx2.ui.notify(`Provider "${providerName}" not found`, "error");
299
325
  return;
300
326
  }
301
- const current = provider2.compat?.[key];
302
- ctx.ui.notify(`${key} = ${current !== void 0 ? JSON.stringify(current) : "(not set)"}`, "info");
327
+ const current = provider.compat?.[key];
328
+ ctx2.ui.notify(`${key} = ${current !== void 0 ? JSON.stringify(current) : "(not set)"}`, "info");
303
329
  return;
304
330
  }
305
- const config = readModelsJson();
306
- const provider = config.providers[providerName];
307
- if (!provider) {
308
- ctx.ui.notify(`Provider "${providerName}" not found`, "error");
309
- return;
310
- }
311
- if (!provider.compat) provider.compat = {};
312
331
  let parsedValue = value;
313
332
  if (value === "true") parsedValue = true;
314
333
  else if (value === "false") parsedValue = false;
@@ -319,9 +338,19 @@ function api_temp_default(pi) {
319
338
  } catch {
320
339
  }
321
340
  }
322
- const oldValue = provider.compat[key];
323
- provider.compat[key] = parsedValue;
324
- writeModelsJson(config);
341
+ let oldValue;
342
+ const written = await readModifyWriteModelsJson((config) => {
343
+ const provider = config.providers[providerName];
344
+ if (!provider) return null;
345
+ if (!provider.compat) provider.compat = {};
346
+ oldValue = provider.compat[key];
347
+ provider.compat[key] = parsedValue;
348
+ return config;
349
+ });
350
+ if (!written) {
351
+ ctx2.ui.notify(`Provider "${providerName}" not found`, "error");
352
+ return;
353
+ }
325
354
  const lines = [branding];
326
355
  lines.push(section("COMPAT FLAG SET"));
327
356
  lines.push(ok(`Provider: ${providerName}`));
@@ -334,9 +363,9 @@ function api_temp_default(pi) {
334
363
  content: lines.join("\n"),
335
364
  display: { type: "content", content: lines.join("\n") }
336
365
  });
337
- ctx.ui.notify(`Compat flag ${key} set to ${JSON.stringify(parsedValue)}`, "success");
366
+ ctx2.ui.notify(`Compat flag ${key} set to ${JSON.stringify(parsedValue)}`, "success");
338
367
  }
339
- function reloadConfig(ctx) {
368
+ function reloadConfig(ctx2) {
340
369
  const lines = [branding];
341
370
  lines.push(section("RELOAD"));
342
371
  lines.push(ok("models.json has been modified"));
@@ -348,13 +377,14 @@ function api_temp_default(pi) {
348
377
  content: lines.join("\n"),
349
378
  display: { type: "content", content: lines.join("\n") }
350
379
  });
351
- ctx.ui.notify("Run /reload to apply models.json changes", "info");
380
+ ctx2.ui.notify("Run /reload to apply models.json changes", "info");
352
381
  }
353
382
  function listModes() {
354
383
  const lines = [branding];
355
384
  lines.push(section("SUPPORTED API MODES"));
356
385
  const config = readModelsJson();
357
- const provider = resolveProvider(config);
386
+ const currentProvider = getCurrentSessionProvider(ctx);
387
+ const provider = resolveProvider(config, void 0, currentProvider);
358
388
  const currentMode = provider?.config?.api || "(not set)";
359
389
  for (const [mode, description] of Object.entries(API_MODES)) {
360
390
  const isActive = mode === currentMode;
@@ -369,7 +399,7 @@ function api_temp_default(pi) {
369
399
  display: { type: "content", content: lines.join("\n") }
370
400
  });
371
401
  }
372
- function handleProvider(ctx, config, arg) {
402
+ function handleProvider(ctx2, config, arg) {
373
403
  const parts = arg.trim().split(/\s+/);
374
404
  const sub = parts[0]?.toLowerCase() || "";
375
405
  const rest = parts.slice(1).join(" ");
@@ -393,7 +423,7 @@ function api_temp_default(pi) {
393
423
  const url = p.baseUrl || "(no URL)";
394
424
  const api = p.api || "(no mode)";
395
425
  const isDefault = name === defaultProvider;
396
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0") || name === "ollama";
426
+ const isLocal = isLocalProvider(url, name);
397
427
  const marker = isDefault ? ok(" \u25C0 default") : "";
398
428
  const tag = isLocal ? " (local)" : " (cloud)";
399
429
  lines.push(info(` ${name}${tag}${marker}`));
@@ -438,7 +468,7 @@ function api_temp_default(pi) {
438
468
  lines2.push(info("Configured providers:"));
439
469
  for (const name of providerNames) {
440
470
  const p = config.providers[name];
441
- const isLocal = (p.baseUrl || "").includes("localhost") || (p.baseUrl || "").includes("127.0.0.1") || name === "ollama";
471
+ const isLocal = isLocalProvider(p.baseUrl || "", name);
442
472
  lines2.push(info(` ${name}${isLocal ? " (local)" : " (cloud)"}`));
443
473
  }
444
474
  const builtins = Object.keys(BUILTIN_PROVIDERS).filter((n) => !providerNames.includes(n));
@@ -453,13 +483,13 @@ function api_temp_default(pi) {
453
483
  content: lines2.join("\n"),
454
484
  display: { type: "content", content: lines2.join("\n") }
455
485
  });
456
- ctx.ui.notify("Specify a provider name: /api provider set <name>", "info");
486
+ ctx2.ui.notify("Specify a provider name: /api provider set <name>", "info");
457
487
  return;
458
488
  }
459
489
  const isBuiltin = targetName in BUILTIN_PROVIDERS;
460
490
  if (!config.providers[targetName] && !isBuiltin) {
461
491
  const allNames = [...providerNames, ...Object.keys(BUILTIN_PROVIDERS).filter((n) => !providerNames.includes(n))];
462
- ctx.ui.notify(`Provider "${targetName}" not found. Available: ${allNames.join(", ")}`, "error");
492
+ ctx2.ui.notify(`Provider "${targetName}" not found. Available: ${allNames.join(", ")}`, "error");
463
493
  return;
464
494
  }
465
495
  const settings = readSettings();
@@ -491,13 +521,27 @@ function api_temp_default(pi) {
491
521
  content: lines.join("\n"),
492
522
  display: { type: "content", content: lines.join("\n") }
493
523
  });
494
- ctx.ui.notify(`Default provider set to ${targetName}`, "success");
524
+ ctx2.ui.notify(`Default provider set to ${targetName}`, "success");
495
525
  return;
496
526
  }
497
527
  if (providerNames.includes(sub) || sub in BUILTIN_PROVIDERS) {
498
- return handleProvider(ctx, config, `set ${sub}`);
528
+ return handleProvider(ctx2, config, `set ${sub}`);
529
+ }
530
+ ctx2.ui.notify(`Unknown sub-command: "${sub}". Use: show, set, list, or a provider name`, "error");
531
+ }
532
+ function getCurrentSessionProvider(ctx2) {
533
+ if (ctx2.session?.state?.provider) {
534
+ return ctx2.session.state.provider;
535
+ }
536
+ if (ctx2.state?.provider) {
537
+ return ctx2.state.provider;
538
+ }
539
+ try {
540
+ const settings = readSettings();
541
+ return settings.defaultProvider;
542
+ } catch {
543
+ return void 0;
499
544
  }
500
- ctx.ui.notify(`Unknown sub-command: "${sub}". Use: show, set, list, or a provider name`, "error");
501
545
  }
502
546
  pi.registerCompletion?.("api", {
503
547
  getCompletions: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-api",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "API Mode Switcher extension for Pi Coding Agent",
5
5
  "main": "api.js",
6
6
  "keywords": ["pi-extensions"],
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
15
  },
16
16
  "dependencies": {
17
- "@vtstech/pi-shared": "1.1.8"
17
+ "@vtstech/pi-shared": "1.2.0"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@mariozechner/pi-coding-agent": ">=0.66"