@victor-software-house/pi-openai-proxy 4.3.0 → 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 +21 -15
- 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
|
@@ -76,18 +76,19 @@ export default function proxyExtension(pi: ExtensionAPI): void {
|
|
|
76
76
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
77
77
|
const packageRoot = resolve(extensionDir, "..");
|
|
78
78
|
|
|
79
|
-
// --- Model registry access (
|
|
79
|
+
// --- Model registry access (cached, refreshed per call) ---
|
|
80
|
+
|
|
81
|
+
const cachedAuth = AuthStorage.create();
|
|
82
|
+
const cachedRegistry = new ModelRegistry(cachedAuth);
|
|
80
83
|
|
|
81
84
|
function getAvailableModels(): Model<Api>[] {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return registry.getAvailable();
|
|
85
|
+
cachedRegistry.refresh();
|
|
86
|
+
return cachedRegistry.getAvailable();
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
function getAllRegisteredModels(): Model<Api>[] {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return registry.getAll();
|
|
90
|
+
cachedRegistry.refresh();
|
|
91
|
+
return cachedRegistry.getAll();
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function buildExposureConfig(): ModelExposureConfig {
|
|
@@ -256,8 +257,13 @@ export default function proxyExtension(pi: ExtensionAPI): void {
|
|
|
256
257
|
|
|
257
258
|
async function probe(): Promise<RuntimeStatus> {
|
|
258
259
|
try {
|
|
260
|
+
const headers: Record<string, string> = {};
|
|
261
|
+
if (config.authToken.length > 0) {
|
|
262
|
+
headers["authorization"] = `Bearer ${config.authToken}`;
|
|
263
|
+
}
|
|
259
264
|
const res = await fetch(`${proxyUrl()}/v1/models`, {
|
|
260
265
|
signal: AbortSignal.timeout(2000),
|
|
266
|
+
headers,
|
|
261
267
|
});
|
|
262
268
|
if (res.ok) {
|
|
263
269
|
const body = (await res.json()) as { data?: unknown[] };
|
|
@@ -639,8 +645,10 @@ export default function proxyExtension(pi: ExtensionAPI): void {
|
|
|
639
645
|
{ enableSearch: true },
|
|
640
646
|
);
|
|
641
647
|
|
|
642
|
-
// SettingsList has no public
|
|
643
|
-
//
|
|
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.
|
|
644
652
|
function jumpProvider(direction: "prev" | "next"): void {
|
|
645
653
|
const sl = list as unknown as Record<string, unknown>;
|
|
646
654
|
const idx = sl["selectedIndex"] as number;
|
|
@@ -837,16 +845,14 @@ export default function proxyExtension(pi: ExtensionAPI): void {
|
|
|
837
845
|
}
|
|
838
846
|
}
|
|
839
847
|
|
|
840
|
-
|
|
841
|
-
|
|
848
|
+
// Local type guards — the extension resolves against the built dist, so it
|
|
849
|
+
// cannot import these from the source config module during development.
|
|
842
850
|
function isPublicModelIdMode(v: string): v is PublicModelIdMode {
|
|
843
|
-
return
|
|
851
|
+
return v === "collision-prefixed" || v === "universal" || v === "always-prefixed";
|
|
844
852
|
}
|
|
845
853
|
|
|
846
|
-
const VALID_EXPOSURE_MODES = new Set<string>(["all", "scoped", "custom"]);
|
|
847
|
-
|
|
848
854
|
function isModelExposureMode(v: string): v is ModelExposureMode {
|
|
849
|
-
return
|
|
855
|
+
return v === "all" || v === "scoped" || v === "custom";
|
|
850
856
|
}
|
|
851
857
|
|
|
852
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.
|
|
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
|
},
|