@victor-software-house/pi-openai-proxy 4.4.1 → 4.5.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
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as configToEnv, c as isPublicModelIdMode, d as saveConfigToFile, i as PublicModelIdMode, l as loadConfigFromFile, n as ModelExposureMode, o as getConfigPath, r as ProxyConfig, s as isModelExposureMode, t as DEFAULT_CONFIG, u as normalizeConfig } from "./schema-BTAG6Urs.mjs";
3
- export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
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
+ 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-x6mps-hM.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-BTAG6Urs.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,9 +1,9 @@
1
1
  #!/usr/bin/env bun
2
- import { l as isRecord, o as loadConfigFromFile, r as getConfigPath } from "./schema-x6mps-hM.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
- import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
5
- import { randomBytes } from "node:crypto";
6
4
  import * as z from "zod";
5
+ import { AuthStorage, ModelRegistry, SettingsManager } from "@mariozechner/pi-coding-agent";
6
+ import { randomBytes } from "node:crypto";
7
7
  import { Type } from "@sinclair/typebox";
8
8
  import { completeSimple, streamSimple } from "@mariozechner/pi-ai";
9
9
  import { Hono } from "hono";
@@ -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
  }
@@ -1,12 +1,7 @@
1
1
 
2
+ import * as z from "zod";
3
+
2
4
  //#region src/config/schema.d.ts
3
- /**
4
- * Proxy configuration schema -- single source of truth.
5
- *
6
- * Used by both the proxy server and the pi extension.
7
- * The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
8
- * The pi extension reads and writes the JSON config file via the /proxy config panel.
9
- */
10
5
  /**
11
6
  * How public model IDs are generated from canonical provider/model-id pairs.
12
7
  *
@@ -17,10 +12,12 @@
17
12
  type PublicModelIdMode = "collision-prefixed" | "universal" | "always-prefixed";
18
13
  /**
19
14
  * Which models are exposed on the public HTTP API.
15
+ * All modes only expose auth-configured models (pi's getAvailable()).
20
16
  *
21
- * - "all": every available model
22
- * - "scoped": all available models from selected providers only
23
- * - "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
24
21
  */
25
22
  type ModelExposureMode = "all" | "scoped" | "custom";
