@victor-software-house/pi-openai-proxy 4.3.1 → 4.4.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,61 +1,3 @@
1
1
 
2
- //#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
- /**
11
- * How public model IDs are generated from canonical provider/model-id pairs.
12
- *
13
- * - "collision-prefixed": prefix only providers in conflict groups that share a raw model ID
14
- * - "universal": expose raw model IDs only; duplicates are a config error
15
- * - "always-prefixed": always expose <prefix>/<model-id> for every model
16
- */
17
- type PublicModelIdMode = "collision-prefixed" | "universal" | "always-prefixed";
18
- /**
19
- * Which models are exposed on the public HTTP API.
20
- *
21
- * - "all": every available model
22
- * - "scoped": all available models from selected providers only
23
- * - "custom": explicit allowlist of canonical model IDs
24
- */
25
- type ModelExposureMode = "all" | "scoped" | "custom";
26
- interface ProxyConfig {
27
- /** Bind address. Default: "127.0.0.1" */
28
- readonly host: string;
29
- /** Listen port. Default: 4141 */
30
- readonly port: number;
31
- /** Bearer token for proxy auth. Empty string = disabled. */
32
- readonly authToken: string;
33
- /** Allow remote image URL fetching. Default: false */
34
- readonly remoteImages: boolean;
35
- /** Max request body in MB. Default: 50 */
36
- readonly maxBodySizeMb: number;
37
- /** Upstream timeout in seconds. Default: 120 */
38
- readonly upstreamTimeoutSec: number;
39
- /** "detached" = background daemon, "session" = dies with pi session. Default: "detached" */
40
- readonly lifetime: "detached" | "session";
41
- /** How public model IDs are generated. Default: "collision-prefixed" */
42
- readonly publicModelIdMode: PublicModelIdMode;
43
- /** Which models are exposed. Default: "all" */
44
- readonly modelExposureMode: ModelExposureMode;
45
- /** Provider keys to expose when modelExposureMode is "scoped". */
46
- readonly scopedProviders: readonly string[];
47
- /** Canonical model IDs to expose when modelExposureMode is "custom". */
48
- readonly customModels: readonly string[];
49
- /** Provider key -> custom public prefix label. Default prefix = provider key. */
50
- readonly providerPrefixes: Readonly<Record<string, string>>;
51
- }
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;
55
- declare function normalizeConfig(raw: unknown): ProxyConfig;
56
- declare function getConfigPath(): string;
57
- declare function loadConfigFromFile(): ProxyConfig;
58
- declare function saveConfigToFile(config: ProxyConfig): void;
59
- declare function configToEnv(config: ProxyConfig): Record<string, string>;
60
- //#endregion
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";
61
3
  export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, configToEnv, getConfigPath, isModelExposureMode, isPublicModelIdMode, loadConfigFromFile, normalizeConfig, saveConfigToFile };
@@ -1,5 +1,5 @@
1
1
 
2
- import { ModelExposureMode, PublicModelIdMode } from "./config.mjs";
2
+ import { i as PublicModelIdMode, n as ModelExposureMode } from "./schema-BTAG6Urs.mjs";
3
3
  import { Api, Model } from "@mariozechner/pi-ai";
4
4
 
5
5
  //#region src/openai/model-exposure.d.ts
