@victor-software-house/pi-openai-proxy 4.9.3 → 4.9.5

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/README.md CHANGED
@@ -20,10 +20,10 @@ A local OpenAI-compatible HTTP proxy built on [pi](https://github.com/badlogic/p
20
20
 
21
21
  ```bash
22
22
  # Install globally
23
- npm install -g @victor-software-house/pi-openai-proxy
23
+ pnpm add -g @victor-software-house/pi-openai-proxy
24
24
 
25
- # Or run directly with npx
26
- npx @victor-software-house/pi-openai-proxy
25
+ # Or run directly
26
+ pnpm dlx @victor-software-house/pi-openai-proxy
27
27
  ```
28
28
 
29
29
  ## Quickstart
@@ -339,17 +339,18 @@ The extension detects externally running instances and shows their status via `/
339
339
  ## Dev Workflow
340
340
 
341
341
  ```bash
342
- bun install # Install dependencies
343
- bun run dev # Run in development
344
- bun run build # Build for npm (tsdown)
345
- bun run typecheck # TypeScript strict check
346
- bun run lint # Biome + oxlint (strict)
347
- bun test # Run all tests
342
+ pnpm install # Install dependencies
343
+ pnpm run dev # Run in development
344
+ pnpm run build # Build for npm (tsdown)
345
+ pnpm run typecheck # TypeScript strict check
346
+ pnpm run lint # Biome + oxlint (strict)
347
+ pnpm test # Run all tests
348
348
  ```
349
349
 
350
350
  ### Tooling
351
351
 
352
- - **Bun** — runtime, test runner, package manager
352
+ - **pnpm** — package manager and script runner
353
+ - **Bun** — runtime and test runner
353
354
  - **tsdown** — npm build (ESM + .d.ts)
354
355
  - **Biome** — format + lint
355
356
  - **oxlint** — type-aware lint with strict rules (`.oxlintrc.json`)
package/dist/index.mjs CHANGED
@@ -4,7 +4,6 @@ import { computeModelExposure, resolveExposedModel } from "./exposure.mjs";
4
4
  import * as z from "zod";
5
5
  import { AuthStorage, ModelRegistry, SettingsManager } from "@mariozechner/pi-coding-agent";
6
6
  import { randomBytes } from "node:crypto";
7
- import { Type } from "@sinclair/typebox";
8
7
  import { completeSimple, streamSimple } from "@mariozechner/pi-ai";
9
8
  import { Hono } from "hono";
10
9
  import { stream } from "hono/streaming";
@@ -53,8 +52,8 @@ let settingsManager;
53
52
  */
54
53
  function initRegistry() {
55
54
  authStorage = AuthStorage.create();
56
- registry = new ModelRegistry(authStorage);
57
- settingsManager = SettingsManager.create();
55
+ registry = ModelRegistry.create(authStorage);
56
+ settingsManager = SettingsManager.create(process.cwd());
58
57
  return registry.getError();
59
58
  }
60
59
  function getRegistry() {
@@ -89,9 +88,10 @@ async function getRequestAuth(model) {
89
88
  *
90
89
  * Returns undefined when no filter is configured (all models enabled).
91
90
  */
92
- function getEnabledModels() {
93
- getSettingsManager().reload();
94
- return getSettingsManager().getEnabledModels();
91
+ async function getEnabledModels() {
92
+ const manager = getSettingsManager();
93
+ await manager.reload();
94
+ return manager.getEnabledModels();
95
95
  }
96
96
  //#endregion
97
97
  //#region src/server/errors.ts
@@ -761,6 +761,326 @@ async function* streamToSSE(events, requestId, model, includeUsage) {
761
761
  yield encodeDone();
762
762
  }
763
763
  //#endregion
764
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/memory/metrics.mjs
765
+ /** TypeBox instantiation metrics */
766
+ const Metrics = {
767
+ assign: 0,
768
+ create: 0,
769
+ clone: 0,
770
+ discard: 0,
771
+ update: 0
772
+ };
773
+ //#endregion
774
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/guard/guard.mjs
775
+ /** Returns true if this value is an array */
776
+ function IsArray(value) {
777
+ return Array.isArray(value);
778
+ }
779
+ /** Returns true if this value is bigint */
780
+ function IsBigInt(value) {
781
+ return IsEqual(typeof value, "bigint");
782
+ }
783
+ /** Returns true if this value is a boolean */
784
+ function IsBoolean(value) {
785
+ return IsEqual(typeof value, "boolean");
786
+ }
787
+ /** Returns true if this value is null */
788
+ function IsNull(value) {
789
+ return IsEqual(value, null);
790
+ }
791
+ /** Returns true if this value is number */
792
+ function IsNumber(value) {
793
+ return Number.isFinite(value);
794
+ }
795
+ /** Returns true if this value is an object */
796
+ function IsObject(value) {
797
+ return IsEqual(typeof value, "object") && !IsNull(value);
798
+ }
799
+ /** Returns true if this value is string */
800
+ function IsString(value) {
801
+ return IsEqual(typeof value, "string");
802
+ }
803
+ function IsEqual(left, right) {
804
+ return left === right;
805
+ }
806
+ /** Returns true if the PropertyKey is Unsafe (ref: prototype-pollution). */
807
+ function IsUnsafePropertyKey(key) {
808
+ return IsEqual(key, "__proto__") || IsEqual(key, "constructor") || IsEqual(key, "prototype");
809
+ }
810
+ /** Returns true if this value has this property key */
811
+ function HasPropertyKey(value, key) {
812
+ return IsUnsafePropertyKey(key) ? Object.prototype.hasOwnProperty.call(value, key) : key in value;
813
+ }
814
+ /** Returns property keys for this object via `Object.getOwnPropertyKeys({ ... })` */
815
+ function Keys(value) {
816
+ return Object.getOwnPropertyNames(value);
817
+ }
818
+ //#endregion
819
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/memory/clone.mjs
820
+ function IsGuard(value) {
821
+ return IsObject(value) && HasPropertyKey(value, "~guard");
822
+ }
823
+ function FromGuard(value) {
824
+ return value;
825
+ }
826
+ function FromArray(value) {
827
+ return value.map((value) => FromValue(value));
828
+ }
829
+ function FromObject(value) {
830
+ const result = {};
831
+ const descriptors = Object.getOwnPropertyDescriptors(value);
832
+ for (const key of Object.keys(descriptors)) {
833
+ const descriptor = descriptors[key];
834
+ if (HasPropertyKey(descriptor, "value")) Object.defineProperty(result, key, {
835
+ ...descriptor,
836
+ value: FromValue(descriptor.value)
837
+ });
838
+ }
839
+ return result;
840
+ }
841
+ function FromRegExp(value) {
842
+ return new RegExp(value.source, value.flags);
843
+ }
844
+ function FromUnknown(value) {
845
+ return value;
846
+ }
847
+ function FromValue(value) {
848
+ return value instanceof RegExp ? FromRegExp(value) : IsGuard(value) ? FromGuard(value) : IsArray(value) ? FromArray(value) : IsObject(value) ? FromObject(value) : FromUnknown(value);
849
+ }
850
+ /**
851
+ * Clones a value using the TypeBox type cloning strategy. This function preserves non-enumerable
852
+ * properties from the source value. This is to ensure cloned types retain discriminable
853
+ * hidden properties.
854
+ */
855
+ function Clone(value) {
856
+ Metrics.clone += 1;
857
+ return FromValue(value);
858
+ }
859
+ //#endregion
860
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/settings/settings.mjs
861
+ const settings = {
862
+ immutableTypes: false,
863
+ maxErrors: 8,
864
+ useAcceleration: true,
865
+ exactOptionalPropertyTypes: false,
866
+ enumerableKind: false,
867
+ correctiveParse: false
868
+ };
869
+ /** Gets current system settings */
870
+ function Get() {
871
+ return settings;
872
+ }
873
+ //#endregion
874
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/memory/create.mjs
875
+ function MergeHidden(left, right) {
876
+ for (const key of Object.keys(right)) Object.defineProperty(left, key, {
877
+ configurable: true,
878
+ writable: true,
879
+ enumerable: false,
880
+ value: right[key]
881
+ });
882
+ return left;
883
+ }
884
+ function Merge(left, right) {
885
+ return {
886
+ ...left,
887
+ ...right
888
+ };
889
+ }
890
+ /**
891
+ * Creates an object with hidden, enumerable, and optional property sets. This function
892
+ * ensures types are instantiated according to configuration rules for enumerable and
893
+ * non-enumerable properties.
894
+ */
895
+ function Create(hidden, enumerable, options = {}) {
896
+ Metrics.create += 1;
897
+ const settings = Get();
898
+ const withOptions = Merge(enumerable, options);
899
+ const withHidden = settings.enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
900
+ return settings.immutableTypes ? Object.freeze(withHidden) : withHidden;
901
+ }
902
+ //#endregion
903
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/memory/update.mjs
904
+ /**
905
+ * Updates a value with new properties while preserving property enumerability. Use this function to modify
906
+ * existing types without altering their configuration.
907
+ */
908
+ function Update(current, hidden, enumerable) {
909
+ Metrics.update += 1;
910
+ const settings = Get();
911
+ const result = Clone(current);
912
+ for (const key of Object.keys(hidden)) Object.defineProperty(result, key, {
913
+ configurable: true,
914
+ writable: true,
915
+ enumerable: settings.enumerableKind,
916
+ value: hidden[key]
917
+ });
918
+ for (const key of Object.keys(enumerable)) Object.defineProperty(result, key, {
919
+ configurable: true,
920
+ enumerable: true,
921
+ writable: true,
922
+ value: enumerable[key]
923
+ });
924
+ return result;
925
+ }
926
+ //#endregion
927
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/schema.mjs
928
+ function IsSchema(value) {
929
+ return IsObject(value);
930
+ }
931
+ //#endregion
932
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/_optional.mjs
933
+ /** Adds Optional to the given type. */
934
+ function OptionalAdd(type) {
935
+ return Update(type, { "~optional": true }, {});
936
+ }
937
+ /** Applies an Optional modifier to the given type. */
938
+ function Optional(type) {
939
+ return OptionalAdd(type);
940
+ }
941
+ /** Returns true if the given value is TOptional */
942
+ function IsOptional(value) {
943
+ return IsSchema(value) && HasPropertyKey(value, "~optional");
944
+ }
945
+ //#endregion
946
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/array.mjs
947
+ /** Creates an Array type. */
948
+ function _Array_(items, options) {
949
+ return Create({ "~kind": "Array" }, {
950
+ type: "array",
951
+ items
952
+ }, options);
953
+ }
954
+ //#endregion
955
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/properties.mjs
956
+ /** Creates a RequiredArray derived from the given TProperties value. */
957
+ function RequiredArray(properties) {
958
+ return Keys(properties).filter((key) => !IsOptional(properties[key]));
959
+ }
960
+ //#endregion
961
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/object.mjs
962
+ /** Creates an Object type. */
963
+ function _Object_(properties, options = {}) {
964
+ const requiredKeys = RequiredArray(properties);
965
+ return Create({ "~kind": "Object" }, {
966
+ type: "object",
967
+ ...requiredKeys.length > 0 ? { required: requiredKeys } : {},
968
+ properties
969
+ }, options);
970
+ }
971
+ //#endregion
972
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/union.mjs
973
+ /** Creates a Union type. */
974
+ function Union(anyOf, options = {}) {
975
+ return Create({ "~kind": "Union" }, { anyOf }, options);
976
+ }
977
+ //#endregion
978
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/unknown.mjs
979
+ /** Creates an Unknown type. */
980
+ function Unknown(options) {
981
+ return Create({ ["~kind"]: "Unknown" }, {}, options);
982
+ }
983
+ //#endregion
984
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/system/hashing/hash.mjs
985
+ var ByteMarker;
986
+ (function(ByteMarker) {
987
+ ByteMarker[ByteMarker["Array"] = 0] = "Array";
988
+ ByteMarker[ByteMarker["BigInt"] = 1] = "BigInt";
989
+ ByteMarker[ByteMarker["Boolean"] = 2] = "Boolean";
990
+ ByteMarker[ByteMarker["Date"] = 3] = "Date";
991
+ ByteMarker[ByteMarker["Constructor"] = 4] = "Constructor";
992
+ ByteMarker[ByteMarker["Function"] = 5] = "Function";
993
+ ByteMarker[ByteMarker["Null"] = 6] = "Null";
994
+ ByteMarker[ByteMarker["Number"] = 7] = "Number";
995
+ ByteMarker[ByteMarker["Object"] = 8] = "Object";
996
+ ByteMarker[ByteMarker["RegExp"] = 9] = "RegExp";
997
+ ByteMarker[ByteMarker["String"] = 10] = "String";
998
+ ByteMarker[ByteMarker["Symbol"] = 11] = "Symbol";
999
+ ByteMarker[ByteMarker["TypeArray"] = 12] = "TypeArray";
1000
+ ByteMarker[ByteMarker["Undefined"] = 13] = "Undefined";
1001
+ })(ByteMarker || (ByteMarker = {}));
1002
+ Array.from({ length: 256 }).map((_, i) => BigInt(i));
1003
+ const F64 = new Float64Array(1);
1004
+ new DataView(F64.buffer);
1005
+ new Uint8Array(F64.buffer);
1006
+ new TextEncoder();
1007
+ //#endregion
1008
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/boolean.mjs
1009
+ /** Creates a Boolean type. */
1010
+ function Boolean(options) {
1011
+ return Create({ "~kind": "Boolean" }, { type: "boolean" }, options);
1012
+ }
1013
+ //#endregion
1014
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/integer.mjs
1015
+ const IntegerPattern = "-?(?:0|[1-9][0-9]*)";
1016
+ /** Creates a Integer type. */
1017
+ function Integer(options) {
1018
+ return Create({ "~kind": "Integer" }, { type: "integer" }, options);
1019
+ }
1020
+ //#endregion
1021
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/literal.mjs
1022
+ var InvalidLiteralValue = class extends Error {
1023
+ constructor(value) {
1024
+ super(`Invalid Literal value`);
1025
+ Object.defineProperty(this, "cause", {
1026
+ value: { value },
1027
+ writable: false,
1028
+ configurable: false,
1029
+ enumerable: false
1030
+ });
1031
+ }
1032
+ };
1033
+ function LiteralTypeName(value) {
1034
+ return IsBigInt(value) ? "bigint" : IsBoolean(value) ? "boolean" : IsNumber(value) ? "number" : IsString(value) ? "string" : (() => {
1035
+ throw new InvalidLiteralValue(value);
1036
+ })();
1037
+ }
1038
+ /** Creates a Literal type. */
1039
+ function Literal(value, options) {
1040
+ return Create({ "~kind": "Literal" }, {
1041
+ type: LiteralTypeName(value),
1042
+ const: value
1043
+ }, options);
1044
+ }
1045
+ //#endregion
1046
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/null.mjs
1047
+ /** Creates a Null type. */
1048
+ function Null(options) {
1049
+ return Create({ "~kind": "Null" }, { type: "null" }, options);
1050
+ }
1051
+ //#endregion
1052
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/number.mjs
1053
+ const NumberPattern = "-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?";
1054
+ /** Creates a Number type. */
1055
+ function Number$1(options) {
1056
+ return Create({ "~kind": "Number" }, { type: "number" }, options);
1057
+ }
1058
+ //#endregion
1059
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/string.mjs
1060
+ /** Creates a String type. */
1061
+ function String$1(options) {
1062
+ return Create({ "~kind": "String" }, { type: "string" }, options);
1063
+ }
1064
+ //#endregion
1065
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/types/record.mjs
1066
+ const IntegerKey = `^${IntegerPattern}$`;
1067
+ `${NumberPattern}`;
1068
+ //#endregion
1069
+ //#region node_modules/.pnpm/typebox@1.1.38/node_modules/typebox/build/type/script/token/internal/char.mjs
1070
+ function Range(start, end) {
1071
+ return Array.from({ length: end - start + 1 }, (_, i) => String.fromCharCode(start + i));
1072
+ }
1073
+ const Alpha = [...Range(97, 122), ...Range(65, 90)];
1074
+ const Digit = ["0", ...Range(49, 57)];
1075
+ [...Digit];
1076
+ [...[
1077
+ ...Alpha,
1078
+ "_",
1079
+ "$"
1080
+ ], ...Digit];
1081
+ [...Digit];
1082
+ new RegExp(IntegerKey);
1083
+ //#endregion
764
1084
  //#region src/openai/json-schema-to-typebox.ts
765
1085
  /**
766
1086
  * JSON Schema -> TypeBox conversion for OpenAI function tool parameters.
@@ -829,7 +1149,7 @@ function jsonSchemaToTypebox(schema, path = "") {
829
1149
  if (typeof rawType !== "string") {
830
1150
  if (Object.keys(schema).length === 0 || Object.keys(schema).length === 1 && description !== void 0) return {
831
1151
  ok: true,
832
- schema: Type.Unknown(opts)
1152
+ schema: Unknown(opts)
833
1153
  };
834
1154
  return {
835
1155
  ok: false,
@@ -841,23 +1161,23 @@ function jsonSchemaToTypebox(schema, path = "") {
841
1161
  case "object": return convertObject(schema, path, opts);
842
1162
  case "string": return {
843
1163
  ok: true,
844
- schema: Type.String(opts)
1164
+ schema: String$1(opts)
845
1165
  };
846
1166
  case "number": return {
847
1167
  ok: true,
848
- schema: Type.Number(opts)
1168
+ schema: Number$1(opts)
849
1169
  };
850
1170
  case "integer": return {
851
1171
  ok: true,
852
- schema: Type.Integer(opts)
1172
+ schema: Integer(opts)
853
1173
  };
854
1174
  case "boolean": return {
855
1175
  ok: true,
856
- schema: Type.Boolean(opts)
1176
+ schema: Boolean(opts)
857
1177
  };
858
1178
  case "null": return {
859
1179
  ok: true,
860
- schema: Type.Null(opts)
1180
+ schema: Null(opts)
861
1181
  };
862
1182
  case "array": return convertArray(schema, path, opts);
863
1183
  default: return {
@@ -894,7 +1214,7 @@ function convertAnyOf(branches, path, opts) {
894
1214
  }
895
1215
  return {
896
1216
  ok: true,
897
- schema: Type.Union(converted, opts)
1217
+ schema: Union(converted, opts)
898
1218
  };
899
1219
  }
900
1220
  function convertEnum(values, path, opts) {
@@ -912,10 +1232,9 @@ function convertEnum(values, path, opts) {
912
1232
  message: `Empty enum at ${path || "root"}`,
913
1233
  path
914
1234
  };
915
- const literals = stringValues.map((v) => Type.Literal(v));
916
1235
  return {
917
1236
  ok: true,
918
- schema: Type.Union(literals, opts)
1237
+ schema: Union(stringValues.map((v) => Literal(v)), opts)
919
1238
  };
920
1239
  }
921
1240
  function convertNullableType(typeArray, schema, path, opts) {
@@ -938,7 +1257,7 @@ function convertNullableType(typeArray, schema, path, opts) {
938
1257
  if (!innerResult.ok) return innerResult;
939
1258
  return {
940
1259
  ok: true,
941
- schema: Type.Union([innerResult.schema, Type.Null()], opts)
1260
+ schema: Union([innerResult.schema, Null()], opts)
942
1261
  };
943
1262
  }
944
1263
  function convertObject(schema, path, opts) {
@@ -952,7 +1271,7 @@ function convertObject(schema, path, opts) {
952
1271
  };
953
1272
  if (rawProperties === void 0) return {
954
1273
  ok: true,
955
- schema: Type.Object({}, opts)
1274
+ schema: _Object_({}, opts)
956
1275
  };
957
1276
  if (!isRecord(rawProperties)) return {
958
1277
  ok: false,
@@ -968,24 +1287,24 @@ function convertObject(schema, path, opts) {
968
1287
  const result = jsonSchemaToTypebox(propSchema, path.length > 0 ? `${path}.${key}` : key);
969
1288
  if (!result.ok) return result;
970
1289
  if (requiredSet.has(key)) typeboxProperties[key] = result.schema;
971
- else typeboxProperties[key] = Type.Optional(result.schema);
1290
+ else typeboxProperties[key] = Optional(result.schema);
972
1291
  }
973
1292
  return {
974
1293
  ok: true,
975
- schema: Type.Object(typeboxProperties, opts)
1294
+ schema: _Object_(typeboxProperties, opts)
976
1295
  };
977
1296
  }
978
1297
  function convertArray(schema, path, opts) {
979
1298
  const items = schema["items"];
980
1299
  if (items === void 0) return {
981
1300
  ok: true,
982
- schema: Type.Array(Type.Unknown(), opts)
1301
+ schema: _Array_(Unknown(), opts)
983
1302
  };
984
1303
  const itemResult = jsonSchemaToTypebox(items, path.length > 0 ? `${path}.items` : "items");
985
1304
  if (!itemResult.ok) return itemResult;
986
1305
  return {
987
1306
  ok: true,
988
- schema: Type.Array(itemResult.schema, opts)
1307
+ schema: _Array_(itemResult.schema, opts)
989
1308
  };
990
1309
  }
991
1310
  //#endregion
@@ -1008,7 +1327,7 @@ function convertTools(openaiTools) {
1008
1327
  param: result.path
1009
1328
  };
1010
1329
  parameters = result.schema;
1011
- } else parameters = Type.Object({});
1330
+ } else parameters = _Object_({});
1012
1331
  piTools.push({
1013
1332
  name: fn.name,
1014
1333
  description: fn.description ?? "",
@@ -1631,19 +1950,19 @@ async function piStream(model, context, request, options) {
1631
1950
  }
1632
1951
  //#endregion
1633
1952
  //#region src/server/routes.ts
1634
- function fileConfigReader() {
1953
+ async function fileConfigReader() {
1635
1954
  const file = loadConfigFromFile();
1636
1955
  return {
1637
1956
  publicModelIdMode: file.publicModelIdMode,
1638
1957
  modelExposureMode: file.modelExposureMode,
1639
- enabledModels: getEnabledModels(),
1958
+ enabledModels: await getEnabledModels(),
1640
1959
  customModels: file.customModels,
1641
1960
  providerPrefixes: file.providerPrefixes
1642
1961
  };
1643
1962
  }
1644
1963
  function createRoutes(config, configReader = fileConfigReader) {
1645
- function getExposure() {
1646
- const outcome = computeModelExposure(getAvailableModels(), configReader());
1964
+ async function getExposure() {
1965
+ const outcome = computeModelExposure(getAvailableModels(), await configReader());
1647
1966
  if (!outcome.ok) throw new Error(`Model exposure configuration error: ${outcome.message}`);
1648
1967
  return outcome;
1649
1968
  }
@@ -1657,17 +1976,17 @@ function createRoutes(config, configReader = fileConfigReader) {
1657
1976
  return mapped;
1658
1977
  }
1659
1978
  const routes = new Hono();
1660
- routes.get("/v1/models", (c) => {
1661
- const exposure = getExposure();
1979
+ routes.get("/v1/models", async (c) => {
1980
+ const exposure = await getExposure();
1662
1981
  return c.json(buildModelList(exposure.models));
1663
1982
  });
1664
- routes.get("/v1/models/*", (c) => {
1983
+ routes.get("/v1/models/*", async (c) => {
1665
1984
  const rawPath = c.req.path;
1666
1985
  if (!rawPath.startsWith("/v1/models/")) return c.json(modelNotFound(""), 404);
1667
1986
  const modelIdEncoded = rawPath.slice(11);
1668
1987
  if (modelIdEncoded.length === 0) return c.json(modelNotFound(""), 404);
1669
1988
  const modelId = decodeURIComponent(modelIdEncoded);
1670
- const resolved = resolveExposedModel(getExposure(), modelId);
1989
+ const resolved = resolveExposedModel(await getExposure(), modelId);
1671
1990
  if (resolved === void 0) return c.json(modelNotFound(modelId), 404);
1672
1991
  return c.json(toOpenAIModel(resolved));
1673
1992
  });
@@ -1687,7 +2006,7 @@ function createRoutes(config, configReader = fileConfigReader) {
1687
2006
  return c.json(invalidRequest(validation.message, validation.param ?? void 0), 400);
1688
2007
  }
1689
2008
  const request = validation.data;
1690
- const resolved = resolveExposedModel(getExposure(), request.model);
2009
+ const resolved = resolveExposedModel(await getExposure(), request.model);
1691
2010
  if (resolved === void 0) return c.json(modelNotFound(request.model), 404);
1692
2011
  const model = resolved.model;
1693
2012
  const canonicalModelId = resolved.canonicalId;
@@ -20,8 +20,10 @@
20
20
  */
21
21
 
22
22
  import { type ChildProcess, spawn } from "node:child_process";
23
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
24
- import { dirname, resolve } from "node:path";
23
+ import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
24
+ import { access } from "node:fs/promises";
25
+ import { homedir } from "node:os";
26
+ import { delimiter, dirname, join, resolve } from "node:path";
25
27
  import { fileURLToPath } from "node:url";
26
28
  import type { Api, Model } from "@mariozechner/pi-ai";
27
29
  import {
@@ -94,24 +96,24 @@ export default function proxyExtension(pi: ExtensionAPI): void {
94
96
  // --- Model registry access (cached, refreshed per call) ---
95
97
 
96
98
  const cachedAuth = AuthStorage.create();
97
- const cachedRegistry = new ModelRegistry(cachedAuth);
98
- const settingsManager = SettingsManager.create();
99
+ const cachedRegistry = ModelRegistry.create(cachedAuth);
100
+ const settingsManager = SettingsManager.create(process.cwd());
99
101
 
100
102
  function getAvailableModels(): Model<Api>[] {
101
103
  cachedRegistry.refresh();
102
104
  return cachedRegistry.getAvailable();
103
105
  }
104
106
 
105
- function getEnabledModels(): readonly string[] | undefined {
106
- settingsManager.reload();
107
+ async function getEnabledModels(): Promise<readonly string[] | undefined> {
108
+ await settingsManager.reload();
107
109
  return settingsManager.getEnabledModels();
108
110
  }
109
111
 
110
- function buildExposureConfig(): ModelExposureConfig {
112
+ async function buildExposureConfig(): Promise<ModelExposureConfig> {
111
113
  return {
112
114
  publicModelIdMode: config.publicModelIdMode,
113
115
  modelExposureMode: config.modelExposureMode,
114
- enabledModels: getEnabledModels(),
116
+ enabledModels: await getEnabledModels(),
115
117
  customModels: config.customModels,
116
118
  providerPrefixes: config.providerPrefixes,
117
119
  };
@@ -129,6 +131,41 @@ export default function proxyExtension(pi: ExtensionAPI): void {
129
131
  return "pi-proxy";
130
132
  }
131
133
 
134
+ async function isExecutable(path: string): Promise<boolean> {
135
+ try {
136
+ await access(path, constants.X_OK);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ async function resolveBunExecutable(): Promise<string> {
144
+ if ("bun" in process.versions) return process.execPath;
145
+
146
+ const bunInstall = process.env["BUN_INSTALL"];
147
+ const pathEntries = (process.env["PATH"] ?? "")
148
+ .split(delimiter)
149
+ .filter((entry) => entry.length > 0);
150
+ const candidates = [
151
+ process.env["PI_OPENAI_PROXY_BUN"],
152
+ bunInstall === undefined ? undefined : join(bunInstall, "bin", "bun"),
153
+ ...pathEntries.map((entry) => join(entry, "bun")),
154
+ join(homedir(), ".bun", "bin", "bun"),
155
+ join(homedir(), ".local", "share", "mise", "installs", "bun", "latest", "bin", "bun"),
156
+ "/opt/homebrew/bin/bun",
157
+ "/usr/local/bin/bun",
158
+ ];
159
+
160
+ for (const candidate of candidates) {
161
+ if (candidate !== undefined && (await isExecutable(candidate))) return candidate;
162
+ }
163
+
164
+ throw new Error(
165
+ "Bun runtime not found. Install Bun or set PI_OPENAI_PROXY_BUN to the Bun executable path.",
166
+ );
167
+ }
168
+
132
169
  function proxyUrl(): string {
133
170
  return `http://${config.host}:${String(config.port)}`;
134
171
  }
