proxitor 0.8.0 → 0.9.0-beta.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/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import V from "node:process";
3
+ import process$1 from "node:process";
4
4
  import os, { homedir } from "node:os";
5
5
  import * as tty$1 from "node:tty";
6
6
  import tty from "node:tty";
@@ -10,6 +10,7 @@ import { join, resolve, sep } from "node:path";
10
10
  import { STATUS_CODES, createServer } from "node:http";
11
11
  import { Http2ServerRequest, constants } from "node:http2";
12
12
  import { Readable } from "node:stream";
13
+ import { createHash } from "node:crypto";
13
14
  //#region \0rolldown/runtime.js
14
15
  var __defProp$1 = Object.defineProperty;
15
16
  var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
@@ -207,7 +208,7 @@ var init_ansi_styles = __esmMin((() => {
207
208
  }));
208
209
  //#endregion
209
210
  //#region node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
210
- function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : V.argv) {
211
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process$1.argv) {
211
212
  const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
212
213
  const position = argv.indexOf(prefix + flag);
213
214
  const terminatorPosition = argv.indexOf("--");
@@ -242,7 +243,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
242
243
  if (haveStream && !streamIsTTY && forceColor === void 0) return 0;
243
244
  const min = forceColor || 0;
244
245
  if (env$1.TERM === "dumb") return min;
245
- if (V.platform === "win32") {
246
+ if (process$1.platform === "win32") {
246
247
  const osRelease = os.release().split(".");
247
248
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
248
249
  return 1;
@@ -287,7 +288,7 @@ function createSupportsColor(stream, options = {}) {
287
288
  }
288
289
  var env$1, flagForceColor, supportsColor;
289
290
  var init_supports_color = __esmMin((() => {
290
- ({env: env$1} = V);
291
+ ({env: env$1} = process$1);
291
292
  if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) flagForceColor = 0;
292
293
  else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) flagForceColor = 1;
293
294
  supportsColor = {
@@ -7001,6 +7002,7 @@ const string$2 = (params) => {
7001
7002
  const integer = /^-?\d+$/;
7002
7003
  const number$2 = /^-?\d+(?:\.\d+)?$/;
7003
7004
  const boolean$1 = /^(?:true|false)$/i;
7005
+ const _null$2 = /^null$/i;
7004
7006
  const lowercase = /^[^A-Z]*$/;
7005
7007
  const uppercase = /^[^a-z]*$/;
7006
7008
  //#endregion
@@ -7813,6 +7815,22 @@ const $ZodBoolean = /*@__PURE__*/ $constructor("$ZodBoolean", (inst, def) => {
7813
7815
  return payload;
7814
7816
  };
7815
7817
  });
7818
+ const $ZodNull = /*@__PURE__*/ $constructor("$ZodNull", (inst, def) => {
7819
+ $ZodType.init(inst, def);
7820
+ inst._zod.pattern = _null$2;
7821
+ inst._zod.values = new Set([null]);
7822
+ inst._zod.parse = (payload, _ctx) => {
7823
+ const input = payload.value;
7824
+ if (input === null) return payload;
7825
+ payload.issues.push({
7826
+ expected: "null",
7827
+ code: "invalid_type",
7828
+ input,
7829
+ inst
7830
+ });
7831
+ return payload;
7832
+ };
7833
+ });
7816
7834
  const $ZodUnknown = /*@__PURE__*/ $constructor("$ZodUnknown", (inst, def) => {
7817
7835
  $ZodType.init(inst, def);
7818
7836
  inst._zod.parse = (payload) => payload;
@@ -8961,6 +8979,13 @@ function _boolean(Class, params) {
8961
8979
  });
8962
8980
  }
8963
8981
  // @__NO_SIDE_EFFECTS__
8982
+ function _null$1(Class, params) {
8983
+ return new Class({
8984
+ type: "null",
8985
+ ...normalizeParams(params)
8986
+ });
8987
+ }
8988
+ // @__NO_SIDE_EFFECTS__
8964
8989
  function _unknown(Class) {
8965
8990
  return new Class({ type: "unknown" });
8966
8991
  }
@@ -9183,7 +9208,7 @@ function initializeContext(params) {
9183
9208
  external: params?.external ?? void 0
9184
9209
  };
9185
9210
  }
9186
- function process$1(schema, ctx, _params = {
9211
+ function process$2(schema, ctx, _params = {
9187
9212
  path: [],
9188
9213
  schemaPath: []
9189
9214
  }) {
@@ -9220,7 +9245,7 @@ function process$1(schema, ctx, _params = {
9220
9245
  const parent = schema._zod.parent;
9221
9246
  if (parent) {
9222
9247
  if (!result.ref) result.ref = parent;
9223
- process$1(parent, ctx, params);
9248
+ process$2(parent, ctx, params);
9224
9249
  ctx.seen.get(parent).isParent = true;
9225
9250
  }
9226
9251
  }
@@ -9440,7 +9465,7 @@ const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
9440
9465
  ...params,
9441
9466
  processors
9442
9467
  });
9443
- process$1(schema, ctx);
9468
+ process$2(schema, ctx);
9444
9469
  extractDefs(ctx, schema);
9445
9470
  return finalize(ctx, schema);
9446
9471
  };
@@ -9452,7 +9477,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
9452
9477
  io,
9453
9478
  processors
9454
9479
  });
9455
- process$1(schema, ctx);
9480
+ process$2(schema, ctx);
9456
9481
  extractDefs(ctx, schema);
9457
9482
  return finalize(ctx, schema);
9458
9483
  };
@@ -9509,6 +9534,13 @@ const numberProcessor = (schema, ctx, _json, _params) => {
9509
9534
  const booleanProcessor = (_schema, _ctx, json, _params) => {
9510
9535
  json.type = "boolean";
9511
9536
  };
9537
+ const nullProcessor = (_schema, ctx, json, _params) => {
9538
+ if (ctx.target === "openapi-3.0") {
9539
+ json.type = "string";
9540
+ json.nullable = true;
9541
+ json.enum = [null];
9542
+ } else json.type = "null";
9543
+ };
9512
9544
  const neverProcessor = (_schema, _ctx, json, _params) => {
9513
9545
  json.not = {};
9514
9546
  };
@@ -9532,7 +9564,7 @@ const arrayProcessor = (schema, ctx, _json, params) => {
9532
9564
  if (typeof minimum === "number") json.minItems = minimum;
9533
9565
  if (typeof maximum === "number") json.maxItems = maximum;
9534
9566
  json.type = "array";
9535
- json.items = process$1(def.element, ctx, {
9567
+ json.items = process$2(def.element, ctx, {
9536
9568
  ...params,
9537
9569
  path: [...params.path, "items"]
9538
9570
  });
@@ -9543,7 +9575,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
9543
9575
  json.type = "object";
9544
9576
  json.properties = {};
9545
9577
  const shape = def.shape;
9546
- for (const key in shape) json.properties[key] = process$1(shape[key], ctx, {
9578
+ for (const key in shape) json.properties[key] = process$2(shape[key], ctx, {
9547
9579
  ...params,
9548
9580
  path: [
9549
9581
  ...params.path,
@@ -9561,7 +9593,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
9561
9593
  if (def.catchall?._zod.def.type === "never") json.additionalProperties = false;
9562
9594
  else if (!def.catchall) {
9563
9595
  if (ctx.io === "output") json.additionalProperties = false;
9564
- } else if (def.catchall) json.additionalProperties = process$1(def.catchall, ctx, {
9596
+ } else if (def.catchall) json.additionalProperties = process$2(def.catchall, ctx, {
9565
9597
  ...params,
9566
9598
  path: [...params.path, "additionalProperties"]
9567
9599
  });
@@ -9569,7 +9601,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
9569
9601
  const unionProcessor = (schema, ctx, json, params) => {
9570
9602
  const def = schema._zod.def;
9571
9603
  const isExclusive = def.inclusive === false;
9572
- const options = def.options.map((x, i) => process$1(x, ctx, {
9604
+ const options = def.options.map((x, i) => process$2(x, ctx, {
9573
9605
  ...params,
9574
9606
  path: [
9575
9607
  ...params.path,
@@ -9582,7 +9614,7 @@ const unionProcessor = (schema, ctx, json, params) => {
9582
9614
  };
9583
9615
  const intersectionProcessor = (schema, ctx, json, params) => {
9584
9616
  const def = schema._zod.def;
9585
- const a = process$1(def.left, ctx, {
9617
+ const a = process$2(def.left, ctx, {
9586
9618
  ...params,
9587
9619
  path: [
9588
9620
  ...params.path,
@@ -9590,7 +9622,7 @@ const intersectionProcessor = (schema, ctx, json, params) => {
9590
9622
  0
9591
9623
  ]
9592
9624
  });
9593
- const b = process$1(def.right, ctx, {
9625
+ const b = process$2(def.right, ctx, {
9594
9626
  ...params,
9595
9627
  path: [
9596
9628
  ...params.path,
@@ -9608,7 +9640,7 @@ const recordProcessor = (schema, ctx, _json, params) => {
9608
9640
  const keyType = def.keyType;
9609
9641
  const patterns = keyType._zod.bag?.patterns;
9610
9642
  if (def.mode === "loose" && patterns && patterns.size > 0) {
9611
- const valueSchema = process$1(def.valueType, ctx, {
9643
+ const valueSchema = process$2(def.valueType, ctx, {
9612
9644
  ...params,
9613
9645
  path: [
9614
9646
  ...params.path,
@@ -9619,11 +9651,11 @@ const recordProcessor = (schema, ctx, _json, params) => {
9619
9651
  json.patternProperties = {};
9620
9652
  for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
9621
9653
  } else {
9622
- if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process$1(def.keyType, ctx, {
9654
+ if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process$2(def.keyType, ctx, {
9623
9655
  ...params,
9624
9656
  path: [...params.path, "propertyNames"]
9625
9657
  });
9626
- json.additionalProperties = process$1(def.valueType, ctx, {
9658
+ json.additionalProperties = process$2(def.valueType, ctx, {
9627
9659
  ...params,
9628
9660
  path: [...params.path, "additionalProperties"]
9629
9661
  });
@@ -9636,7 +9668,7 @@ const recordProcessor = (schema, ctx, _json, params) => {
9636
9668
  };
9637
9669
  const nullableProcessor = (schema, ctx, json, params) => {
9638
9670
  const def = schema._zod.def;
9639
- const inner = process$1(def.innerType, ctx, params);
9671
+ const inner = process$2(def.innerType, ctx, params);
9640
9672
  const seen = ctx.seen.get(schema);
9641
9673
  if (ctx.target === "openapi-3.0") {
9642
9674
  seen.ref = def.innerType;
@@ -9645,27 +9677,27 @@ const nullableProcessor = (schema, ctx, json, params) => {
9645
9677
  };
9646
9678
  const nonoptionalProcessor = (schema, ctx, _json, params) => {
9647
9679
  const def = schema._zod.def;
9648
- process$1(def.innerType, ctx, params);
9680
+ process$2(def.innerType, ctx, params);
9649
9681
  const seen = ctx.seen.get(schema);
9650
9682
  seen.ref = def.innerType;
9651
9683
  };
9652
9684
  const defaultProcessor = (schema, ctx, json, params) => {
9653
9685
  const def = schema._zod.def;
9654
- process$1(def.innerType, ctx, params);
9686
+ process$2(def.innerType, ctx, params);
9655
9687
  const seen = ctx.seen.get(schema);
9656
9688
  seen.ref = def.innerType;
9657
9689
  json.default = JSON.parse(JSON.stringify(def.defaultValue));
9658
9690
  };
9659
9691
  const prefaultProcessor = (schema, ctx, json, params) => {
9660
9692
  const def = schema._zod.def;
9661
- process$1(def.innerType, ctx, params);
9693
+ process$2(def.innerType, ctx, params);
9662
9694
  const seen = ctx.seen.get(schema);
9663
9695
  seen.ref = def.innerType;
9664
9696
  if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
9665
9697
  };
9666
9698
  const catchProcessor = (schema, ctx, json, params) => {
9667
9699
  const def = schema._zod.def;
9668
- process$1(def.innerType, ctx, params);
9700
+ process$2(def.innerType, ctx, params);
9669
9701
  const seen = ctx.seen.get(schema);
9670
9702
  seen.ref = def.innerType;
9671
9703
  let catchValue;
@@ -9680,20 +9712,20 @@ const pipeProcessor = (schema, ctx, _json, params) => {
9680
9712
  const def = schema._zod.def;
9681
9713
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
9682
9714
  const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
9683
- process$1(innerType, ctx, params);
9715
+ process$2(innerType, ctx, params);
9684
9716
  const seen = ctx.seen.get(schema);
9685
9717
  seen.ref = innerType;
9686
9718
  };
9687
9719
  const readonlyProcessor = (schema, ctx, json, params) => {
9688
9720
  const def = schema._zod.def;
9689
- process$1(def.innerType, ctx, params);
9721
+ process$2(def.innerType, ctx, params);
9690
9722
  const seen = ctx.seen.get(schema);
9691
9723
  seen.ref = def.innerType;
9692
9724
  json.readOnly = true;
9693
9725
  };
9694
9726
  const optionalProcessor = (schema, ctx, _json, params) => {
9695
9727
  const def = schema._zod.def;
9696
- process$1(def.innerType, ctx, params);
9728
+ process$2(def.innerType, ctx, params);
9697
9729
  const seen = ctx.seen.get(schema);
9698
9730
  seen.ref = def.innerType;
9699
9731
  };
@@ -10178,6 +10210,14 @@ const ZodBoolean = /*@__PURE__*/ $constructor("ZodBoolean", (inst, def) => {
10178
10210
  function boolean(params) {
10179
10211
  return /* @__PURE__ */ _boolean(ZodBoolean, params);
10180
10212
  }
10213
+ const ZodNull = /*@__PURE__*/ $constructor("ZodNull", (inst, def) => {
10214
+ $ZodNull.init(inst, def);
10215
+ ZodType.init(inst, def);
10216
+ inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
10217
+ });
10218
+ function _null(params) {
10219
+ return /* @__PURE__ */ _null$1(ZodNull, params);
10220
+ }
10181
10221
  const ZodUnknown = /*@__PURE__*/ $constructor("ZodUnknown", (inst, def) => {
10182
10222
  $ZodUnknown.init(inst, def);
10183
10223
  ZodType.init(inst, def);
@@ -10541,6 +10581,7 @@ function superRefine(fn, params) {
10541
10581
  }
10542
10582
  //#endregion
10543
10583
  //#region src/config-schema.ts
10584
+ const OPENROUTER_API_URL = "https://openrouter.ai/api";
10544
10585
  const percentileCutoffsSchema = object({
10545
10586
  p50: number$1().positive().optional(),
10546
10587
  p75: number$1().positive().optional(),
@@ -10580,15 +10621,23 @@ const providerConfigSchema = object({
10580
10621
  preferredMinThroughput: union([number$1().positive(), percentileCutoffsSchema]).optional(),
10581
10622
  preferredMaxLatency: union([number$1().positive(), percentileCutoffsSchema]).optional()
10582
10623
  }).strict();
10624
+ const triStateSchema = _enum([
10625
+ "auto",
10626
+ "always",
10627
+ "never"
10628
+ ]);
10583
10629
  const modelOverrideSchema = object({
10584
10630
  provider: providerConfigSchema.optional(),
10585
- headers: record(string$1(), string$1()).optional()
10631
+ headers: record(string$1(), string$1()).optional(),
10632
+ cacheControl: triStateSchema.optional(),
10633
+ cacheControlTtl: union([_enum(["5m", "1h"]), _null()]).optional(),
10634
+ sessionId: triStateSchema.optional()
10586
10635
  }).strict();
10587
10636
  const proxyConfigSchema = object({
10588
10637
  host: string$1().min(1).default("0.0.0.0"),
10589
10638
  port: number$1().int().min(1).max(65535).default(8828),
10590
10639
  openrouterKey: string$1().default(""),
10591
- openrouterBaseUrl: string$1().url().default("https://openrouter.ai/api/v1"),
10640
+ openrouterBaseUrl: string$1().url().default(OPENROUTER_API_URL),
10592
10641
  openrouterDataUrl: string$1().url().optional(),
10593
10642
  authType: _enum(["bearer", "oauth"]).default("bearer"),
10594
10643
  verbose: boolean().default(false),
@@ -10597,6 +10646,9 @@ const proxyConfigSchema = object({
10597
10646
  attributionReferer: string$1().min(1).default("https://github.com/neiromaster/proxitor"),
10598
10647
  attributionTitle: string$1().min(1).default("proxitor"),
10599
10648
  headers: record(string$1(), string$1()).optional(),
10649
+ cacheControl: triStateSchema.default("auto"),
10650
+ cacheControlTtl: _enum(["5m", "1h"]).optional(),
10651
+ sessionId: triStateSchema.default("auto"),
10600
10652
  modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
10601
10653
  }).strict();
10602
10654
  const DEFAULTS = proxyConfigSchema.parse({});
@@ -10628,15 +10680,6 @@ function toArray(value) {
10628
10680
  const arr = Array.isArray(value) ? [...value] : [value];
10629
10681
  return arr.length > 0 ? arr : void 0;
10630
10682
  }
10631
- /** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */
10632
- function tryParseBody(raw) {
10633
- if (raw.byteLength === 0) return void 0;
10634
- try {
10635
- return JSON.parse(new TextDecoder().decode(raw));
10636
- } catch {
10637
- return;
10638
- }
10639
- }
10640
10683
  //#endregion
10641
10684
  //#region src/config.ts
10642
10685
  const ARRAY_FIELDS = [
@@ -10716,27 +10759,48 @@ function matchScore(pattern, modelName) {
10716
10759
  function resolveModelConfig(config, modelName) {
10717
10760
  const result = {
10718
10761
  provider: config.provider,
10719
- headers: config.headers ? { ...config.headers } : void 0
10762
+ headers: config.headers ? { ...config.headers } : void 0,
10763
+ cacheControl: config.cacheControl,
10764
+ cacheControlTtl: config.cacheControlTtl,
10765
+ sessionId: config.sessionId
10720
10766
  };
10721
10767
  if (!modelName || !config.modelOverrides) return result;
10768
+ const bestPattern = findBestMatch(Object.keys(config.modelOverrides), modelName);
10769
+ if (bestPattern) applyOverride(result, config.modelOverrides[bestPattern]);
10770
+ return result;
10771
+ }
10772
+ function findBestMatch(patterns, modelName) {
10722
10773
  let bestPattern = null;
10723
10774
  let bestScore = -1;
10724
- for (const pattern of Object.keys(config.modelOverrides)) {
10775
+ for (const pattern of patterns) {
10725
10776
  const score = matchScore(pattern, modelName);
10726
10777
  if (score > bestScore) {
10727
10778
  bestScore = score;
10728
10779
  bestPattern = pattern;
10729
10780
  }
10730
10781
  }
10731
- if (bestPattern) {
10732
- const override = config.modelOverrides[bestPattern];
10733
- if (override?.provider !== void 0) result.provider = override.provider;
10734
- if (override?.headers) result.headers = {
10735
- ...result.headers ?? {},
10736
- ...override.headers
10737
- };
10738
- }
10739
- return result;
10782
+ return bestPattern;
10783
+ }
10784
+ function applyOverride(result, override) {
10785
+ if (!override) return;
10786
+ if (override.provider !== void 0) result.provider = override.provider;
10787
+ if (override.headers) result.headers = {
10788
+ ...result.headers ?? {},
10789
+ ...override.headers
10790
+ };
10791
+ if (override.cacheControl !== void 0) result.cacheControl = override.cacheControl;
10792
+ if (override.cacheControlTtl === null) result.cacheControlTtl = void 0;
10793
+ else if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
10794
+ if (override.sessionId !== void 0) result.sessionId = override.sessionId;
10795
+ }
10796
+ /**
10797
+ * Throw if a URL ends with /v1 — a common misconfiguration after the URL routing change.
10798
+ * Paths like /v1/chat/completions are now forwarded as-is, so including /v1 in the base
10799
+ * URL would produce doubled paths like /v1/v1/chat/completions.
10800
+ */
10801
+ function throwIfV1Suffix(url, field) {
10802
+ const { pathname } = new URL(url);
10803
+ if (pathname.endsWith("/v1") || pathname.endsWith("/v1/")) throw new Error(`${field} "${url}" ends with /v1 — paths are now forwarded as-is, so this would produce doubled paths like /v1/v1/chat/completions. Remove the /v1 suffix (use "${url.replace(/\/v1\/?$/, "")}")`);
10740
10804
  }
10741
10805
  async function loadConfig(options) {
10742
10806
  let fileConfig = {};
@@ -10756,6 +10820,8 @@ async function loadConfig(options) {
10756
10820
  const result = proxyConfigSchema.safeParse(merged);
10757
10821
  if (!result.success) throw new ConfigValidationError("(merged config)", result.error);
10758
10822
  if (!result.data.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
10823
+ throwIfV1Suffix(result.data.openrouterBaseUrl, "openrouterBaseUrl");
10824
+ if (result.data.openrouterDataUrl) throwIfV1Suffix(result.data.openrouterDataUrl, "openrouterDataUrl");
10759
10825
  return result.data;
10760
10826
  }
10761
10827
  function getXdgConfigDir() {
@@ -11648,9 +11714,9 @@ function stringWidth$1(string, options = {}) {
11648
11714
  return width;
11649
11715
  }
11650
11716
  function isUnicodeSupported() {
11651
- const { env } = V;
11717
+ const { env } = process$1;
11652
11718
  const { TERM, TERM_PROGRAM } = env;
11653
- if (V.platform !== "win32") return TERM !== "linux";
11719
+ if (process$1.platform !== "win32") return TERM !== "linux";
11654
11720
  return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
11655
11721
  }
11656
11722
  const TYPE_COLOR_MAP = {
@@ -11810,7 +11876,7 @@ var OpenRouterClient = class {
11810
11876
  };
11811
11877
  //#endregion
11812
11878
  //#region src/openrouter/data-client.ts
11813
- const OPENROUTER_FALLBACK_URL = "https://openrouter.ai/api/v1";
11879
+ const OPENROUTER_FALLBACK_URL = OPENROUTER_API_URL;
11814
11880
  function isValidProvidersResponse(data) {
11815
11881
  return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
11816
11882
  }
@@ -11824,7 +11890,7 @@ function isValidEndpointsResponse(data) {
11824
11890
  * Client for fetching provider/model data with automatic fallback to OpenRouter.
11825
11891
  *
11826
11892
  * When the primary API (openrouterDataUrl or openrouterBaseUrl) doesn't support
11827
- * OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api/v1
11893
+ * OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api
11828
11894
  * which hosts public, unauthenticated endpoints for /providers, /models, etc.
11829
11895
  */
11830
11896
  var OpenRouterDataClient = class {
@@ -11840,13 +11906,13 @@ var OpenRouterDataClient = class {
11840
11906
  this.onFallback = config.onFallback;
11841
11907
  }
11842
11908
  async fetchProviders() {
11843
- return (await this.withFallback("/providers", () => this.primaryClient.get("/providers"), isValidProvidersResponse)).data.data;
11909
+ return (await this.withFallback("/v1/providers", () => this.primaryClient.get("/v1/providers"), isValidProvidersResponse)).data.data;
11844
11910
  }
11845
11911
  async fetchModels() {
11846
- return (await this.withFallback("/models", () => this.primaryClient.get("/models"), isValidModelsResponse)).data.data;
11912
+ return (await this.withFallback("/v1/models", () => this.primaryClient.get("/v1/models"), isValidModelsResponse)).data.data;
11847
11913
  }
11848
11914
  async fetchModelEndpoints(author, slug) {
11849
- const path = `/models/${author}/${slug}/endpoints`;
11915
+ const path = `/v1/models/${author}/${slug}/endpoints`;
11850
11916
  return (await this.withFallback(path, () => this.primaryClient.get(path), isValidEndpointsResponse)).data.data.endpoints ?? [];
11851
11917
  }
11852
11918
  /**
@@ -12849,7 +12915,7 @@ var compose = (middleware, onError, onNotFound) => {
12849
12915
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
12850
12916
  //#endregion
12851
12917
  //#region node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/body.js
12852
- var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
12918
+ var parseBody$1 = async (request, options = /* @__PURE__ */ Object.create(null)) => {
12853
12919
  const { all = false, dot = false } = options;
12854
12920
  const contentType = (request instanceof HonoRequest ? request.raw.headers : request.headers).get("Content-Type");
12855
12921
  if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) return parseFormData(request, {
@@ -13136,7 +13202,7 @@ var HonoRequest = class {
13136
13202
  return headerData;
13137
13203
  }
13138
13204
  async parseBody(options) {
13139
- return parseBody(this, options);
13205
+ return parseBody$1(this, options);
13140
13206
  }
13141
13207
  #cachedBody = (key) => {
13142
13208
  const { bodyCache, raw } = this;
@@ -14590,6 +14656,9 @@ var Hono = class extends Hono$1 {
14590
14656
  }
14591
14657
  };
14592
14658
  //#endregion
14659
+ //#region node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/helper/factory/index.js
14660
+ var createMiddleware = (middleware) => middleware;
14661
+ //#endregion
14593
14662
  //#region src/proxy/headers.ts
14594
14663
  const HOP_BY_HOP = new Set([
14595
14664
  "connection",
@@ -14606,7 +14675,9 @@ const STRIP_REQUEST = new Set([
14606
14675
  "authorization",
14607
14676
  "x-api-key",
14608
14677
  "host",
14609
- "content-length"
14678
+ "content-length",
14679
+ "x-claude-code-session-id",
14680
+ "x-session-id"
14610
14681
  ]);
14611
14682
  /** Headers to strip from upstream response before forwarding */
14612
14683
  const STRIP_RESPONSE = new Set(["content-length", "content-encoding"]);
@@ -14621,17 +14692,6 @@ function filterHeaders(incoming, blocklist) {
14621
14692
  }
14622
14693
  return headers;
14623
14694
  }
14624
- /** Build request headers for upstream fetch */
14625
- function buildRequestHeaders(incoming, config, inject, extraHeaders) {
14626
- const headers = filterHeaders(incoming, STRIP_REQUEST);
14627
- headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
14628
- headers["HTTP-Referer"] = config.attributionReferer;
14629
- headers["X-OpenRouter-Title"] = config.attributionTitle;
14630
- headers["Accept-Encoding"] = "identity";
14631
- if (extraHeaders) Object.assign(headers, extraHeaders);
14632
- if (inject) headers["Content-Type"] = "application/json";
14633
- return headers;
14634
- }
14635
14695
  /** Filter response headers and add SSE-friendly defaults */
14636
14696
  function buildResponseHeaders(from) {
14637
14697
  const headers = filterHeaders(from, STRIP_RESPONSE);
@@ -14640,6 +14700,49 @@ function buildResponseHeaders(from) {
14640
14700
  return headers;
14641
14701
  }
14642
14702
  //#endregion
14703
+ //#region src/proxy/middleware/build-upstream-req.ts
14704
+ const PROTO_POLLUTION_KEYS = new Set([
14705
+ "__proto__",
14706
+ "constructor",
14707
+ "prototype"
14708
+ ]);
14709
+ function resolveForwardBody(c) {
14710
+ if (c.var.bodyMutated && c.var.parsedBody) try {
14711
+ return new TextEncoder().encode(JSON.stringify(c.var.parsedBody)).buffer;
14712
+ } catch (err) {
14713
+ logger.warn(withReq(c.var.reqId, `Failed to re-serialize mutated body (${err instanceof Error ? err.message : "unknown"}); forwarding raw body as-is`));
14714
+ return c.var.rawBody;
14715
+ }
14716
+ return c.var.rawBody;
14717
+ }
14718
+ function applyProxyHeaders(headers, config) {
14719
+ headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
14720
+ headers["HTTP-Referer"] = config.attributionReferer;
14721
+ headers["X-OpenRouter-Title"] = config.attributionTitle;
14722
+ headers["Accept-Encoding"] = "identity";
14723
+ }
14724
+ function applyExtraHeaders(headers, extraHeaders) {
14725
+ if (!extraHeaders) return;
14726
+ for (const [key, value] of Object.entries(extraHeaders)) {
14727
+ if (PROTO_POLLUTION_KEYS.has(key)) continue;
14728
+ headers[key] = value;
14729
+ }
14730
+ }
14731
+ function forceJsonContentType(headers) {
14732
+ for (const key of Object.keys(headers)) if (key.toLowerCase() === "content-type") delete headers[key];
14733
+ headers["Content-Type"] = "application/json";
14734
+ }
14735
+ const buildUpstreamReq = createMiddleware(async (c, next) => {
14736
+ c.set("forwardBody", resolveForwardBody(c));
14737
+ const headers = filterHeaders(c.req.raw.headers, STRIP_REQUEST);
14738
+ applyProxyHeaders(headers, c.var.config);
14739
+ applyExtraHeaders(headers, c.var.resolvedConfig.headers);
14740
+ if (c.var.effectiveSessionId !== void 0) headers["x-session-id"] = c.var.effectiveSessionId;
14741
+ if (c.var.bodyMutated) forceJsonContentType(headers);
14742
+ c.set("upstreamHeaders", headers);
14743
+ await next();
14744
+ });
14745
+ //#endregion
14643
14746
  //#region src/proxy/cache-logging.ts
14644
14747
  function extractCacheUsage(bodyText) {
14645
14748
  try {
@@ -14754,130 +14857,10 @@ function buildUpstreamResponseWithLogging(upstream, method, reqId) {
14754
14857
  });
14755
14858
  }
14756
14859
  //#endregion
14757
- //#region src/proxy/inject.ts
14758
- /** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */
14759
- function extractModel(rawBody) {
14760
- const json = tryParseBody(rawBody);
14761
- return typeof json?.model === "string" ? json.model : void 0;
14762
- }
14763
- /** Inject provider routing into request body, always overwriting existing value */
14764
- function injectProvider(rawBody, providerRouting) {
14765
- if (rawBody.byteLength === 0) throw new Error("Request body is empty; cannot inject provider");
14766
- let json;
14767
- try {
14768
- json = JSON.parse(new TextDecoder().decode(rawBody));
14769
- } catch (parseError) {
14770
- throw new Error("Request body is not valid JSON; cannot inject provider", { cause: parseError });
14771
- }
14772
- const modified = {
14773
- ...json,
14774
- provider: providerRouting
14775
- };
14776
- return new TextEncoder().encode(JSON.stringify(modified)).buffer;
14777
- }
14778
- //#endregion
14779
- //#region src/proxy/paths.ts
14860
+ //#region src/proxy/utils/error.ts
14780
14861
  /**
14781
- * Paths where provider routing is injected into the request body.
14782
- * All three are OpenRouter-supported endpoints:
14783
- * /v1/chat/completions — OpenAI Chat Completions
14784
- * /v1/responses — OpenAI Responses API
14785
- * /v1/messages — Anthropic Messages API
14786
- */
14787
- const INJECT_PATHS = new Set([
14788
- "/v1/chat/completions",
14789
- "/v1/responses",
14790
- "/v1/messages"
14791
- ]);
14792
- /** Check if this request should have provider routing injected */
14793
- function shouldInject(method, path) {
14794
- return method === "POST" && INJECT_PATHS.has(path);
14795
- }
14796
- /** Strip /v1 prefix from path: /v1/chat/completions → /chat/completions */
14797
- function toUpstreamPath(pathname) {
14798
- if (pathname.startsWith("/v1")) return pathname.slice(3);
14799
- return pathname;
14800
- }
14801
- /** Build full upstream URL from request and config */
14802
- function buildUpstreamUrl(requestUrl, config) {
14803
- const { pathname } = new URL(requestUrl);
14804
- return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`;
14805
- }
14806
- //#endregion
14807
- //#region src/proxy.ts
14808
- function readRequestBody(method, raw, inject, providerRouting) {
14809
- if (["GET", "HEAD"].includes(method)) return void 0;
14810
- if (inject) return injectProvider(raw, providerRouting);
14811
- return raw.byteLength > 0 ? raw : void 0;
14812
- }
14813
- async function fetchUpstream(url, method, headers, body, signal) {
14814
- return fetch(url, {
14815
- method,
14816
- headers,
14817
- body,
14818
- signal,
14819
- duplex: body ? "half" : void 0
14820
- });
14821
- }
14822
- async function readRawBody(request, reqId) {
14823
- try {
14824
- return {
14825
- ok: true,
14826
- body: await request.arrayBuffer()
14827
- };
14828
- } catch (err) {
14829
- const message = err instanceof Error ? err.message : "Failed to read request body";
14830
- logger.error(withReq(reqId, message));
14831
- return {
14832
- ok: false,
14833
- response: Response.json({ error: {
14834
- message,
14835
- type: "proxy_request_error"
14836
- } }, { status: 400 })
14837
- };
14838
- }
14839
- }
14840
- function resolveRequest(rawBody, config, method, path, reqId) {
14841
- const modelName = extractModel(rawBody);
14842
- const resolved = resolveModelConfig(config, modelName);
14843
- const providerRouting = buildProviderRouting(resolved.provider);
14844
- const inject = shouldInject(method, path) && providerRouting !== void 0;
14845
- let body;
14846
- try {
14847
- body = readRequestBody(method, rawBody, inject, providerRouting);
14848
- } catch (err) {
14849
- const message = err instanceof Error ? err.message : "Failed to process request body";
14850
- logger.error(withReq(reqId, message));
14851
- return {
14852
- inject,
14853
- body: void 0,
14854
- modelName,
14855
- headers: resolved.headers,
14856
- error: new Response(JSON.stringify({ error: {
14857
- message,
14858
- type: "proxy_request_error"
14859
- } }), {
14860
- status: 400,
14861
- headers: { "Content-Type": "application/json" }
14862
- })
14863
- };
14864
- }
14865
- return {
14866
- inject,
14867
- body,
14868
- modelName,
14869
- headers: resolved.headers
14870
- };
14871
- }
14872
- /**
14873
- * Extract a readable error detail from an upstream response body.
14874
- *
14875
14862
  * OpenRouter error format:
14876
- * { error: { code: 400, message: "...", metadata: { raw: "...", provider_name: "..." } } }
14877
- *
14878
- * - `error.message` — human-readable summary
14879
- * - `error.metadata.provider_name` — which provider caused it (null = OpenRouter itself)
14880
- * - `error.metadata.raw` — the original provider error (most specific cause)
14863
+ * { error: { code, message, metadata: { raw, provider_name } } }
14881
14864
  */
14882
14865
  function formatMetadata(meta) {
14883
14866
  const parts = [];
@@ -14904,41 +14887,315 @@ function extractErrorDetail(bodyText) {
14904
14887
  } catch {}
14905
14888
  return bodyText;
14906
14889
  }
14907
- async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt, reqId) {
14908
- let upstream;
14909
- try {
14910
- upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal);
14911
- } catch (err) {
14912
- if (err instanceof DOMException && err.name === "AbortError") {
14913
- logger.warn(withReq(reqId, `Aborted: ${method} ${path}`));
14914
- return new Response(null, { status: 499 });
14915
- }
14916
- logger.error(withReq(reqId, "Upstream fetch error:"), err);
14890
+ //#endregion
14891
+ //#region src/proxy/middleware/forward-request.ts
14892
+ function buildErrorResponse(err, ctx) {
14893
+ if (err instanceof TypeError) {
14894
+ logger.error(withReq(ctx.reqId, "Upstream fetch error:"), err);
14917
14895
  return Response.json({ error: {
14918
14896
  message: "Proxy failed to reach upstream",
14919
14897
  type: "proxy_upstream_error"
14920
14898
  } }, { status: 502 });
14921
14899
  }
14922
- if (upstream.status >= 400) {
14923
- const bodyText = await upstream.text();
14924
- const detail = extractErrorDetail(bodyText);
14925
- const truncated = detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
14926
- (upstream.status >= 500 ? logger.error : logger.warn)(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms): ${truncated}`));
14927
- const responseHeaders = buildResponseHeaders(upstream.headers);
14928
- if (method === "HEAD") return new Response(null, {
14929
- status: upstream.status,
14930
- headers: responseHeaders
14931
- });
14932
- return new Response(bodyText, {
14933
- status: upstream.status,
14934
- headers: responseHeaders
14900
+ if (err instanceof DOMException && err.name === "AbortError") {
14901
+ logger.warn(withReq(ctx.reqId, `Aborted: ${ctx.method} ${ctx.path}`));
14902
+ return new Response(null, { status: 499 });
14903
+ }
14904
+ throw err;
14905
+ }
14906
+ async function buildUpstreamErrorResponse(upstream, ctx) {
14907
+ const bodyText = await upstream.text();
14908
+ const detail = extractErrorDetail(bodyText);
14909
+ const truncated = detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
14910
+ (upstream.status >= 500 ? logger.error : logger.warn)(withReq(ctx.reqId, `${ctx.method} ${ctx.path} ← ${upstream.status} (${Date.now() - ctx.startedAt}ms): ${truncated}`));
14911
+ const responseHeaders = buildResponseHeaders(upstream.headers);
14912
+ if (ctx.method === "HEAD") return new Response(null, {
14913
+ status: upstream.status,
14914
+ headers: responseHeaders
14915
+ });
14916
+ return new Response(bodyText, {
14917
+ status: upstream.status,
14918
+ headers: responseHeaders
14919
+ });
14920
+ }
14921
+ const forwardRequest = createMiddleware(async (c) => {
14922
+ const { upstreamUrl, forwardBody, upstreamHeaders, reqId, path, startedAt, method } = c.var;
14923
+ const ctx = {
14924
+ reqId,
14925
+ method,
14926
+ path,
14927
+ startedAt,
14928
+ upstreamShort: upstreamUrl.replace(/^https?:\/\//, ""),
14929
+ modelLog: c.var.modelName ? ` model=${c.var.modelName}` : "",
14930
+ bodyMutated: c.var.bodyMutated
14931
+ };
14932
+ const controller = new AbortController();
14933
+ const onClientAbort = () => controller.abort();
14934
+ c.req.raw.signal.addEventListener("abort", onClientAbort);
14935
+ logger.info(withReq(reqId, `${method} ${path} → ${ctx.upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${ctx.modelLog}`));
14936
+ let upstream;
14937
+ try {
14938
+ upstream = await fetch(upstreamUrl, {
14939
+ method,
14940
+ headers: upstreamHeaders,
14941
+ body: forwardBody,
14942
+ signal: controller.signal,
14943
+ ...forwardBody ? { duplex: "half" } : {}
14935
14944
  });
14945
+ } catch (err) {
14946
+ return buildErrorResponse(err, ctx);
14947
+ } finally {
14948
+ c.req.raw.signal.removeEventListener("abort", onClientAbort);
14936
14949
  }
14950
+ if (upstream.status >= 400) return buildUpstreamErrorResponse(upstream, ctx);
14937
14951
  logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
14938
14952
  return buildUpstreamResponseWithLogging(upstream, method, reqId);
14953
+ });
14954
+ //#endregion
14955
+ //#region src/proxy/paths.ts
14956
+ const ENDPOINT_MAP = {
14957
+ "/v1/chat/completions": "chat-completions",
14958
+ "/v1/responses": "responses",
14959
+ "/v1/messages": "messages"
14960
+ };
14961
+ const INJECT_PATHS = new Set(Object.keys(ENDPOINT_MAP));
14962
+ /** Endpoints that are natively Anthropic (cache_control is always safe regardless of model). */
14963
+ const ANTHROPIC_NATIVE_ENDPOINTS = new Set(["messages", "responses"]);
14964
+ function classifyEndpoint(pathname) {
14965
+ return ENDPOINT_MAP[pathname] ?? "other";
14966
+ }
14967
+ function buildUpstreamUrl(pathname, config) {
14968
+ return `${config.openrouterBaseUrl}${pathname}`;
14969
+ }
14970
+ //#endregion
14971
+ //#region src/proxy/utils/model.ts
14972
+ function isAnthropicModel(modelName) {
14973
+ const lower = modelName.toLowerCase();
14974
+ return lower.startsWith("anthropic/claude") || lower.startsWith("claude-");
14975
+ }
14976
+ //#endregion
14977
+ //#region src/proxy/utils/cache-control.ts
14978
+ function isAnthropicEndpoint(modelName, path) {
14979
+ const endpoint = classifyEndpoint(path);
14980
+ return ANTHROPIC_NATIVE_ENDPOINTS.has(endpoint) || isAnthropicModel(modelName ?? "");
14939
14981
  }
14982
+ function shouldInjectCacheControl(mode, modelName, path) {
14983
+ if (mode === "never") return false;
14984
+ if (mode === "always") return true;
14985
+ return isAnthropicEndpoint(modelName, path);
14986
+ }
14987
+ const TTL_SECONDS = {
14988
+ "5m": 300,
14989
+ "1h": 3600
14990
+ };
14991
+ /**
14992
+ * Build cache_control value for injection.
14993
+ * Merges existing cache_control with configured TTL.
14994
+ * If TTL is configured and the endpoint is Anthropic, it always overrides.
14995
+ */
14996
+ function buildCacheControl(existing, ttl, isAnthropic) {
14997
+ const result = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : { type: "ephemeral" };
14998
+ if (!("type" in result)) result.type = "ephemeral";
14999
+ if (ttl && isAnthropic) result.ttl = TTL_SECONDS[ttl];
15000
+ return result;
15001
+ }
15002
+ //#endregion
15003
+ //#region src/proxy/middleware/inject-cache-control.ts
15004
+ const injectCacheControl = createMiddleware(async (c, next) => {
15005
+ const resolved = c.var.resolvedConfig;
15006
+ const parsedBody = c.var.parsedBody;
15007
+ if (!parsedBody) {
15008
+ await next();
15009
+ return;
15010
+ }
15011
+ if (shouldInjectCacheControl(resolved.cacheControl, c.var.modelName, c.var.path)) {
15012
+ const isAnthropic = isAnthropicEndpoint(c.var.modelName, c.var.path);
15013
+ parsedBody.cache_control = buildCacheControl(parsedBody.cache_control, resolved.cacheControlTtl, isAnthropic);
15014
+ c.set("bodyMutated", true);
15015
+ }
15016
+ await next();
15017
+ });
15018
+ //#endregion
15019
+ //#region src/proxy/middleware/inject-provider.ts
15020
+ const injectProvider = createMiddleware(async (c, next) => {
15021
+ const resolved = c.var.resolvedConfig;
15022
+ const parsedBody = c.var.parsedBody;
15023
+ if (!parsedBody || !resolved.provider) {
15024
+ await next();
15025
+ return;
15026
+ }
15027
+ const providerRouting = buildProviderRouting(resolved.provider);
15028
+ if (providerRouting !== void 0) {
15029
+ parsedBody.provider = providerRouting;
15030
+ c.set("parsedBody", parsedBody);
15031
+ c.set("bodyMutated", true);
15032
+ }
15033
+ await next();
15034
+ });
15035
+ //#endregion
15036
+ //#region src/proxy/utils/session-id.ts
15037
+ const PROXY_SESSION_ID = crypto.randomUUID();
15038
+ function extractConversationFingerprint(parsedBody, path) {
15039
+ let system;
15040
+ let user;
15041
+ switch (classifyEndpoint(path)) {
15042
+ case "responses":
15043
+ system = parsedBody.instructions;
15044
+ user = parsedBody.input;
15045
+ break;
15046
+ case "messages": {
15047
+ system = parsedBody.system;
15048
+ const messages = parsedBody.messages;
15049
+ if (Array.isArray(messages)) user = messages.find((m) => m.role === "user" && m.content != null)?.content;
15050
+ break;
15051
+ }
15052
+ default: {
15053
+ const messages = parsedBody.messages;
15054
+ if (Array.isArray(messages)) {
15055
+ const firstSystem = messages.find((m) => (m.role === "system" || m.role === "developer") && m.content != null);
15056
+ const firstUser = messages.find((m) => m.role === "user" && m.content != null);
15057
+ system = firstSystem?.content;
15058
+ user = firstUser?.content;
15059
+ }
15060
+ }
15061
+ }
15062
+ if (system == null && user == null) return null;
15063
+ const safeStringify = (value) => {
15064
+ try {
15065
+ return JSON.stringify(value);
15066
+ } catch {
15067
+ return "";
15068
+ }
15069
+ };
15070
+ const hash = createHash("sha256");
15071
+ hash.update(String(parsedBody.model ?? ""));
15072
+ if (system != null) hash.update(safeStringify(system));
15073
+ if (user != null) hash.update(safeStringify(user));
15074
+ return hash.digest("hex");
15075
+ }
15076
+ /**
15077
+ * Derive session ID for sticky routing from multiple sources:
15078
+ *
15079
+ * 1. `x-claude-code-session-id` header — Claude Code
15080
+ * 2. `session_id` from parsed body — Codex CLI (Responses API)
15081
+ * 3. hash(model + system + user) — content-based, per-conversation
15082
+ * 4. crypto.randomUUID() — fallback when no content available
15083
+ */
15084
+ function deriveSessionId(incomingHeaders, parsedBody, path, mode) {
15085
+ if (mode === "never") return void 0;
15086
+ const fromHeader = incomingHeaders.get("x-claude-code-session-id");
15087
+ if (fromHeader) return fromHeader.slice(0, 256);
15088
+ if (parsedBody && typeof parsedBody.session_id === "string" && parsedBody.session_id.length > 0) return parsedBody.session_id;
15089
+ if (parsedBody) {
15090
+ const fingerprint = extractConversationFingerprint(parsedBody, path);
15091
+ if (fingerprint) return fingerprint;
15092
+ }
15093
+ return PROXY_SESSION_ID;
15094
+ }
15095
+ //#endregion
15096
+ //#region src/proxy/middleware/inject-session-id.ts
15097
+ const injectSessionId = createMiddleware(async (c, next) => {
15098
+ const mode = c.var.resolvedConfig.sessionId;
15099
+ const sessionId = deriveSessionId(c.req.raw.headers, c.var.parsedBody, c.var.path, mode);
15100
+ if (sessionId) c.set("effectiveSessionId", sessionId);
15101
+ await next();
15102
+ });
15103
+ //#endregion
15104
+ //#region src/proxy/middleware/parse-body.ts
15105
+ const decoder = new TextDecoder();
15106
+ const parseBody = createMiddleware(async (c, next) => {
15107
+ const rawBody = c.var.rawBody;
15108
+ if (!rawBody || rawBody.byteLength === 0) {
15109
+ c.set("parsedBody", void 0);
15110
+ c.set("modelName", void 0);
15111
+ await next();
15112
+ return;
15113
+ }
15114
+ try {
15115
+ const json = JSON.parse(decoder.decode(rawBody));
15116
+ c.set("parsedBody", json);
15117
+ c.set("modelName", typeof json.model === "string" ? json.model : void 0);
15118
+ } catch {
15119
+ logger.debug(withReq(c.var.reqId, "Failed to parse request body as JSON — skipping injection"));
15120
+ c.set("parsedBody", void 0);
15121
+ c.set("modelName", void 0);
15122
+ }
15123
+ await next();
15124
+ });
15125
+ //#endregion
15126
+ //#region src/proxy/middleware/read-body.ts
15127
+ function parseBodyLimit(limit) {
15128
+ const match = limit.match(/^(\d+)\s*(kb|mb|gb)$/i);
15129
+ if (!match) return Number.MAX_SAFE_INTEGER;
15130
+ const n = parseInt(match[1], 10);
15131
+ switch (match[2].toLowerCase()) {
15132
+ case "kb": return n * 1024;
15133
+ case "mb": return n * 1024 * 1024;
15134
+ case "gb": return n * 1024 * 1024 * 1024;
15135
+ default: return Number.MAX_SAFE_INTEGER;
15136
+ }
15137
+ }
15138
+ const readBody = createMiddleware(async (c, next) => {
15139
+ const method = c.var.method;
15140
+ if (method === "GET" || method === "HEAD") {
15141
+ c.set("rawBody", void 0);
15142
+ await next();
15143
+ return;
15144
+ }
15145
+ try {
15146
+ const body = await c.req.raw.arrayBuffer();
15147
+ const maxBytes = parseBodyLimit(c.var.config.bodyLimit);
15148
+ if (body.byteLength > maxBytes) return c.json({ error: {
15149
+ message: "Request body too large",
15150
+ type: "proxy_request_error"
15151
+ } }, { status: 413 });
15152
+ c.set("rawBody", body.byteLength > 0 ? body : void 0);
15153
+ } catch (err) {
15154
+ const internalMessage = err instanceof Error ? err.message : "Failed to read request body";
15155
+ logger.error(withReq(c.var.reqId, internalMessage));
15156
+ return c.json({ error: {
15157
+ message: "Failed to read request body",
15158
+ type: "proxy_request_error"
15159
+ } }, { status: 400 });
15160
+ }
15161
+ await next();
15162
+ });
15163
+ //#endregion
15164
+ //#region src/proxy/middleware/resolve-config.ts
15165
+ const resolveConfig = createMiddleware(async (c, next) => {
15166
+ const resolved = resolveModelConfig(c.var.config, c.var.modelName);
15167
+ c.set("resolvedConfig", resolved);
15168
+ await next();
15169
+ });
15170
+ //#endregion
15171
+ //#region src/proxy/middleware/setup-request.ts
15172
+ const setupRequest = createMiddleware(async (c, next) => {
15173
+ const method = c.req.method;
15174
+ const { pathname, search } = new URL(c.req.url);
15175
+ const path = `${pathname}${search}`;
15176
+ c.set("reqId", requestId());
15177
+ c.set("method", method);
15178
+ c.set("path", path);
15179
+ c.set("upstreamUrl", buildUpstreamUrl(path, c.var.config));
15180
+ c.set("startedAt", Date.now());
15181
+ c.set("bodyMutated", false);
15182
+ await next();
15183
+ });
15184
+ //#endregion
15185
+ //#region src/proxy.ts
15186
+ const injectChain = [
15187
+ parseBody,
15188
+ resolveConfig,
15189
+ injectProvider,
15190
+ injectCacheControl,
15191
+ injectSessionId
15192
+ ];
14940
15193
  function createProxyServer(config, onReady) {
14941
15194
  const app = new Hono();
15195
+ app.use("*", async (c, next) => {
15196
+ c.set("config", config);
15197
+ await next();
15198
+ });
14942
15199
  app.get("/health", (c) => {
14943
15200
  const globalRouting = buildProviderRouting(config.provider);
14944
15201
  return c.json({
@@ -14948,23 +15205,15 @@ function createProxyServer(config, onReady) {
14948
15205
  modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : []
14949
15206
  });
14950
15207
  });
14951
- app.all("*", async (c) => {
14952
- const method = c.req.method;
14953
- const path = new URL(c.req.url).pathname;
14954
- const upstreamUrl = buildUpstreamUrl(c.req.url, config);
14955
- const startedAt = Date.now();
14956
- const reqId = requestId();
14957
- const raw = await readRawBody(c.req.raw, reqId);
14958
- if (!raw.ok) return raw.response;
14959
- const resolved = resolveRequest(raw.body, config, method, path, reqId);
14960
- if (resolved.error) return resolved.error;
14961
- const headers = buildRequestHeaders(c.req.raw.headers, config, resolved.inject, resolved.headers);
14962
- const controller = new AbortController();
14963
- c.req.raw.signal.addEventListener("abort", () => controller.abort());
14964
- const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
14965
- const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : "";
14966
- logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${resolved.inject ? " [inject]" : ""}${modelLog}`));
14967
- return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt, reqId);
15208
+ for (const path of INJECT_PATHS) app.post(path, setupRequest, readBody, ...injectChain, buildUpstreamReq, forwardRequest);
15209
+ app.all("*", setupRequest, readBody, resolveConfig, buildUpstreamReq, forwardRequest);
15210
+ app.onError((err, c) => {
15211
+ const reqId = c.var.reqId ?? "unknown";
15212
+ logger.error(withReq(String(reqId), `Unhandled error: ${err.message}`));
15213
+ return Response.json({ error: {
15214
+ message: "Internal proxy error",
15215
+ type: "proxy_internal_error"
15216
+ } }, { status: 500 });
14968
15217
  });
14969
15218
  return serve({
14970
15219
  fetch: app.fetch,
@@ -14996,7 +15245,7 @@ function startProxyServer(config, onReady) {
14996
15245
  }
14997
15246
  //#endregion
14998
15247
  //#region src/version.ts
14999
- const version = "0.8.0";
15248
+ const version = "0.9.0-beta.1";
15000
15249
  //#endregion
15001
15250
  //#region src/cli.ts
15002
15251
  const argv = process.argv.slice(2);
@@ -15097,8 +15346,8 @@ const withClient = (fn) => async (args) => {
15097
15346
  authType: cfg.authType,
15098
15347
  onFallback: (path) => {
15099
15348
  let endpoint;
15100
- if (path === "/providers") endpoint = "providers";
15101
- else if (path === "/models") endpoint = "models";
15349
+ if (path === "/v1/providers") endpoint = "providers";
15350
+ else if (path === "/v1/models") endpoint = "models";
15102
15351
  else endpoint = "model providers";
15103
15352
  logger.warn(`Custom API did not return ${endpoint}, using OpenRouter data as fallback`);
15104
15353
  }