26
23
  interface ProxyConfig {
@@ -40,15 +37,20 @@ interface ProxyConfig {
40
37
  readonly lifetime: "detached" | "session";
41
38
  /** How public model IDs are generated. Default: "collision-prefixed" */
42
39
  readonly publicModelIdMode: PublicModelIdMode;
43
- /** Which models are exposed. Default: "all" */
40
+ /** Which models are exposed. Default: "scoped" */
44
41
  readonly modelExposureMode: ModelExposureMode;
45
- /** Provider keys to expose when modelExposureMode is "scoped". */
46
- readonly scopedProviders: readonly string[];
47
42
  /** Canonical model IDs to expose when modelExposureMode is "custom". */
48
43
  readonly customModels: readonly string[];
49
44
  /** Provider key -> custom public prefix label. Default prefix = provider key. */
50
45
  readonly providerPrefixes: Readonly<Record<string, string>>;
46
+ /** Zed editor sync settings. */
47
+ readonly zed: ZedSyncConfig;
51
48
  }
49
+ declare const ZedSyncConfigSchema: z.ZodObject<{
50
+ providerName: z.ZodDefault<z.ZodString>;
51
+ autoSync: z.ZodDefault<z.ZodBoolean>;
52
+ }, z.core.$strip>;
53
+ type ZedSyncConfig = z.infer<typeof ZedSyncConfigSchema>;
52
54
  declare const DEFAULT_CONFIG: Readonly<ProxyConfig>;
53
55
  declare function isPublicModelIdMode(value: string): value is PublicModelIdMode;
54
56
  declare function isModelExposureMode(value: string): value is ModelExposureMode;
@@ -58,4 +60,4 @@ declare function loadConfigFromFile(): ProxyConfig;
58
60
  declare function saveConfigToFile(config: ProxyConfig): void;
59
61
  declare function configToEnv(config: ProxyConfig): Record<string, string>;
60
62
  //#endregion
61
- export { configToEnv as a, isPublicModelIdMode as c, saveConfigToFile as d, PublicModelIdMode as i, loadConfigFromFile as l, ModelExposureMode as n, getConfigPath as o, ProxyConfig as r, isModelExposureMode as s, DEFAULT_CONFIG as t, normalizeConfig as u };
63
+ export { ZedSyncConfig as a, isModelExposureMode as c, normalizeConfig as d, saveConfigToFile as f, PublicModelIdMode as i, isPublicModelIdMode as l, ModelExposureMode as n, configToEnv as o, ProxyConfig as r, getConfigPath as s, DEFAULT_CONFIG as t, loadConfigFromFile as u };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
+ import * as z from "zod";
4
5
  //#region src/utils/guards.ts
5
6
  /**
6
7
  * Shared type guard utilities.
@@ -21,6 +22,10 @@ function isRecord(value) {
21
22
  * The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
22
23
  * The pi extension reads and writes the JSON config file via the /proxy config panel.
23
24
  */
25
+ const ZedSyncConfigSchema = z.object({
26
+ providerName: z.string().trim().min(1, { error: "Provider name must not be empty" }).default("Pi Proxy"),
27
+ autoSync: z.boolean({ error: "autoSync must be a boolean" }).default(false)
28
+ });
24
29
  const DEFAULT_CONFIG = {
25
30
  host: "127.0.0.1",
26
31
  port: 4141,
@@ -31,9 +36,12 @@ const DEFAULT_CONFIG = {
31
36
  lifetime: "detached",
32
37
  publicModelIdMode: "collision-prefixed",
33
38
  modelExposureMode: "scoped",
34
- scopedProviders: [],
35
39
  customModels: [],
36
- providerPrefixes: {}
40
+ providerPrefixes: {},
41
+ zed: {
42
+ providerName: "Pi Proxy",
43
+ autoSync: false
44
+ }
37
45
  };
38
46
  function clampInt(raw, min, max, fallback) {
39
47
  if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
@@ -84,11 +92,16 @@ function normalizeConfig(raw) {
84
92
  lifetime: v["lifetime"] === "session" ? "session" : "detached",
85
93
  publicModelIdMode: typeof rawPublicIdMode === "string" && isPublicModelIdMode(rawPublicIdMode) ? rawPublicIdMode : DEFAULT_CONFIG.publicModelIdMode,
86
94
  modelExposureMode: typeof rawExposureMode === "string" && isModelExposureMode(rawExposureMode) ? rawExposureMode : DEFAULT_CONFIG.modelExposureMode,
87
- scopedProviders: normalizeStringArray(v["scopedProviders"]),
88
95
  customModels: normalizeStringArray(v["customModels"]),
89
- providerPrefixes: normalizeStringRecord(v["providerPrefixes"])
96
+ providerPrefixes: normalizeStringRecord(v["providerPrefixes"]),
97
+ zed: parseZedSyncConfig(v["zed"])
90
98
  };
91
99
  }
100
+ function parseZedSyncConfig(raw) {
101
+ const result = ZedSyncConfigSchema.safeParse(isRecord(raw) ? raw : {});
102
+ if (result.success) return result.data;
103
+ return ZedSyncConfigSchema.parse({});
104
+ }
92
105
  function getConfigPath() {
93
106
  return resolve(process.env.PI_CODING_AGENT_DIR ?? resolve(process.env.HOME ?? "~", ".pi", "agent"), "proxy-config.json");
94
107
  }
@@ -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
  };
@@ -274,8 +287,12 @@ export default function proxyExtension(pi: ExtensionAPI): void {
274
287
  headers,
275
288
  });
