@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 +3 -1
- package/dist/config.mjs +2 -117
- package/dist/exposure.mjs +5 -1
- package/dist/index.mjs +34 -15
- package/dist/schema-x6mps-hM.mjs +127 -0
- package/extensions/proxy.ts +73 -16
- package/package.json +2 -1
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 {
|
|
3
|
-
|
|
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":
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
728
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|
package/extensions/proxy.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
return registry.getAvailable();
|
|
85
|
+
cachedRegistry.refresh();
|
|
86
|
+
return cachedRegistry.getAvailable();
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function getAllRegisteredModels(): Model<Api>[] {
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
592
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
},
|