@@ -0,0 +1,61 @@
1
+
2
+ //#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
+ /**
11
+ * How public model IDs are generated from canonical provider/model-id pairs.
12
+ *
13
+ * - "collision-prefixed": prefix only providers in conflict groups that share a raw model ID
14
+ * - "universal": expose raw model IDs only; duplicates are a config error
15
+ * - "always-prefixed": always expose <prefix>/<model-id> for every model
16
+ */
17
+ type PublicModelIdMode = "collision-prefixed" | "universal" | "always-prefixed";
18
+ /**
19
+ * Which models are exposed on the public HTTP API.
20
+ *
21
+ * - "all": every available model
22
+ * - "scoped": all available models from selected providers only
23
+ * - "custom": explicit allowlist of canonical model IDs
24
+ */
25
+ type ModelExposureMode = "all" | "scoped" | "custom";
26
+ interface ProxyConfig {
27
+ /** Bind address. Default: "127.0.0.1" */
28
+ readonly host: string;
29
+ /** Listen port. Default: 4141 */
30
+ readonly port: number;
31
+ /** Bearer token for proxy auth. Empty string = disabled. */
32
+ readonly authToken: string;
33
+ /** Allow remote image URL fetching. Default: false */
34
+ readonly remoteImages: boolean;
35
+ /** Max request body in MB. Default: 50 */
36
+ readonly maxBodySizeMb: number;
37
+ /** Upstream timeout in seconds. Default: 120 */
38
+ readonly upstreamTimeoutSec: number;
39
+ /** "detached" = background daemon, "session" = dies with pi session. Default: "detached" */
40
+ readonly lifetime: "detached" | "session";
41
+ /** How public model IDs are generated. Default: "collision-prefixed" */
42
+ readonly publicModelIdMode: PublicModelIdMode;
43
+ /** Which models are exposed. Default: "all" */
44
+ readonly modelExposureMode: ModelExposureMode;
45
+ /** Provider keys to expose when modelExposureMode is "scoped". */
46
+ readonly scopedProviders: readonly string[];
47
+ /** Canonical model IDs to expose when modelExposureMode is "custom". */
48
+ readonly customModels: readonly string[];
49
+ /** Provider key -> custom public prefix label. Default prefix = provider key. */
50
+ readonly providerPrefixes: Readonly<Record<string, string>>;
51
+ }
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;
55
+ declare function normalizeConfig(raw: unknown): ProxyConfig;
56
+ declare function getConfigPath(): string;
57
+ declare function loadConfigFromFile(): ProxyConfig;
58
+ declare function saveConfigToFile(config: ProxyConfig): void;
59
+ declare function configToEnv(config: ProxyConfig): Record<string, string>;
60
+ //#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 };
@@ -0,0 +1,53 @@
1
+
2
+ import { ExposedModel } from "./exposure.mjs";
3
+
4
+ //#region src/sync/zed.d.ts
5
+ interface ZedAvailableModel {
6
+ readonly name: string;
7
+ readonly display_name: string;
8
+ readonly max_tokens: number;
9
+ readonly max_output_tokens: number;
10
+ readonly capabilities: {
11
+ readonly tools: boolean;
12
+ readonly images: boolean;
13
+ readonly parallel_tool_calls: boolean;
14
+ readonly prompt_cache_key: boolean;
15
+ readonly chat_completions: boolean;
16
+ };
17
+ }
18
+ interface ZedSyncOptions {
19
+ /** Provider label in Zed settings (e.g. "Pi Proxy"). */
20
+ readonly providerName: string;
21
+ /** API URL written into the provider block. */
22
+ readonly apiUrl: string;
23
+ /** Preview changes without writing to disk. */
24
+ readonly dryRun: boolean;
25
+ }
26
+ interface ZedSyncResult {
27
+ readonly ok: boolean;
28
+ readonly configPath: string;
29
+ readonly added: number;
30
+ readonly removed: number;
31
+ readonly unchanged: number;
32
+ readonly summary: string;
33
+ readonly error?: string | undefined;
34
+ }
35
+ /**
36
+ * Resolve Zed's global settings.json path.
37
+ *
38
+ * Resolution order:
39
+ * 1. CUSTOM_DATA_DIR env var (Zed's own override)
40
+ * 2. ~/.config/zed/settings.json (macOS + Linux standard)
41
+ * 3. $XDG_CONFIG_HOME/zed/settings.json (Linux XDG)
42
+ */
43
+ declare function findZedSettings(): string | undefined;
44
+ /**
45
+ * Map an ExposedModel to Zed's available_models entry.
46
+ */
47
+ declare function toZedModel(exposed: ExposedModel): ZedAvailableModel;
48
+ /**
49
+ * Sync exposed models into Zed's settings.json.
50
+ */
51
+ declare function syncToZed(exposedModels: readonly ExposedModel[], options: ZedSyncOptions): ZedSyncResult;
52
+ //#endregion
53
+ export { ZedAvailableModel, ZedSyncOptions, ZedSyncResult, findZedSettings, syncToZed, toZedModel };
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { applyEdits, modify, parse } from "jsonc-parser";
5
+ //#region src/sync/zed.ts
6
+ /**
7
+ * Zed settings.json model sync.
8
+ *
9
+ * Discovers Zed's settings file, maps exposed proxy models to Zed's
10
+ * openai_compatible available_models shape, and writes them back using
11
+ * jsonc-parser's modify() to preserve comments and formatting.
12
+ */
13
+ /**
14
+ * Resolve Zed's global settings.json path.
15
+ *
16
+ * Resolution order:
17
+ * 1. CUSTOM_DATA_DIR env var (Zed's own override)
18
+ * 2. ~/.config/zed/settings.json (macOS + Linux standard)
19
+ * 3. $XDG_CONFIG_HOME/zed/settings.json (Linux XDG)
20
+ */
21
+ function findZedSettings() {
22
+ const customDir = process.env.CUSTOM_DATA_DIR;
23
+ if (customDir !== void 0 && customDir.length > 0) {
24
+ const p = resolve(customDir, "settings.json");
25
+ if (existsSync(p)) return p;
26
+ }
27
+ const home = process.env.HOME;
28
+ if (home !== void 0 && home.length > 0) {
29
+ const p = resolve(home, ".config", "zed", "settings.json");
30
+ if (existsSync(p)) return p;
31
+ }
32
+ const xdg = process.env.XDG_CONFIG_HOME;
33
+ if (xdg !== void 0 && xdg.length > 0) {
34
+ const p = resolve(xdg, "zed", "settings.json");
35
+ if (existsSync(p)) return p;
36
+ }
37
+ }
38
+ /**
39
+ * Map an ExposedModel to Zed's available_models entry.
40
+ */
41
+ function toZedModel(exposed) {
42
+ const model = exposed.model;
43
+ return {
44
+ name: exposed.publicId,
45
+ display_name: model.name,
46
+ max_tokens: model.contextWindow,
47
+ max_output_tokens: model.maxTokens,
48
+ capabilities: {
49
+ tools: true,
50
+ images: model.input.includes("image"),
51
+ parallel_tool_calls: false,
52
+ prompt_cache_key: false,
53
+ chat_completions: true
54
+ }
55
+ };
56
+ }
57
+ const MODIFY_OPTIONS = {
58
+ isArrayInsertion: false,
59
+ formattingOptions: {
60
+ tabSize: 2,
61
+ insertSpaces: true,
62
+ eol: "\n"
63
+ }
64
+ };
65
+ function isRecord(value) {
66
+ return value !== null && typeof value === "object" && !Array.isArray(value);
67
+ }
68
+ /**
69
+ * Read the current available_models for a provider from Zed settings.
70
+ * Returns the parsed model names, or an empty array if the path does not exist.
71
+ */
72
+ function readCurrentModelNames(text, providerName) {
73
+ const root = parse(text);
74
+ if (!isRecord(root)) return [];
75
+ const langModels = root["language_models"];
76
+ if (!isRecord(langModels)) return [];
77
+ const compat = langModels["openai_compatible"];
78
+ if (!isRecord(compat)) return [];
79
+ const provider = compat[providerName];
80
+ if (!isRecord(provider)) return [];
81
+ const models = provider["available_models"];
82
+ if (!Array.isArray(models)) return [];
83
+ const names = [];
84
+ for (const entry of models) if (isRecord(entry)) {
85
+ const name = entry["name"];
86
+ if (typeof name === "string") names.push(name);
87
+ }
88
+ return names;
89
+ }
90
+ /**
91
+ * Apply the full provider block (api_url + available_models) to the JSONC text.
92
+ * Returns the new text with edits applied.
93
+ */
94
+ function applyProviderBlock(text, providerName, apiUrl, models) {
95
+ return applyEdits(text, modify(text, [
96
+ "language_models",
97
+ "openai_compatible",
98
+ providerName
99
+ ], {
100
+ api_url: apiUrl,
101
+ available_models: models
102
+ }, MODIFY_OPTIONS));
103
+ }
104
+ /**
105
+ * Sync exposed models into Zed's settings.json.
106
+ */
107
+ function syncToZed(exposedModels, options) {
108
+ const configPath = findZedSettings();
109
+ if (configPath === void 0) return {
110
+ ok: false,
111
+ configPath: "",
112
+ added: 0,
113
+ removed: 0,
114
+ unchanged: 0,
115
+ summary: "",
116
+ error: "Zed settings.json not found. Checked ~/.config/zed/settings.json and $XDG_CONFIG_HOME/zed/settings.json"
117
+ };
118
+ const originalText = readFileSync(configPath, "utf-8");
119
+ const currentNames = new Set(readCurrentModelNames(originalText, options.providerName));
120
+ const zedModels = exposedModels.map(toZedModel);
121
+ const newNames = new Set(zedModels.map((m) => m.name));
122
+ let added = 0;
123
+ let removed = 0;
124
+ let unchanged = 0;
125
+ for (const name of newNames) if (currentNames.has(name)) unchanged += 1;
126
+ else added += 1;
127
+ for (const name of currentNames) if (!newNames.has(name)) removed += 1;
128
+ const parts = [];
129
+ if (added > 0) parts.push(`${String(added)} added`);
130
+ if (removed > 0) parts.push(`${String(removed)} removed`);
131
+ if (unchanged > 0) parts.push(`${String(unchanged)} unchanged`);
132
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
133
+ if (options.dryRun) return {
134
+ ok: true,
135
+ configPath,
136
+ added,
137
+ removed,
138
+ unchanged,
139
+ summary: `[dry-run] ${summary}`
140
+ };
141
+ writeFileSync(configPath, applyProviderBlock(originalText, options.providerName, options.apiUrl, zedModels), "utf-8");
142
+ return {
143
+ ok: true,
144
+ configPath,
145
+ added,
146
+ removed,
147
+ unchanged,
148
+ summary
149
+ };
150
+ }
151
+ //#endregion
152
+ export { findZedSettings, syncToZed, toZedModel };
@@ -8,6 +8,7 @@
8
8
  * /proxy status Show proxy status
