@victor-software-house/pi-openai-proxy 4.2.6 → 4.3.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/dist/config.d.mts CHANGED
@@ -50,10 +50,12 @@ interface ProxyConfig {
50
50
  readonly providerPrefixes: Readonly<Record<string, string>>;
51
51
  }
52
52
  declare const DEFAULT_CONFIG: Readonly<ProxyConfig>;
53
+ declare function isPublicModelIdMode(value: string): value is PublicModelIdMode;
54
+ declare function isModelExposureMode(value: string): value is ModelExposureMode;
53
55
  declare function normalizeConfig(raw: unknown): ProxyConfig;
54
56
  declare function getConfigPath(): string;
55
57
  declare function loadConfigFromFile(): ProxyConfig;
56
58
  declare function saveConfigToFile(config: ProxyConfig): void;
57
59
  declare function configToEnv(config: ProxyConfig): Record<string, string>;
58
60
  //#endregion
59
- export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, configToEnv, getConfigPath, loadConfigFromFile, normalizeConfig, saveConfigToFile };
61
+ export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
package/dist/config.mjs CHANGED
@@ -1,118 +1,3 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
4
- //#region src/config/schema.ts
5
- /**
6
- * Proxy configuration schema -- single source of truth.
7
- *
8
- * Used by both the proxy server and the pi extension.
9
- * The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
10
- * The pi extension reads and writes the JSON config file via the /proxy config panel.
11
- */
12
- const DEFAULT_CONFIG = {
13
- host: "127.0.0.1",
14
- port: 4141,
15
- authToken: "",
16
- remoteImages: false,
17
- maxBodySizeMb: 50,
18
- upstreamTimeoutSec: 120,
19
- lifetime: "detached",
20
- publicModelIdMode: "collision-prefixed",
21
- modelExposureMode: "scoped",
22
- scopedProviders: [],
23
- customModels: [],
24
- providerPrefixes: {}
25
- };
26
- function isRecord(value) {
27
- return value !== null && value !== void 0 && typeof value === "object" && !Array.isArray(value);
28
- }
29
- function clampInt(raw, min, max, fallback) {
30
- if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
31
- return Math.max(min, Math.min(max, Math.round(raw)));
32
- }
33
- const VALID_PUBLIC_ID_MODES = new Set([
34
- "collision-prefixed",
35
- "universal",
36
- "always-prefixed"
37
- ]);
38
- function isPublicModelIdMode(value) {
39
- return VALID_PUBLIC_ID_MODES.has(value);
40
- }
41
- const VALID_EXPOSURE_MODES = new Set([
42
- "all",
43
- "scoped",
44
- "custom"
45
- ]);
46
- function isModelExposureMode(value) {
47
- return VALID_EXPOSURE_MODES.has(value);
48
- }
49
- function normalizeStringArray(raw) {
50
- if (!Array.isArray(raw)) return [];
51
- const result = [];
52
- for (const item of raw) if (typeof item === "string" && item.length > 0) result.push(item);
53
- return result;
54
- }
55
- function normalizeStringRecord(raw) {
56
- if (!isRecord(raw)) return {};
57
- const result = {};
58
- for (const [key, val] of Object.entries(raw)) if (typeof val === "string" && val.length > 0) result[key] = val;
59
- return result;
60
- }
61
- function normalizeConfig(raw) {
62
- const v = isRecord(raw) ? raw : {};
63
- const rawHost = v["host"];
64
- const rawAuthToken = v["authToken"];
65
- const rawRemoteImages = v["remoteImages"];
66
- const rawPublicIdMode = v["publicModelIdMode"];
67
- const rawExposureMode = v["modelExposureMode"];
68
- return {
69
- host: typeof rawHost === "string" && rawHost.length > 0 ? rawHost : DEFAULT_CONFIG.host,
70
- port: clampInt(v["port"], 1, 65535, DEFAULT_CONFIG.port),
71
- authToken: typeof rawAuthToken === "string" ? rawAuthToken : DEFAULT_CONFIG.authToken,
72
- remoteImages: typeof rawRemoteImages === "boolean" ? rawRemoteImages : DEFAULT_CONFIG.remoteImages,
73
- maxBodySizeMb: clampInt(v["maxBodySizeMb"], 1, 500, DEFAULT_CONFIG.maxBodySizeMb),
74
- upstreamTimeoutSec: clampInt(v["upstreamTimeoutSec"], 5, 600, DEFAULT_CONFIG.upstreamTimeoutSec),
75
- lifetime: v["lifetime"] === "session" ? "session" : "detached",
76
- publicModelIdMode: typeof rawPublicIdMode === "string" && isPublicModelIdMode(rawPublicIdMode) ? rawPublicIdMode : DEFAULT_CONFIG.publicModelIdMode,
77
- modelExposureMode: typeof rawExposureMode === "string" && isModelExposureMode(rawExposureMode) ? rawExposureMode : DEFAULT_CONFIG.modelExposureMode,
78
- scopedProviders: normalizeStringArray(v["scopedProviders"]),
79
- customModels: normalizeStringArray(v["customModels"]),
80
- providerPrefixes: normalizeStringRecord(v["providerPrefixes"])
81
- };
82
- }
83
- function getConfigPath() {
84
- return resolve(process.env["PI_CODING_AGENT_DIR"] ?? resolve(process.env["HOME"] ?? "~", ".pi", "agent"), "proxy-config.json");
85
- }
86
- function loadConfigFromFile() {
87
- const p = getConfigPath();
88
- if (!existsSync(p)) return { ...DEFAULT_CONFIG };
89
- try {
90
- return normalizeConfig(JSON.parse(readFileSync(p, "utf-8")));
91
- } catch {
92
- return { ...DEFAULT_CONFIG };
93
- }
94
- }
95
- function saveConfigToFile(config) {
96
- const p = getConfigPath();
97
- const normalized = normalizeConfig(config);
98
- const tmp = `${p}.tmp`;
99
- try {
100
- mkdirSync(dirname(p), { recursive: true });
101
- writeFileSync(tmp, `${JSON.stringify(normalized, null, " ")}\n`, "utf-8");
102
- renameSync(tmp, p);
103
- } catch {
104
- if (existsSync(tmp)) unlinkSync(tmp);
105
- }
106
- }
107
- function configToEnv(config) {
108
- const env = {};
109
- env["PI_PROXY_HOST"] = config.host;
110
- env["PI_PROXY_PORT"] = String(config.port);
111
- if (config.authToken.length > 0) env["PI_PROXY_AUTH_TOKEN"] = config.authToken;
112
- env["PI_PROXY_REMOTE_IMAGES"] = String(config.remoteImages);
113
- env["PI_PROXY_MAX_BODY_SIZE"] = String(config.maxBodySizeMb * 1024 * 1024);
114
- env["PI_PROXY_UPSTREAM_TIMEOUT_MS"] = String(config.upstreamTimeoutSec * 1e3);
115
- return env;
116
- }
117
- //#endregion
118
- export { DEFAULT_CONFIG, configToEnv, getConfigPath, loadConfigFromFile, normalizeConfig, saveConfigToFile };
2
+ import { a as isPublicModelIdMode, c as saveConfigToFile, i as isModelExposureMode, n as configToEnv, o as loadConfigFromFile, r as getConfigPath, s as normalizeConfig, t as DEFAULT_CONFIG } from "./schema-x6mps-hM.mjs";
3
+ export { DEFAULT_CONFIG, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
package/dist/exposure.mjs CHANGED
@@ -2,7 +2,11 @@
2
2
  //#region src/openai/model-exposure.ts
3
3
  function filterExposedModels(available, allRegistered, config) {
4
4
  switch (config.modelExposureMode) {
5
- case "scoped": return [...available];
5
+ case "scoped": {
6
+ if (config.scopedProviders.length === 0) return [...available];
7
+ const allowed = new Set(config.scopedProviders);
8
+ return available.filter((m) => allowed.has(m.provider));
9
+ }
6
10
  case "all": return [...allRegistered];
7
11
  case "custom": {
8
12
  const allowed = new Set(config.customModels);
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { getConfigPath, loadConfigFromFile } from "./config.mjs";
2
+ import { l as isRecord, o as loadConfigFromFile, r as getConfigPath } from "./schema-x6mps-hM.mjs";
3
3
  import { computeModelExposure, resolveExposedModel } from "./exposure.mjs";
4
4
  import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
5
5
  import { randomBytes } from "node:crypto";
@@ -476,6 +476,8 @@ function parseAndValidateDataUri(uri) {
476
476
  }
477
477
  //#endregion
478
478
  //#region src/openai/models.ts
479
+ /** Unix timestamp (seconds) from when the module was first loaded. */
480
+ const MODULE_CREATED = Math.floor(Date.now() / 1e3);
479
481
  /**
480
482
  * Convert an ExposedModel to an OpenAI model object.
481
483
  */
@@ -483,7 +485,7 @@ function toOpenAIModel(exposed) {
483
485
  return {
484
486
  id: exposed.publicId,
485
487
  object: "model",
486
- created: 0,
488
+ created: MODULE_CREATED,
487
489
  owned_by: exposed.provider
488
490
  };
489
491
  }
@@ -549,6 +551,7 @@ function buildChatCompletion(requestId, canonicalModelId, message) {
549
551
  object: "chat.completion",
550
552
  created: Math.floor(Date.now() / 1e3),
551
553
  model: canonicalModelId,
554
+ system_fingerprint: null,
552
555
  choices: [{
553
556
  index: 0,
554
557
  message: messageBody,
@@ -724,9 +727,31 @@ async function* streamToSSE(events, requestId, model, includeUsage) {
724
727
  }
725
728
  //#endregion
726
729
  //#region src/openai/json-schema-to-typebox.ts
727
- function isRecord$2(value) {
728
- return value !== null && typeof value === "object" && !Array.isArray(value);
729
- }
730
+ /**
731
+ * JSON Schema -> TypeBox conversion for OpenAI function tool parameters.
732
+ *
733
+ * Phase 2 contract:
734
+ * - Support a documented subset of JSON Schema only
735
+ * - Reject unsupported schema constructs with ConversionError
736
+ * - Do not silently downgrade complex schemas
737
+ *
738
+ * Supported subset:
739
+ * - type: object, string, number, integer, boolean, array, null
740
+ * - properties, required
741
+ * - enum (string enums)
742
+ * - arrays with supported item schema
743
+ * - nullable via type: [T, "null"]
744
+ * - anyOf for nullable types and simple unions (max 10 branches)
745
+ * - description on any schema node
746
+ *
747
+ * Rejected:
748
+ * - $ref
749
+ * - oneOf, allOf
750
+ * - recursive schemas
751
+ * - additionalProperties as a schema (boolean true/false allowed)
752
+ * - patternProperties
753
+ * - if/then/else
754
+ */
730
755
  /**
731
756
  * Unsupported JSON Schema keywords that we reject explicitly.
732
757
  * Note: `anyOf` is handled separately for common patterns (nullable types, simple unions).
@@ -747,7 +772,7 @@ const REJECTED_KEYWORDS = [
747
772
  * Returns a ConversionError for unsupported constructs.
748
773
  */
749
774
  function jsonSchemaToTypebox(schema, path = "") {
750
- if (!isRecord$2(schema)) return {
775
+ if (!isRecord(schema)) return {
751
776
  ok: false,
752
777
  message: "Schema must be an object",
753
778
  path
@@ -894,7 +919,7 @@ function convertObject(schema, path, opts) {
894
919
  ok: true,
895
920
  schema: Type.Object({}, opts)
896
921
  };
897
- if (!isRecord$2(rawProperties)) return {
922
+ if (!isRecord(rawProperties)) return {
898
923
  ok: false,
899
924
  message: `'properties' must be an object at ${path || "root"}`,
900
925
  path
@@ -1123,16 +1148,13 @@ const rejectedFields = [
1123
1148
  *
1124
1149
  * Phase 2 contract: unknown fields -> 422, rejected fields -> 422
1125
1150
  */
1126
- function isRecord$1(value) {
1127
- return value !== null && typeof value === "object" && !Array.isArray(value);
1128
- }
1129
1151
  /**
1130
1152
  * Validate a raw request body against the Phase 1 schema.
1131
1153
  *
1132
1154
  * Also checks for known rejected fields to give friendly errors.
1133
1155
  */
1134
1156
  function validateChatRequest(body) {
1135
- if (isRecord$1(body)) {
1157
+ if (isRecord(body)) {
1136
1158
  for (const field of rejectedFields) if (body[field] !== void 0) return {
1137
1159
  ok: false,
1138
1160
  status: 422,
@@ -1170,9 +1192,6 @@ function validateChatRequest(body) {
1170
1192
  }
1171
1193
  //#endregion
1172
1194
  //#region src/pi/complete.ts
1173
- function isRecord(value) {
1174
- return value !== null && typeof value === "object" && !Array.isArray(value);
1175
- }
1176
1195
  /**
1177
1196
  * Map OpenAI reasoning_effort to pi ThinkingLevel.
1178
1197
  *
@@ -1276,7 +1295,7 @@ async function piComplete(model, context, request, options) {
1276
1295
  return completeSimple(model, context, await buildStreamOptions(model, request, options));
1277
1296
  }
1278
1297
  /**
1279
- * Streaming completion: returns an async iterable of events.
1298
+ * Streaming completion: returns an event stream with abort capability.
1280
1299
  */
1281
1300
  async function piStream(model, context, request, options) {
1282
1301
  return streamSimple(model, context, await buildStreamOptions(model, request, options));
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ //#region src/utils/guards.ts
5
+ /**
6
+ * Shared type guard utilities.
7
+ */
8
+ /**
9
+ * Narrow `unknown` to `Record<string, unknown>`.
10
+ * Rejects null, undefined, and arrays.
11
+ */
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ //#endregion
16
+ //#region src/config/schema.ts
17
+ /**
18
+ * Proxy configuration schema -- single source of truth.
19
+ *
20
+ * Used by both the proxy server and the pi extension.
21
+ * The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
22
+ * The pi extension reads and writes the JSON config file via the /proxy config panel.
23
+ */
24
+ const DEFAULT_CONFIG = {
25
+ host: "127.0.0.1",
26
+ port: 4141,
27
+ authToken: "",
28
+ remoteImages: false,
29
+ maxBodySizeMb: 50,
30
+ upstreamTimeoutSec: 120,
31
+ lifetime: "detached",
32
+ publicModelIdMode: "collision-prefixed",
33
+ modelExposureMode: "scoped",
34
+ scopedProviders: [],
35
+ customModels: [],
36
+ providerPrefixes: {}
37
+ };
38
+ function clampInt(raw, min, max, fallback) {
39
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
40
+ return Math.max(min, Math.min(max, Math.round(raw)));
41
+ }
42
+ const VALID_PUBLIC_ID_MODES = new Set([
43
+ "collision-prefixed",
44
+ "universal",
45
+ "always-prefixed"
46
+ ]);
47
+ function isPublicModelIdMode(value) {
48
+ return VALID_PUBLIC_ID_MODES.has(value);
49
+ }
50
+ const VALID_EXPOSURE_MODES = new Set([
51
+ "all",
52
+ "scoped",
53
+ "custom"
54
+ ]);
55
+ function isModelExposureMode(value) {
56
+ return VALID_EXPOSURE_MODES.has(value);
57
+ }
58
+ function normalizeStringArray(raw) {
59
+ if (!Array.isArray(raw)) return [];
60
+ const result = [];
61
+ for (const item of raw) if (typeof item === "string" && item.length > 0) result.push(item);
62
+ return result;
63
+ }
64
+ function normalizeStringRecord(raw) {
65
+ if (!isRecord(raw)) return {};
66
+ const result = {};
67
+ for (const [key, val] of Object.entries(raw)) if (typeof val === "string" && val.length > 0) result[key] = val;
68
+ return result;
69
+ }
70
+ function normalizeConfig(raw) {
71
+ const v = isRecord(raw) ? raw : {};
72
+ const rawHost = v["host"];
73
+ const rawAuthToken = v["authToken"];
74
+ const rawRemoteImages = v["remoteImages"];
75
+ const rawPublicIdMode = v["publicModelIdMode"];
76
+ const rawExposureMode = v["modelExposureMode"];
77
+ return {
78
+ host: typeof rawHost === "string" && rawHost.length > 0 ? rawHost : DEFAULT_CONFIG.host,
79
+ port: clampInt(v["port"], 1, 65535, DEFAULT_CONFIG.port),
80
+ authToken: typeof rawAuthToken === "string" ? rawAuthToken : DEFAULT_CONFIG.authToken,
81
+ remoteImages: typeof rawRemoteImages === "boolean" ? rawRemoteImages : DEFAULT_CONFIG.remoteImages,
82
+ maxBodySizeMb: clampInt(v["maxBodySizeMb"], 1, 500, DEFAULT_CONFIG.maxBodySizeMb),
83
+ upstreamTimeoutSec: clampInt(v["upstreamTimeoutSec"], 5, 600, DEFAULT_CONFIG.upstreamTimeoutSec),
84
+ lifetime: v["lifetime"] === "session" ? "session" : "detached",
85
+ publicModelIdMode: typeof rawPublicIdMode === "string" && isPublicModelIdMode(rawPublicIdMode) ? rawPublicIdMode : DEFAULT_CONFIG.publicModelIdMode,
86
+ modelExposureMode: typeof rawExposureMode === "string" && isModelExposureMode(rawExposureMode) ? rawExposureMode : DEFAULT_CONFIG.modelExposureMode,
87
+ scopedProviders: normalizeStringArray(v["scopedProviders"]),
88
+ customModels: normalizeStringArray(v["customModels"]),
89
+ providerPrefixes: normalizeStringRecord(v["providerPrefixes"])
90
+ };
91
+ }
92
+ function getConfigPath() {
93
+ return resolve(process.env.PI_CODING_AGENT_DIR ?? resolve(process.env.HOME ?? "~", ".pi", "agent"), "proxy-config.json");
94
+ }
95
+ function loadConfigFromFile() {
96
+ const p = getConfigPath();
97
+ if (!existsSync(p)) return { ...DEFAULT_CONFIG };
98
+ try {
99
+ return normalizeConfig(JSON.parse(readFileSync(p, "utf-8")));
100
+ } catch {
101
+ return { ...DEFAULT_CONFIG };
102
+ }
103
+ }
104
+ function saveConfigToFile(config) {
105
+ const p = getConfigPath();
106
+ const normalized = normalizeConfig(config);
107
+ const tmp = `${p}.tmp`;
108
+ try {
109
+ mkdirSync(dirname(p), { recursive: true });
110
+ writeFileSync(tmp, `${JSON.stringify(normalized, null, " ")}\n`, "utf-8");
111
+ renameSync(tmp, p);
112
+ } catch {
113
+ if (existsSync(tmp)) unlinkSync(tmp);
114
+ }
115
+ }
116
+ function configToEnv(config) {
117
+ const env = {};
118
+ env["PI_PROXY_HOST"] = config.host;
119
+ env["PI_PROXY_PORT"] = String(config.port);
120
+ if (config.authToken.length > 0) env["PI_PROXY_AUTH_TOKEN"] = config.authToken;
121
+ env["PI_PROXY_REMOTE_IMAGES"] = String(config.remoteImages);
122
+ env["PI_PROXY_MAX_BODY_SIZE"] = String(config.maxBodySizeMb * 1024 * 1024);
123
+ env["PI_PROXY_UPSTREAM_TIMEOUT_MS"] = String(config.upstreamTimeoutSec * 1e3);
124
+ return env;
125
+ }
126
+ //#endregion
127
+ export { isPublicModelIdMode as a, saveConfigToFile as c, isModelExposureMode as i, isRecord as l, configToEnv as n, loadConfigFromFile as o, getConfigPath as r, normalizeConfig as s, DEFAULT_CONFIG as t };
@@ -7,6 +7,7 @@
7
7
  * /proxy stop Stop the proxy server
8
8
  * /proxy status Show proxy status
9
9
  * /proxy verify Validate model exposure config against available models
10
+ * /proxy models List all exposed models with their public IDs
10
11
  * /proxy config Open settings panel (alias)
11
12
  * /proxy show Summarize current config and exposure policy
12
13
  * /proxy path Show config file location
@@ -75,18 +76,19 @@ export default function proxyExtension(pi: ExtensionAPI): void {
75
76
  const extensionDir = dirname(fileURLToPath(import.meta.url));
76
77
  const packageRoot = resolve(extensionDir, "..");
77
78
 
78
- // --- Model registry access (for verify/show/selectors) ---
79
+ // --- Model registry access (cached, refreshed per call) ---
80
+
81
+ const cachedAuth = AuthStorage.create();
82
+ const cachedRegistry = new ModelRegistry(cachedAuth);
79
83
 
80
84
  function getAvailableModels(): Model<Api>[] {
81
- const auth = AuthStorage.create();
82
- const registry = new ModelRegistry(auth);
83
- return registry.getAvailable();
85
+ cachedRegistry.refresh();
86
+ return cachedRegistry.getAvailable();
84
87
  }
85
88
 
86
89
  function getAllRegisteredModels(): Model<Api>[] {
87
- const auth = AuthStorage.create();
88
- const registry = new ModelRegistry(auth);
89
- return registry.getAll();
90
+ cachedRegistry.refresh();
91
+ return cachedRegistry.getAll();
90
92
  }
91
93
 
92
94
  function buildExposureConfig(): ModelExposureConfig {
@@ -137,13 +139,14 @@ export default function proxyExtension(pi: ExtensionAPI): void {
137
139
  "restart",
138
140
  "status",
139
141
  "verify",
142
+ "models",
140
143
  "config",
141
144
  "show",
142
145
  "path",
143
146
  "reset",
144
147
  "help",
145
148
  ];
146
- const USAGE = "/proxy [start|stop|restart|status|verify|config|show|path|reset|help]";
149
+ const USAGE = "/proxy [start|stop|restart|status|verify|models|config|show|path|reset|help]";
147
150
 
148
151
  pi.registerCommand("proxy", {
149
152
  description: "Manage the OpenAI-compatible proxy",
@@ -173,6 +176,9 @@ export default function proxyExtension(pi: ExtensionAPI): void {
173
176
  case "verify":
174
177
  verifyExposure(ctx);
175
178
  return;
179
+ case "models":
180
+ showModels(ctx);
181
+ return;
176
182
  case "show":
177
183
  showConfig(ctx);
178
184
  return;
@@ -251,8 +257,13 @@ export default function proxyExtension(pi: ExtensionAPI): void {
251
257
 
252
258
  async function probe(): Promise<RuntimeStatus> {
253
259
  try {
260
+ const headers: Record<string, string> = {};
261
+ if (config.authToken.length > 0) {
262
+ headers["authorization"] = `Bearer ${config.authToken}`;
263
+ }
254
264
  const res = await fetch(`${proxyUrl()}/v1/models`, {
255
265
  signal: AbortSignal.timeout(2000),
266
+ headers,
256
267
  });
257
268
  if (res.ok) {
258
269
  const body = (await res.json()) as { data?: unknown[] };
@@ -476,6 +487,52 @@ export default function proxyExtension(pi: ExtensionAPI): void {
476
487
  ctx.ui.notify(`${serverLines.join(" | ")}\n${exposureLines.join(" | ")}`, "info");
477
488
  }
478
489
 
490
+ // --- /proxy models ---
491
+
492
+ function showModels(ctx: ExtensionContext): void {
493
+ config = loadConfigFromFile();
494
+ const models = getAvailableModels();
495
+ const allModels = getAllRegisteredModels();
496
+ const outcome = computeModelExposure(models, allModels, buildExposureConfig());
497
+
498
+ if (!outcome.ok) {
499
+ ctx.ui.notify(`Model exposure error: ${outcome.message}`, "warning");
500
+ return;
501
+ }
502
+
503
+ if (outcome.models.length === 0) {
504
+ ctx.ui.notify("No models exposed. Check /proxy verify for details.", "info");
505
+ return;
506
+ }
507
+
508
+ // Group models by provider for readable output
509
+ const byProvider = new Map<string, { publicId: string; canonicalId: string }[]>();
510
+ for (const m of outcome.models) {
511
+ const list = byProvider.get(m.provider);
512
+ const entry = { publicId: m.publicId, canonicalId: m.canonicalId };
513
+ if (list !== undefined) {
514
+ list.push(entry);
515
+ } else {
516
+ byProvider.set(m.provider, [entry]);
517
+ }
518
+ }
519
+
520
+ const sections: string[] = [];
521
+ for (const [provider, entries] of byProvider) {
522
+ const lines = entries.map((e) => {
523
+ // Only show canonical ID when it differs from the public ID
524
+ if (e.publicId === e.canonicalId) {
525
+ return ` ${e.publicId}`;
526
+ }
527
+ return ` ${e.publicId} (${e.canonicalId})`;
528
+ });
529
+ sections.push(`${provider} (${String(entries.length)}):\n${lines.join("\n")}`);
530
+ }
531
+
532
+ const header = `${String(outcome.models.length)} exposed model(s)`;
533
+ ctx.ui.notify(`${header}\n\n${sections.join("\n\n")}`, "info");
534
+ }
535
+
479
536
  // --- /proxy verify ---
480
537
 
481
538
  function verifyExposure(ctx: ExtensionContext): void {
@@ -588,8 +645,10 @@ export default function proxyExtension(pi: ExtensionAPI): void {
588
645
  { enableSearch: true },
589
646
  );
590
647
 
591
- // SettingsList has no public selectedIndex setter.
592
- // Access internals via bracket notation for provider jumping.
648
+ // HACK: SettingsList has no public API for jumping to an index.
649
+ // Accesses private fields via bracket notation for provider jumping.
650
+ // Pinned to pi-tui behavior as of @mariozechner/pi-coding-agent ^0.62.0.
651
+ // Remove when SettingsList exposes a jumpTo/setSelectedIndex method.
593
652
  function jumpProvider(direction: "prev" | "next"): void {
594
653
  const sl = list as unknown as Record<string, unknown>;
595
654
  const idx = sl["selectedIndex"] as number;
@@ -786,16 +845,14 @@ export default function proxyExtension(pi: ExtensionAPI): void {
786
845
  }
787
846
  }
788
847
 
789
- const VALID_ID_MODES = new Set<string>(["collision-prefixed", "universal", "always-prefixed"]);
790
-
848
+ // Local type guards the extension resolves against the built dist, so it
849
+ // cannot import these from the source config module during development.
791
850
  function isPublicModelIdMode(v: string): v is PublicModelIdMode {
792
- return VALID_ID_MODES.has(v);
851
+ return v === "collision-prefixed" || v === "universal" || v === "always-prefixed";
793
852
  }
794
853
 
795
- const VALID_EXPOSURE_MODES = new Set<string>(["all", "scoped", "custom"]);
796
-
797
854
  function isModelExposureMode(v: string): v is ModelExposureMode {
798
- return VALID_EXPOSURE_MODES.has(v);
855
+ return v === "all" || v === "scoped" || v === "custom";
799
856
  }
800
857
 
801
858
  function applySetting(id: string, value: string): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.2.6",
3
+ "version": "4.3.1",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",
@@ -63,6 +63,7 @@
63
63
  "format": "biome format --write .",
64
64
  "test": "bun test",
65
65
  "test:ci": "bun test test/unit/ test/integration/",
66
+ "prepare": "tsdown",
66
67
  "prepack": "bun run build",
67
68
  "prepublishOnly": "bun test && bun run build"
68
69
  },