@victor-software-house/pi-openai-proxy 4.5.0 → 4.6.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/config.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as ZedSyncConfig, c as isModelExposureMode, d as normalizeConfig, f as saveConfigToFile, i as PublicModelIdMode, l as isPublicModelIdMode, n as ModelExposureMode, o as configToEnv, r as ProxyConfig, s as getConfigPath, t as DEFAULT_CONFIG, u as loadConfigFromFile } from "./schema-B2x0rLJN.mjs";
2
+ import { a as ZedSyncConfig, c as isModelExposureMode, d as normalizeConfig, f as saveConfigToFile, i as PublicModelIdMode, l as isPublicModelIdMode, n as ModelExposureMode, o as configToEnv, r as ProxyConfig, s as getConfigPath, t as DEFAULT_CONFIG, u as loadConfigFromFile } from "./schema-CrOJW-mE.mjs";
3
3
  export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, ZedSyncConfig, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
package/dist/config.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
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-IE4WK0Dj.mjs";
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-quaXHZsz.mjs";
3
3
  export { DEFAULT_CONFIG, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
@@ -1,5 +1,5 @@
1
1
 
2
- import { i as PublicModelIdMode, n as ModelExposureMode } from "./schema-B2x0rLJN.mjs";
2
+ import { i as PublicModelIdMode, n as ModelExposureMode } from "./schema-CrOJW-mE.mjs";
3
3
  import { Api, Model } from "@mariozechner/pi-ai";
4
4
 
5
5
  //#region src/openai/model-exposure.d.ts
@@ -16,7 +16,13 @@ interface ExposedModel {
16
16
  interface ModelExposureConfig {
17
17
  readonly publicModelIdMode: PublicModelIdMode;
18
18
  readonly modelExposureMode: ModelExposureMode;
19
- readonly scopedProviders: readonly string[];
19
+ /**
20
+ * Canonical model IDs from pi's global `enabledModels` setting.
21
+ * Read from `SettingsManager.getEnabledModels()` at request time.
22
+ * Undefined or empty means no filter (all available models exposed).
23
+ * Used only when modelExposureMode is "scoped".
24
+ */
25
+ readonly enabledModels: readonly string[] | undefined;
20
26
  readonly customModels: readonly string[];
21
27
  readonly providerPrefixes: Readonly<Record<string, string>>;
22
28
  }
@@ -38,12 +44,11 @@ type ModelExposureOutcome = ModelExposureResult | ModelExposureError;
38
44
  * Compute the full model-exposure result from config and available models.
39
45
  *
40
46
  * @param available - Models with auth configured (pi's getAvailable())
41
- * @param allRegistered - All registered models regardless of auth (pi's getAll())
42
47
  * @param config - Model exposure configuration
43
48
  *
44
49
  * Call this at startup and whenever config or the model registry changes.
45
50
  */
46
- declare function computeModelExposure(available: readonly Model<Api>[], allRegistered: readonly Model<Api>[], config: ModelExposureConfig): ModelExposureOutcome;
51
+ declare function computeModelExposure(available: readonly Model<Api>[], config: ModelExposureConfig): ModelExposureOutcome;
47
52
  /**
48
53
  * Resolve a model ID from an incoming request against the exposure result.
49
54
  *
package/dist/exposure.mjs CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env bun
2
2
  //#region src/openai/model-exposure.ts
3
- function filterExposedModels(available, allRegistered, config) {
3
+ function filterExposedModels(available, config) {
4
4
  switch (config.modelExposureMode) {
5
+ case "all": return [...available];
5
6
  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));
7
+ const enabled = config.enabledModels;
8
+ if (enabled === void 0 || enabled.length === 0) return [...available];
9
+ const allowed = new Set(enabled);
10
+ return available.filter((m) => allowed.has(`${m.provider}/${m.id}`));
9
11
  }
10
- case "all": return [...allRegistered];
11
12
  case "custom": {
13
+ if (config.customModels.length === 0) return [...available];
12
14
  const allowed = new Set(config.customModels);
13
15
  return available.filter((m) => allowed.has(`${m.provider}/${m.id}`));
14
16
  }
@@ -127,13 +129,12 @@ function validatePrefixUniqueness(models, prefixes, mode) {
127
129
  * Compute the full model-exposure result from config and available models.
128
130
  *
129
131
  * @param available - Models with auth configured (pi's getAvailable())
130
- * @param allRegistered - All registered models regardless of auth (pi's getAll())
131
132
  * @param config - Model exposure configuration
132
133
  *
133
134
  * Call this at startup and whenever config or the model registry changes.
134
135
  */
135
- function computeModelExposure(available, allRegistered, config) {
136
- const exposed = filterExposedModels(available, allRegistered, config);
136
+ function computeModelExposure(available, config) {
137
+ const exposed = filterExposedModels(available, config);
137
138
  const prefixError = validatePrefixUniqueness(exposed, config.providerPrefixes, config.publicModelIdMode);
138
139
  if (prefixError !== void 0) return {
139
140
  ok: false,
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- import { l as isRecord, o as loadConfigFromFile, r as getConfigPath } from "./schema-IE4WK0Dj.mjs";
2
+ import { l as isRecord, o as loadConfigFromFile, r as getConfigPath } from "./schema-quaXHZsz.mjs";
3
3
  import { computeModelExposure, resolveExposedModel } from "./exposure.mjs";
4
4
  import * as z from "zod";
5
- import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
5
+ import { AuthStorage, ModelRegistry, SettingsManager } from "@mariozechner/pi-coding-agent";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { Type } from "@sinclair/typebox";
8
8
  import { completeSimple, streamSimple } from "@mariozechner/pi-ai";
@@ -38,7 +38,6 @@ function loadConfig(cli = {}) {
38
38
  upstreamTimeoutMs,
39
39
  publicModelIdMode: file.publicModelIdMode,
40
40
  modelExposureMode: file.modelExposureMode,
41
- scopedProviders: file.scopedProviders,
42
41
  customModels: file.customModels,
43
42
  providerPrefixes: file.providerPrefixes
44
43
  };
@@ -47,19 +46,25 @@ function loadConfig(cli = {}) {
47
46
  //#region src/pi/registry.ts
48
47
  let registry;
49
48
  let authStorage;
49
+ let settingsManager;
50
50
  /**
51
- * Initialize the registry. Call once at startup.
51
+ * Initialize the registry and settings. Call once at startup.
52
52
  * Returns the load error if models.json failed to parse, or undefined on success.
53
53
  */
54
54
  function initRegistry() {
55
55
  authStorage = AuthStorage.create();
56
56
  registry = new ModelRegistry(authStorage);
57
+ settingsManager = SettingsManager.create();
57
58
  return registry.getError();
58
59
  }
59
60
  function getRegistry() {
60
61
  if (registry === void 0) throw new Error("ModelRegistry not initialized. Call initRegistry() first.");
61
62
  return registry;
62
63
  }
64
+ function getSettingsManager() {
65
+ if (settingsManager === void 0) throw new Error("SettingsManager not initialized. Call initRegistry() first.");
66
+ return settingsManager;
67
+ }
63
68
  /**
64
69
  * Get all models available (have auth configured).
65
70
  */
@@ -67,10 +72,16 @@ function getAvailableModels() {
67
72
  return getRegistry().getAvailable();
68
73
  }
69
74
  /**
70
- * Get all registered models (regardless of auth state).
75
+ * Get the `enabledModels` patterns from pi's global settings.
76
+ *
77
+ * These are the canonical model IDs (e.g. "anthropic/claude-sonnet-4-6")
78
+ * persisted by the `/scoped-models` TUI when the user presses Ctrl+S.
79
+ *
80
+ * Returns undefined when no filter is configured (all models enabled).
71
81
  */
72
- function getAllModels() {
73
- return getRegistry().getAll();
82
+ function getEnabledModels() {
83
+ getSettingsManager().reload();
84
+ return getSettingsManager().getEnabledModels();
74
85
  }
75
86
  //#endregion
76
87
  //#region src/server/errors.ts
@@ -1307,14 +1318,14 @@ function fileConfigReader() {
1307
1318
  return {
1308
1319
  publicModelIdMode: file.publicModelIdMode,
1309
1320
  modelExposureMode: file.modelExposureMode,
1310
- scopedProviders: file.scopedProviders,
1321
+ enabledModels: getEnabledModels(),
1311
1322
  customModels: file.customModels,
1312
1323
  providerPrefixes: file.providerPrefixes
1313
1324
  };
1314
1325
  }
1315
1326
  function createRoutes(config, configReader = fileConfigReader) {
1316
1327
  function getExposure() {
1317
- const outcome = computeModelExposure(getAvailableModels(), getAllModels(), configReader());
1328
+ const outcome = computeModelExposure(getAvailableModels(), configReader());
1318
1329
  if (!outcome.ok) throw new Error(`Model exposure configuration error: ${outcome.message}`);
1319
1330
  return outcome;
1320
1331
  }
@@ -12,10 +12,12 @@ import * as z from "zod";
12
12
  type PublicModelIdMode = "collision-prefixed" | "universal" | "always-prefixed";
13
13
  /**
14
14
  * Which models are exposed on the public HTTP API.
15
+ * All modes only expose auth-configured models (pi's getAvailable()).
15
16
  *
16
- * - "all": every available model
17
- * - "scoped": all available models from selected providers only
18
- * - "custom": explicit allowlist of canonical model IDs
17
+ * - "all": every available model, no filter
18
+ * - "scoped": delegates to pi's global `enabledModels` setting (from `/scoped-models` Ctrl+S)
19
+ * - "custom": same per-model filtering as "scoped" but independently managed
20
+ * via the proxy's own `customModels` config
19
21
  */
20
22
  type ModelExposureMode = "all" | "scoped" | "custom";
21
23
  interface ProxyConfig {
@@ -35,10 +37,8 @@ interface ProxyConfig {
35
37
  readonly lifetime: "detached" | "session";
36
38
  /** How public model IDs are generated. Default: "collision-prefixed" */
37
39
  readonly publicModelIdMode: PublicModelIdMode;
38
- /** Which models are exposed. Default: "all" */
40
+ /** Which models are exposed. Default: "scoped" */
39
41
  readonly modelExposureMode: ModelExposureMode;
40
- /** Provider keys to expose when modelExposureMode is "scoped". */
41
- readonly scopedProviders: readonly string[];
42
42
  /** Canonical model IDs to expose when modelExposureMode is "custom". */
43
43
  readonly customModels: readonly string[];
44
44
  /** Provider key -> custom public prefix label. Default prefix = provider key. */
@@ -36,7 +36,6 @@ const DEFAULT_CONFIG = {
36
36
  lifetime: "detached",
37
37
  publicModelIdMode: "collision-prefixed",
38
38
  modelExposureMode: "scoped",
39
- scopedProviders: [],
40
39
  customModels: [],
41
40
  providerPrefixes: {},
42
41
  zed: {
@@ -93,7 +92,6 @@ function normalizeConfig(raw) {
93
92
  lifetime: v["lifetime"] === "session" ? "session" : "detached",
94
93
  publicModelIdMode: typeof rawPublicIdMode === "string" && isPublicModelIdMode(rawPublicIdMode) ? rawPublicIdMode : DEFAULT_CONFIG.publicModelIdMode,
95
94
  modelExposureMode: typeof rawExposureMode === "string" && isModelExposureMode(rawExposureMode) ? rawExposureMode : DEFAULT_CONFIG.modelExposureMode,
96
- scopedProviders: normalizeStringArray(v["scopedProviders"]),
97
95
  customModels: normalizeStringArray(v["customModels"]),
98
96
  providerPrefixes: normalizeStringRecord(v["providerPrefixes"]),
99
97
  zed: parseZedSyncConfig(v["zed"])
@@ -31,6 +31,7 @@ import {
31
31
  type ExtensionContext,
32
32
  getSettingsListTheme,
33
33
  ModelRegistry,
34
+ SettingsManager,
34
35
  } from "@mariozechner/pi-coding-agent";
35
36
  import {
36
37
  type Component,
@@ -68,6 +69,17 @@ interface RuntimeStatus {
68
69
  models: number;
69
70
  }
70
71
 
72
+ interface ProbeBody {
73
+ data: unknown[];
74
+ }
75
+
76
+ function isProbeBody(value: unknown): value is ProbeBody {
77
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
78
+ if (!("data" in value)) return false;
79
+ const v: { data: unknown } = value;
80
+ return Array.isArray(v.data);
81
+ }
82
+
71
83
  // ---------------------------------------------------------------------------
72
84
  // Extension
73
85
  // ---------------------------------------------------------------------------
@@ -83,22 +95,23 @@ export default function proxyExtension(pi: ExtensionAPI): void {
83
95
 
84
96
  const cachedAuth = AuthStorage.create();
85
97
  const cachedRegistry = new ModelRegistry(cachedAuth);
98
+ const settingsManager = SettingsManager.create();
86
99
 
87
100
  function getAvailableModels(): Model<Api>[] {
88
101
  cachedRegistry.refresh();
89
102
  return cachedRegistry.getAvailable();
90
103
  }
91
104
 
92
- function getAllRegisteredModels(): Model<Api>[] {
93
- cachedRegistry.refresh();
94
- return cachedRegistry.getAll();
105
+ function getEnabledModels(): readonly string[] | undefined {
106
+ settingsManager.reload();
107
+ return settingsManager.getEnabledModels();
95
108
  }
96
109
 
97
110
  function buildExposureConfig(): ModelExposureConfig {
98
111
  return {
99
112
  publicModelIdMode: config.publicModelIdMode,
100
113
  modelExposureMode: config.modelExposureMode,
101
- scopedProviders: config.scopedProviders,
114
+ enabledModels: getEnabledModels(),
102
115
  customModels: config.customModels,
103
116
  providerPrefixes: config.providerPrefixes,
104
117
  };
@@ -124,6 +137,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
124
137
 
125
138
  pi.on("session_start", async (_event, ctx) => {
126
139
  config = loadConfigFromFile();
140
+ maybeAutoSyncZed(ctx);
127
141
  await refreshStatus(ctx);
128
142
  });
129
143
 
@@ -274,8 +288,12 @@ export default function proxyExtension(pi: ExtensionAPI): void {
274
288
  headers,
275
289
  });
276
290
  if (res.ok) {
277
- const body = (await res.json()) as { data?: unknown[] };
278
- return { reachable: true, models: body.data?.length ?? 0 };
291
+ const body: unknown = await res.json();
292
+ let modelCount = 0;
293
+ if (isProbeBody(body)) {
294
+ modelCount = body.data.length;
295
+ }
296
+ return { reachable: true, models: modelCount };
279
297
  }
280
298
  } catch {
281
299
  // not reachable
@@ -464,8 +482,11 @@ export default function proxyExtension(pi: ExtensionAPI): void {
464
482
  `exposure: ${config.modelExposureMode}`,
465
483
  ];
466
484
 
467
- if (config.modelExposureMode === "scoped" && config.scopedProviders.length > 0) {
468
- exposureLines.push(`providers: ${config.scopedProviders.join(", ")}`);
485
+ if (config.modelExposureMode === "scoped") {
486
+ const enabledModels = getEnabledModels();
487
+ if (enabledModels !== undefined && enabledModels.length > 0) {
488
+ exposureLines.push(`enabled: ${String(enabledModels.length)} pi model(s)`);
489
+ }
469
490
  }
470
491
  if (config.modelExposureMode === "custom" && config.customModels.length > 0) {
471
492
  exposureLines.push(`models: ${String(config.customModels.length)} custom`);
@@ -479,8 +500,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
479
500
 
480
501
  // Public ID preview (first 5 exposed models)
481
502
  const models = getAvailableModels();
482
- const allModels = getAllRegisteredModels();
483
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
503
+ const outcome = computeModelExposure(models, buildExposureConfig());
484
504
  if (outcome.ok && outcome.models.length > 0) {
485
505
  const preview = outcome.models.slice(0, 5).map((m) => m.publicId);
486
506
  const suffix =
@@ -500,8 +520,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
500
520
  function showModels(ctx: ExtensionContext): void {
501
521
  config = loadConfigFromFile();
502
522
  const models = getAvailableModels();
503
- const allModels = getAllRegisteredModels();
504
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
523
+ const outcome = computeModelExposure(models, buildExposureConfig());
505
524
 
506
525
  if (!outcome.ok) {
507
526
  ctx.ui.notify(`Model exposure error: ${outcome.message}`, "warning");
@@ -548,8 +567,6 @@ export default function proxyExtension(pi: ExtensionAPI): void {
548
567
  const models = getAvailableModels();
549
568
  const issues: string[] = [];
550
569
 
551
- const allModels = getAllRegisteredModels();
552
-
553
570
  // Check available models
554
571
  if (models.length === 0) {
555
572
  issues.push("No models have auth configured. The proxy will expose 0 models.");
@@ -569,7 +586,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
569
586
  }
570
587
 
571
588
  // Run the full exposure computation to catch ID/prefix errors
572
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
589
+ const outcome = computeModelExposure(models, buildExposureConfig());
573
590
  if (!outcome.ok) {
574
591
  issues.push(outcome.message);
575
592
  }
@@ -592,8 +609,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
592
609
  */
593
610
  function runZedSync(dryRun: boolean): { ok: boolean; message: string } {
594
611
  const available = getAvailableModels();
595
- const allModels = getAllRegisteredModels();
596
- const outcome = computeModelExposure(available, allModels, buildExposureConfig());
612
+ const outcome = computeModelExposure(available, buildExposureConfig());
597
613
  if (!outcome.ok) {
598
614
  return { ok: false, message: `Model exposure error: ${outcome.message}` };
599
615
  }
@@ -709,12 +725,40 @@ export default function proxyExtension(pi: ExtensionAPI): void {
709
725
  // Accesses private fields via bracket notation for provider jumping.
710
726
  // Pinned to pi-tui behavior as of @mariozechner/pi-coding-agent ^0.62.0.
711
727
  // Remove when SettingsList exposes a jumpTo/setSelectedIndex method.
728
+
729
+ // Isolated unsafe accessor for SettingsList private fields.
730
+ // Consolidated here so jumpProvider itself is fully type-safe.
731
+ function listGet(key: string): unknown {
732
+ return Reflect.get(list, key);
733
+ }
734
+ function listSet(key: string, value: unknown): void {
735
+ Reflect.set(list, key, value);
736
+ }
737
+
738
+ function getSettingsListItems(): SettingItem[] {
739
+ const rawSearch = listGet("searchEnabled");
740
+ const raw =
741
+ typeof rawSearch === "boolean" && rawSearch ? listGet("filteredItems") : listGet("items");
742
+ if (!Array.isArray(raw)) return [];
743
+ const result: SettingItem[] = [];
744
+ for (const item of raw) {
745
+ if (isSettingItem(item)) result.push(item);
746
+ }
747
+ return result;
748
+ }
749
+
750
+ function isSettingItem(value: unknown): value is SettingItem {
751
+ if (value === null || typeof value !== "object") return false;
752
+ if (!("id" in value)) return false;
753
+ const v: { id: unknown } = value;
754
+ return typeof v.id === "string";
755
+ }
756
+
712
757
  function jumpProvider(direction: "prev" | "next"): void {
713
- const sl = list as unknown as Record<string, unknown>;
714
- const idx = sl["selectedIndex"] as number;
715
- const display = (
716
- (sl["searchEnabled"] as boolean) ? sl["filteredItems"] : sl["items"]
717
- ) as SettingItem[];
758
+ const rawIdx = listGet("selectedIndex");
759
+ if (typeof rawIdx !== "number") return;
760
+ const idx = rawIdx;
761
+ const display = getSettingsListItems();
718
762
  if (display.length === 0) return;
719
763
 
720
764
  const current = display[idx];
@@ -750,7 +794,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
750
794
  }
751
795
  }
752
796
  }
753
- sl["selectedIndex"] = target;
797
+ listSet("selectedIndex", target);
754
798
  }
755
799
 
756
800
  return {
@@ -878,7 +922,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
878
922
  label: "Select models",
879
923
  description: customModelsDescription(),
880
924
  currentValue: customModelsDisplay(),
881
- submenu: config.modelExposureMode === "custom" ? buildModelSelectorSubmenu : undefined,
925
+ ...(config.modelExposureMode === "custom" ? { submenu: buildModelSelectorSubmenu } : {}),
882
926
  },
883
927
  // --- Zed sync ---
884
928
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",
@@ -62,7 +62,7 @@
62
62
  "typecheck": "tsc --noEmit",
63
63
  "lint": "bun run lint:biome && bun run lint:oxlint",
64
64
  "lint:biome": "biome check .",
65
- "lint:oxlint": "oxlint --import-plugin --type-aware --tsconfig=./tsconfig.json src/",
65
+ "lint:oxlint": "oxlint --import-plugin --type-aware --tsconfig=./tsconfig.json .",
66
66
  "lint:fix": "biome check --write .",
67
67
  "format": "biome format --write .",
68
68
  "test": "bun test",