9
9
  * /proxy verify Validate model exposure config against available models
10
10
  * /proxy models List all exposed models with their public IDs
11
+ * /proxy zed-sync Sync exposed models to Zed settings.json (--dry-run)
11
12
  * /proxy config Open settings panel (alias)
12
13
  * /proxy show Summarize current config and exposure policy
13
14
  * /proxy path Show config file location
@@ -56,6 +57,8 @@ import {
56
57
  type ModelExposureConfig,
57
58
  } from "@victor-software-house/pi-openai-proxy/exposure";
58
59
 
60
+ import { syncToZed, type ZedSyncOptions } from "@victor-software-house/pi-openai-proxy/sync/zed";
61
+
59
62
  // ---------------------------------------------------------------------------
60
63
  // Runtime status
61
64
  // ---------------------------------------------------------------------------
@@ -140,13 +143,15 @@ export default function proxyExtension(pi: ExtensionAPI): void {
140
143
  "status",
141
144
  "verify",
142
145
  "models",
146
+ "zed-sync",
143
147
  "config",
144
148
  "show",
145
149
  "path",
146
150
  "reset",
147
151
  "help",
148
152
  ];
149
- const USAGE = "/proxy [start|stop|restart|status|verify|models|config|show|path|reset|help]";
153
+ const USAGE =
154
+ "/proxy [start|stop|restart|status|verify|models|zed-sync|config|show|path|reset|help]";
150
155
 