@@ -137,7 +174,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
137
174
 
138
175
  pi.on("session_start", async (_event, ctx) => {
139
176
  config = loadConfigFromFile();
140
- maybeAutoSyncZed(ctx);
177
+ await maybeAutoSyncZed(ctx);
141
178
  await refreshStatus(ctx);
142
179
  });
143
180
 
@@ -193,16 +230,16 @@ export default function proxyExtension(pi: ExtensionAPI): void {
193
230
  await showStatus(ctx);
194
231
  return;
195
232
  case "verify":
196
- verifyExposure(ctx);
233
+ await verifyExposure(ctx);
197
234
  return;
198
235
  case "models":
199
- showModels(ctx);
236
+ await showModels(ctx);
200
237
  return;
201
238
  case "zed-sync":
202
- handleZedSync(ctx, args);
239
+ await handleZedSync(ctx, args);
203
240
  return;
204
241
  case "show":
205
- showConfig(ctx);
242
+ await showConfig(ctx);
206
243
  return;
207
244
  case "path":
208
245
  ctx.ui.notify(getConfigPath(), "info");
@@ -343,9 +380,9 @@ export default function proxyExtension(pi: ExtensionAPI): void {
343
380
  const bin = findProxyBinary();
344
381
 
345
382
  if (config.lifetime === "detached") {
346
- startDetached(bin, proxyEnv);
383
+ await startDetached(bin, proxyEnv);
347
384
  } else {
348
- startSessionTied(ctx, bin, proxyEnv);
385
+ await startSessionTied(ctx, bin, proxyEnv);
349
386
  }
350
387
 
351
388
  const ready = await waitForReady(3000);
@@ -366,9 +403,9 @@ export default function proxyExtension(pi: ExtensionAPI): void {
366
403
  }
367
404
  }
368
405
 
369
- function startDetached(bin: string, env: Record<string, string>): void {
406
+ async function startDetached(bin: string, env: Record<string, string>): Promise<void> {
370
407
  const usesBun = bin.endsWith(".mjs");
371
- const cmd = usesBun ? "bun" : bin;
408
+ const cmd = usesBun ? await resolveBunExecutable() : bin;
372
409
  const cmdArgs = usesBun ? ["run", bin] : [];
373
410
 
374
411
  const child = spawn(cmd, cmdArgs, {
@@ -377,6 +414,10 @@ export default function proxyExtension(pi: ExtensionAPI): void {
377
414
  env: { ...process.env, ...env },
378
415
  });
379
416
 
417
+ child.once("error", () => {
418
+ if (child.pid !== undefined) removePid();
419
+ });
420
+
380
421
  if (child.pid === undefined) {
381
422
  throw new Error("No PID returned from spawn");
382
423
  }
@@ -385,14 +426,18 @@ export default function proxyExtension(pi: ExtensionAPI): void {
385
426
  writePid(child.pid);
386
427
  }
387
428
 
388
- function startSessionTied(ctx: ExtensionContext, bin: string, env: Record<string, string>): void {
429
+ async function startSessionTied(
430
+ ctx: ExtensionContext,
431
+ bin: string,
432
+ env: Record<string, string>,
433
+ ): Promise<void> {
389
434
  if (sessionProcess !== undefined) {
390
435
  ctx.ui.notify("Session proxy already running", "info");
391
436
  return;
392
437
  }
393
438
 
394
439
  const usesBun = bin.endsWith(".mjs");
395
- const cmd = usesBun ? "bun" : bin;
440
+ const cmd = usesBun ? await resolveBunExecutable() : bin;
396
441
  const cmdArgs = usesBun ? ["run", bin] : [];
397
442
 
398
443
  sessionProcess = spawn(cmd, cmdArgs, {
@@ -460,7 +505,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
460
505
  await refreshStatus(ctx);
461
506
  }
462
507
 
463
- function showConfig(ctx: ExtensionContext): void {
508
+ async function showConfig(ctx: ExtensionContext): Promise<void> {
464
509
  config = loadConfigFromFile();
465
510
  const authDisplay =
466
511
  config.authToken.length > 0 ? `enabled (token: ${config.authToken})` : "disabled";
@@ -483,7 +528,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
483
528
  ];
484
529
 
485
530
  if (config.modelExposureMode === "scoped") {
486
- const enabledModels = getEnabledModels();
531
+ const enabledModels = await getEnabledModels();
487
532
  if (enabledModels !== undefined && enabledModels.length > 0) {
488
533
  exposureLines.push(`enabled: ${String(enabledModels.length)} pi model(s)`);
489
534
  }
@@ -500,7 +545,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
500
545
 
501
546
  // Public ID preview (first 5 exposed models)
502
547
  const models = getAvailableModels();
503
- const outcome = computeModelExposure(models, buildExposureConfig());
548
+ const outcome = computeModelExposure(models, await buildExposureConfig());
504
549
  if (outcome.ok && outcome.models.length > 0) {
505
550
  const preview = outcome.models.slice(0, 5).map((m) => m.publicId);
506
551
  const suffix =
@@ -517,10 +562,10 @@ export default function proxyExtension(pi: ExtensionAPI): void {
517
562
 
518
563
  // --- /proxy models ---
519
564
 
520
- function showModels(ctx: ExtensionContext): void {
565
+ async function showModels(ctx: ExtensionContext): Promise<void> {
521
566
  config = loadConfigFromFile();
522
567
  const models = getAvailableModels();
523
- const outcome = computeModelExposure(models, buildExposureConfig());
568
+ const outcome = computeModelExposure(models, await buildExposureConfig());
524
569
 
525
570
  if (!outcome.ok) {
526
571
  ctx.ui.notify(`Model exposure error: ${outcome.message}`, "warning");
@@ -562,7 +607,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
562
607
 
563
608
  // --- /proxy verify ---
564
609
 
565
- function verifyExposure(ctx: ExtensionContext): void {
610
+ async function verifyExposure(ctx: ExtensionContext): Promise<void> {
566
611
  config = loadConfigFromFile();
567
612
  const models = getAvailableModels();
568
613
  const issues: string[] = [];
@@ -586,7 +631,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
586
631
  }
587
632
 
588
633
  // Run the full exposure computation to catch ID/prefix errors
589
- const outcome = computeModelExposure(models, buildExposureConfig());
634
+ const outcome = computeModelExposure(models, await buildExposureConfig());
590
635
  if (!outcome.ok) {
591
636
  issues.push(outcome.message);
592
637
  }
@@ -607,9 +652,9 @@ export default function proxyExtension(pi: ExtensionAPI): void {
607
652
  /**
608
653
  * Run Zed sync and return the result. Shared by the command and auto-sync.
609
654
  */
610
- function runZedSync(dryRun: boolean): { ok: boolean; message: string } {
655
+ async function runZedSync(dryRun: boolean): Promise<{ ok: boolean; message: string }> {
611
656
  const available = getAvailableModels();
612
- const outcome = computeModelExposure(available, buildExposureConfig());
657
+ const outcome = computeModelExposure(available, await buildExposureConfig());
613
658
  if (!outcome.ok) {
614
659
  return { ok: false, message: `Model exposure error: ${outcome.message}` };
615
660
  }
@@ -634,19 +679,19 @@ export default function proxyExtension(pi: ExtensionAPI): void {
634
679
  return { ok: true, message: `${prefix}${result.summary} (${result.configPath})` };
635
680
  }
636
681
 
637
- function handleZedSync(ctx: ExtensionContext, args: string): void {
682
+ async function handleZedSync(ctx: ExtensionContext, args: string): Promise<void> {
638
683
  config = loadConfigFromFile();
639
684
  const dryRun = args.includes("--dry-run");
640
- const result = runZedSync(dryRun);
685
+ const result = await runZedSync(dryRun);
641
686
  ctx.ui.notify(`Zed sync: ${result.message}`, result.ok ? "info" : "error");
642
687
  }
643
688
 
644
689
  /**
645
690
  * Trigger auto-sync to Zed if enabled. Called after config save.
646
691
  */
647
- function maybeAutoSyncZed(ctx: ExtensionContext): void {
692
+ async function maybeAutoSyncZed(ctx: ExtensionContext): Promise<void> {
648
693
  if (!config.zed.autoSync) return;
649
- const result = runZedSync(false);
694
+ const result = await runZedSync(false);
650
695
  if (result.ok) {
651
696
  ctx.ui.notify(`Zed auto-sync: ${result.message}`, "info");
652
697
  }
@@ -1106,7 +1151,7 @@ export default function proxyExtension(pi: ExtensionAPI): void {
1106
1151
  settingsList.updateValue("customModels", customModelsDisplay());
1107
1152
  }
1108
1153
 
1109
- maybeAutoSyncZed(ctx);
1154
+ void maybeAutoSyncZed(ctx);
1110
1155
  tui.requestRender();
1111
1156
  },
1112
1157
  () => done(undefined),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.9.3",
3
+ "version": "4.9.5",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",
@@ -28,6 +28,7 @@
28
28
  "engines": {
29
29
  "node": ">=20"
30
30
  },
31
+ "packageManager": "pnpm@11.0.0-dev.1005",
31
32
  "type": "module",
32
33
  "exports": {
33
34
  ".": {
@@ -60,7 +61,7 @@
60
61
  "build": "tsdown",
61
62
  "start": "bun dist/index.mjs",
62
63
  "typecheck": "tsc --noEmit",
63
- "lint": "bun run lint:biome && bun run lint:oxlint",
64
+ "lint": "pnpm run lint:biome && pnpm run lint:oxlint",
64
65
  "lint:biome": "biome check .",
65
66
  "lint:oxlint": "oxlint --import-plugin --type-aware --tsconfig=./tsconfig.json .",
66
67
  "lint:fix": "biome check --write .",
@@ -68,36 +69,37 @@
68
69
  "test": "bun test",
69
70
  "test:ci": "bun test test/unit/ test/integration/",
70
71
  "prepare": "tsdown",
71
- "prepack": "bun run build",
72
- "prepublishOnly": "bun test && bun run build"
72
+ "prepack": "pnpm run build",
73
+ "prepublishOnly": "pnpm test && pnpm run build"
73
74
  },
74
75
  "dependencies": {
75
- "@mariozechner/pi-ai": "^0.63.1",
76
- "@mariozechner/pi-coding-agent": "^0.63.1",
77
- "@sinclair/typebox": "^0.34.0",
78
- "citty": "^0.1.6",
79
- "hono": "^4.12.8",
76
+ "@mariozechner/pi-ai": "^0.73.0",
77
+ "@mariozechner/pi-coding-agent": "^0.73.0",
78
+ "@mariozechner/pi-tui": "^0.73.0",
79
+ "citty": "^0.2.2",
80
+ "hono": "^4.12.18",
80
81
  "jsonc-parser": "^3.3.1",
81
- "zod": "^4.3.6"
82
+ "zod": "^4.4.3"
82
83
  },
83
84
  "devDependencies": {
84
- "@biomejs/biome": "^2.4.8",
85
- "@commitlint/cli": "^20.5.0",
86
- "@commitlint/config-conventional": "^20.5.0",
85
+ "@biomejs/biome": "^2.4.14",
86
+ "@commitlint/cli": "^20.5.3",
87
+ "@commitlint/config-conventional": "^20.5.3",
87
88
  "@limegrass/eslint-plugin-import-alias": "^1.6.1",
88
89
  "@semantic-release/changelog": "^6.0.3",
89
90
  "@semantic-release/git": "^10.0.1",
90
91
  "@semantic-release/github": "^12.0.6",
91
92
  "@semantic-release/npm": "^13.1.5",
92
- "@types/bun": "^1.3.11",
93
- "@types/node": "^25.5.0",
94
- "eslint-plugin-zod": "^3.5.0",
95
- "lefthook": "^2.1.4",
96
- "openai": "^6.26.0",
97
- "oxlint": "^1.56.0",
98
- "oxlint-tsgolint": "^0.17.1",
93
+ "@types/bun": "^1.3.13",
94
+ "@types/node": "^25.6.0",
95
+ "eslint-plugin-zod": "^3.12.0",
96
+ "lefthook": "^2.1.6",
97
+ "openai": "^6.36.0",
98
+ "oxlint": "^1.63.0",
99
+ "oxlint-tsgolint": "^0.22.1",
99
100
  "semantic-release": "^25.0.3",
100
- "tsdown": "^0.21.4",
101
- "typescript": "^5.9.3"
101
+ "tsdown": "^0.21.10",
102
+ "typebox": "^1.1.38",
103
+ "typescript": "^6.0.3"
102
104
  }
103
105
  }