276
289
  if (res.ok) {
277
- const body = (await res.json()) as { data?: unknown[] };
278
- return { reachable: true, models: body.data?.length ?? 0 };
290
+ const body: unknown = await res.json();
291
+ let modelCount = 0;
292
+ if (isProbeBody(body)) {
293
+ modelCount = body.data.length;
294
+ }
295
+ return { reachable: true, models: modelCount };
279
296
  }
280
297
  } catch {
281
298
  // not reachable
@@ -464,8 +481,11 @@ export default function proxyExtension(pi: ExtensionAPI): void {
464
481
  `exposure: ${config.modelExposureMode}`,
465
482
  ];
466
483
 
467
- if (config.modelExposureMode === "scoped" && config.scopedProviders.length > 0) {
468
- exposureLines.push(`providers: ${config.scopedProviders.join(", ")}`);
484
+ if (config.modelExposureMode === "scoped") {
485
+ const enabledModels = getEnabledModels();
486
+ if (enabledModels !== undefined && enabledModels.length > 0) {
487
+ exposureLines.push(`enabled: ${String(enabledModels.length)} pi model(s)`);
488
+ }
469
489
  }
470
490
  if (config.modelExposureMode === "custom" && config.customModels.length > 0) {
471
491
  exposureLines.push(`models: ${String(config.customModels.length)} custom`);
@@ -479,8 +499,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
479
499
 
480
500
  // Public ID preview (first 5 exposed models)
481
501
  const models = getAvailableModels();
482
- const allModels = getAllRegisteredModels();
483
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
502
+ const outcome = computeModelExposure(models, buildExposureConfig());
484
503
  if (outcome.ok && outcome.models.length > 0) {
485
504
  const preview = outcome.models.slice(0, 5).map((m) => m.publicId);
486
505
  const suffix =
@@ -500,8 +519,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
500
519
  function showModels(ctx: ExtensionContext): void {
501
520
  config = loadConfigFromFile();
502
521
  const models = getAvailableModels();
503
- const allModels = getAllRegisteredModels();
504
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
522
+ const outcome = computeModelExposure(models, buildExposureConfig());
505
523
 
506
524
  if (!outcome.ok) {
507
525
  ctx.ui.notify(`Model exposure error: ${outcome.message}`, "warning");
@@ -548,8 +566,6 @@ export default function proxyExtension(pi: ExtensionAPI): void {
548
566
  const models = getAvailableModels();
549
567
  const issues: string[] = [];
550
568
 
551
- const allModels = getAllRegisteredModels();
552
-
553
569
  // Check available models
554
570
  if (models.length === 0) {
555
571
  issues.push("No models have auth configured. The proxy will expose 0 models.");
@@ -569,7 +585,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
569
585
  }
570
586
 
571
587
  // Run the full exposure computation to catch ID/prefix errors
572
- const outcome = computeModelExposure(models, allModels, buildExposureConfig());
588
+ const outcome = computeModelExposure(models, buildExposureConfig());
573
589
  if (!outcome.ok) {
574
590
  issues.push(outcome.message);
575
591
  }
@@ -587,27 +603,22 @@ export default function proxyExtension(pi: ExtensionAPI): void {
587
603
 
588
604
  // --- Zed sync ---
589
605
 
590
- function handleZedSync(ctx: ExtensionContext, args: string): void {
591
- config = loadConfigFromFile();
592
-
593
- const dryRun = args.includes("--dry-run");
594
-
595
- // Compute exposed models (same as /proxy models)
606
+ /**
607
+ * Run Zed sync and return the result. Shared by the command and auto-sync.
608
+ */
609
+ function runZedSync(dryRun: boolean): { ok: boolean; message: string } {
596
610
  const available = getAvailableModels();
597
- const allModels = getAllRegisteredModels();
598
- const outcome = computeModelExposure(available, allModels, buildExposureConfig());
611
+ const outcome = computeModelExposure(available, buildExposureConfig());
599
612
  if (!outcome.ok) {
600
- ctx.ui.notify(`Model exposure error: ${outcome.message}`, "error");
601
- return;
613
+ return { ok: false, message: `Model exposure error: ${outcome.message}` };
602
614
  }
603
615
 
604
616
  if (outcome.models.length === 0) {
605
- ctx.ui.notify("No models exposed. Nothing to sync.", "warning");
606
- return;
617
+ return { ok: false, message: "No models exposed. Nothing to sync." };
607
618
  }
608
619
 
609
620
  const syncOptions: ZedSyncOptions = {
610
- providerName: "Pi Proxy",
621
+ providerName: config.zed.providerName,
611
622
  apiUrl: `http://${config.host}:${String(config.port)}/v1`,
612
623
  dryRun,
613
624
  };
@@ -615,14 +626,30 @@ export default function proxyExtension(pi: ExtensionAPI): void {
615
626
  const result = syncToZed(outcome.models, syncOptions);
616
627
 
617
628
  if (!result.ok) {
618
- ctx.ui.notify(result.error ?? "Zed sync failed", "error");
619
- return;
629
+ return { ok: false, message: result.error ?? "Zed sync failed" };
620
630
  }
621
631
 
622
- const msg = dryRun
623
- ? `Zed sync dry-run: ${result.summary}\n${result.configPath}`
624
- : `Zed sync complete: ${result.summary}\n${result.configPath}`;
625
- ctx.ui.notify(msg, "info");
632
+ const prefix = dryRun ? "[dry-run] " : "";
633
+ return { ok: true, message: `${prefix}${result.summary} (${result.configPath})` };
634
+ }
635
+
636
+ function handleZedSync(ctx: ExtensionContext, args: string): void {
637
+ config = loadConfigFromFile();
638
+ const dryRun = args.includes("--dry-run");
639
+ const result = runZedSync(dryRun);
640
+ ctx.ui.notify(`Zed sync: ${result.message}`, result.ok ? "info" : "error");
641
+ }
642
+
643
+ /**
644
+ * Trigger auto-sync to Zed if enabled. Called after config save.
645
+ */
646
+ function maybeAutoSyncZed(ctx: ExtensionContext): void {
647
+ if (!config.zed.autoSync) return;
648
+ const result = runZedSync(false);
649
+ if (result.ok) {
650
+ ctx.ui.notify(`Zed auto-sync: ${result.message}`, "info");
651
+ }
652
+ // Silent on failure during auto-sync -- don't spam the user
626
653
  }
627
654
 
628
655
  async function waitForReady(timeoutMs: number): Promise<RuntimeStatus> {
@@ -697,12 +724,40 @@ export default function proxyExtension(pi: ExtensionAPI): void {
697
724
  // Accesses private fields via bracket notation for provider jumping.
698
725
  // Pinned to pi-tui behavior as of @mariozechner/pi-coding-agent ^0.62.0.
699
726
  // Remove when SettingsList exposes a jumpTo/setSelectedIndex method.
727
+
728
+ // Isolated unsafe accessor for SettingsList private fields.
729
+ // Consolidated here so jumpProvider itself is fully type-safe.
730
+ function listGet(key: string): unknown {
731
+ return Reflect.get(list, key);
732
+ }
733
+ function listSet(key: string, value: unknown): void {
734
+ Reflect.set(list, key, value);
735
+ }
736
+
737
+ function getSettingsListItems(): SettingItem[] {
738
+ const rawSearch = listGet("searchEnabled");
739
+ const raw =
740
+ typeof rawSearch === "boolean" && rawSearch ? listGet("filteredItems") : listGet("items");
741
+ if (!Array.isArray(raw)) return [];
742
+ const result: SettingItem[] = [];
743
+ for (const item of raw) {
744
+ if (isSettingItem(item)) result.push(item);
745
+ }
746
+ return result;
747
+ }
748
+
749
+ function isSettingItem(value: unknown): value is SettingItem {
750
+ if (value === null || typeof value !== "object") return false;
751
+ if (!("id" in value)) return false;
752
+ const v: { id: unknown } = value;
753
+ return typeof v.id === "string";
754
+ }
755
+
700
756
  function jumpProvider(direction: "prev" | "next"): void {
701
- const sl = list as unknown as Record<string, unknown>;
702
- const idx = sl["selectedIndex"] as number;
703
- const display = (
704
- (sl["searchEnabled"] as boolean) ? sl["filteredItems"] : sl["items"]
705
- ) as SettingItem[];
757
+ const rawIdx = listGet("selectedIndex");
758
+ if (typeof rawIdx !== "number") return;
759
+ const idx = rawIdx;
760
+ const display = getSettingsListItems();
706
761
  if (display.length === 0) return;
707
762
 
708
763
  const current = display[idx];
@@ -738,7 +793,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
738
793
  }
739
794
  }
740
795
  }
741
- sl["selectedIndex"] = target;
796
+ listSet("selectedIndex", target);
742
797
  }
743
798
 
744
799
  return {
@@ -866,7 +921,22 @@ export default function proxyExtension(pi: ExtensionAPI): void {
866
921
  label: "Select models",
867
922
  description: customModelsDescription(),
868
923
  currentValue: customModelsDisplay(),
869
- submenu: config.modelExposureMode === "custom" ? buildModelSelectorSubmenu : undefined,
924
+ ...(config.modelExposureMode === "custom" ? { submenu: buildModelSelectorSubmenu } : {}),
925
+ },
926
+ // --- Zed sync ---
927
+ {
928
+ id: "zed.autoSync",
929
+ label: "Zed auto-sync",
930
+ description: "Sync models to Zed settings.json when config changes",
931
+ currentValue: config.zed.autoSync ? "on" : "off",
932
+ values: ["off", "on"],
933
+ },
934
+ {
935
+ id: "zed.providerName",
936
+ label: "Zed provider name",
937
+ description: "Provider label in Zed's openai_compatible section",
938
+ currentValue: config.zed.providerName,
939
+ values: ["Pi Proxy"],
870
940
  },
871
941
  ];
872
942
  }
@@ -958,6 +1028,14 @@ export default function proxyExtension(pi: ExtensionAPI): void {
958
1028
  case "customModels":
959
1029
  // Handled by submenu -- no cycling
960
1030
  break;
1031
+ case "zed.autoSync":
1032
+ config = { ...config, zed: { ...config.zed, autoSync: value === "on" } };
1033
+ break;
1034
+ case "zed.providerName":
1035
+ if (value.length > 0) {
1036
+ config = { ...config, zed: { ...config.zed, providerName: value } };
1037
+ }
1038
+ break;
961
1039
  }
962
1040
  saveConfigToFile(config);
963
1041
  config = loadConfigFromFile();
@@ -988,6 +1066,10 @@ export default function proxyExtension(pi: ExtensionAPI): void {
988
1066
  return config.modelExposureMode;
989
1067
  case "customModels":
990
1068
  return customModelsDisplay();
1069
+ case "zed.autoSync":
1070
+ return config.zed.autoSync ? "on" : "off";
1071
+ case "zed.providerName":
1072
+ return config.zed.providerName;
991
1073
  default:
992
1074
  return "";
993
1075
  }
@@ -1023,6 +1105,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
1023
1105
  settingsList.updateValue("customModels", customModelsDisplay());
1024
1106
  }
1025
1107
 
1108
+ maybeAutoSyncZed(ctx);
1026
1109
  tui.requestRender();
1027
1110
  },
1028
1111
  () => done(undefined),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.4.1",
3
+ "version": "4.5.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",
@@ -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",