151
156
  pi.registerCommand("proxy", {
152
157
  description: "Manage the OpenAI-compatible proxy",
@@ -179,6 +184,9 @@ export default function proxyExtension(pi: ExtensionAPI): void {
179
184
  case "models":
180
185
  showModels(ctx);
181
186
  return;
187
+ case "zed-sync":
188
+ handleZedSync(ctx, args);
189
+ return;
182
190
  case "show":
183
191
  showConfig(ctx);
184
192
  return;
@@ -577,6 +585,46 @@ export default function proxyExtension(pi: ExtensionAPI): void {
577
585
  }
578
586
  }
579
587
 
588
+ // --- Zed sync ---
589
+
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)
596
+ const available = getAvailableModels();
597
+ const allModels = getAllRegisteredModels();
598
+ const outcome = computeModelExposure(available, allModels, buildExposureConfig());
599
+ if (!outcome.ok) {
600
+ ctx.ui.notify(`Model exposure error: ${outcome.message}`, "error");
601
+ return;
602
+ }
603
+
604
+ if (outcome.models.length === 0) {
605
+ ctx.ui.notify("No models exposed. Nothing to sync.", "warning");
606
+ return;
607
+ }
608
+
609
+ const syncOptions: ZedSyncOptions = {
610
+ providerName: "Pi Proxy",
611
+ apiUrl: `http://${config.host}:${String(config.port)}/v1`,
612
+ dryRun,
613
+ };
614
+
615
+ const result = syncToZed(outcome.models, syncOptions);
616
+
617
+ if (!result.ok) {
618
+ ctx.ui.notify(result.error ?? "Zed sync failed", "error");
619
+ return;
620
+ }
621
+
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");
626
+ }
627
+
580
628
  async function waitForReady(timeoutMs: number): Promise<RuntimeStatus> {
581
629
  const start = Date.now();
582
630
  const interval = 300;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.3.1",
3
+ "version": "4.4.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",
@@ -41,6 +41,10 @@
41
41
  "./exposure": {
42
42
  "import": "./dist/exposure.mjs",
43
43
  "types": "./dist/exposure.d.mts"
44
+ },
45
+ "./sync/zed": {
46
+ "import": "./dist/sync-zed.mjs",
47
+ "types": "./dist/sync-zed.d.mts"
44
48
  }
45
49
  },
46
50
  "main": "dist/index.mjs",
@@ -73,6 +77,7 @@
73
77
  "@sinclair/typebox": "^0.34.0",
74
78
  "citty": "^0.1.6",
75
79
  "hono": "^4.12.8",
80
+ "jsonc-parser": "^3.3.1",
76
81
  "zod": "^4.3.6"
77
82
  },
78
83
  "devDependencies": {