proxitor 0.2.0 → 0.3.0

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.
Files changed (52) hide show
  1. package/README.md +221 -81
  2. package/dist/add.cjs +139 -0
  3. package/dist/add.cjs.map +1 -0
  4. package/dist/add.mjs +138 -0
  5. package/dist/add.mjs.map +1 -0
  6. package/dist/browse.cjs +88 -0
  7. package/dist/browse.cjs.map +1 -0
  8. package/dist/browse.mjs +87 -0
  9. package/dist/browse.mjs.map +1 -0
  10. package/dist/cli.cjs +148 -25
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.mjs +149 -26
  13. package/dist/cli.mjs.map +1 -1
  14. package/dist/config.cjs +68 -0
  15. package/dist/config.cjs.map +1 -0
  16. package/dist/config.mjs +45 -0
  17. package/dist/config.mjs.map +1 -0
  18. package/dist/config2.cjs +75 -0
  19. package/dist/config2.cjs.map +1 -0
  20. package/dist/config2.mjs +74 -0
  21. package/dist/config2.mjs.map +1 -0
  22. package/dist/edit.cjs +82 -0
  23. package/dist/edit.cjs.map +1 -0
  24. package/dist/edit.mjs +81 -0
  25. package/dist/edit.mjs.map +1 -0
  26. package/dist/index.cjs +2 -0
  27. package/dist/index.d.cts +223 -53
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +223 -53
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +2 -2
  32. package/dist/list.cjs +33 -0
  33. package/dist/list.cjs.map +1 -0
  34. package/dist/list.mjs +31 -0
  35. package/dist/list.mjs.map +1 -0
  36. package/dist/providers.cjs +376 -0
  37. package/dist/providers.cjs.map +1 -0
  38. package/dist/providers.mjs +279 -0
  39. package/dist/providers.mjs.map +1 -0
  40. package/dist/proxy.cjs +128 -8
  41. package/dist/proxy.cjs.map +1 -1
  42. package/dist/proxy.mjs +99 -9
  43. package/dist/proxy.mjs.map +1 -1
  44. package/dist/remove.cjs +38 -0
  45. package/dist/remove.cjs.map +1 -0
  46. package/dist/remove.mjs +37 -0
  47. package/dist/remove.mjs.map +1 -0
  48. package/dist/validate.cjs +26 -0
  49. package/dist/validate.cjs.map +1 -0
  50. package/dist/validate.mjs +25 -0
  51. package/dist/validate.mjs.map +1 -0
  52. package/package.json +10 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.mjs","names":["CACHE_KEY","CACHE_TTL"],"sources":["../src/openrouter/client.ts","../src/openrouter/cache.ts","../src/openrouter/models.ts","../src/commands/config/format.ts","../src/openrouter/endpoints.ts","../src/openrouter/providers.ts","../src/commands/config/providers.ts"],"sourcesContent":["const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1'\n\nexport class OpenRouterClientError extends Error {\n readonly status: number\n\n constructor(status: number, message: string) {\n super(`OpenRouter API error (${status}): ${message}`)\n this.name = 'OpenRouterClientError'\n this.status = status\n }\n}\n\n/** HTTP client for OpenRouter REST endpoints. Auth header is only sent when apiKey is non-empty. */\nexport class OpenRouterClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n\n constructor(apiKey: string, baseUrl?: string) {\n this.apiKey = apiKey\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL\n }\n\n async get<T>(path: string): Promise<T> {\n const url = `${this.baseUrl}${path}`\n\n const res = await fetch(url, {\n headers: {\n ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),\n },\n })\n\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new OpenRouterClientError(res.status, body || res.statusText)\n }\n\n return res.json() as Promise<T>\n }\n}\n","import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type CacheEntry<T> = {\n data: T\n fetchedAt: number\n}\n\nexport const CACHE_DIR = join(homedir(), '.proxitor', 'cache')\n\n/** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */\nexport function readCache<T>(key: string, ttlMs: number): T | null {\n const path = join(CACHE_DIR, `${key}.json`)\n if (!existsSync(path)) return null\n\n try {\n const entry: CacheEntry<T> = JSON.parse(readFileSync(path, 'utf-8'))\n if (Date.now() - entry.fetchedAt > ttlMs) return null\n return entry.data\n } catch {\n return null\n }\n}\n\nexport function writeCache<T>(key: string, data: T): void {\n mkdirSync(CACHE_DIR, { recursive: true })\n const entry: CacheEntry<T> = { fetchedAt: Date.now(), data }\n writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(entry))\n}\n\nexport function clearCache(key: string): void {\n const path = join(CACHE_DIR, `${key}.json`)\n if (existsSync(path)) unlinkSync(path)\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterModel, OpenRouterModelsResponse } from './types.js'\n\nconst CACHE_KEY = 'models'\nconst CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nexport async function fetchModels(client: OpenRouterClient): Promise<OpenRouterModel[]> {\n const cached = readCache<OpenRouterModel[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterModelsResponse>('/models')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"anthropic\"` */\nexport function parseModelAuthor(modelId: string): string {\n return modelId.split('/')[0] ?? ''\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"claude-sonnet-4\"` */\nexport function parseModelSlug(modelId: string): string {\n return modelId.split('/').slice(1).join('/')\n}\n\n/** `\"0.000003\"` → `\"$3.00\"`, `\"0\"` → `\"free\"` */\nexport function formatPrice(pricePerToken: string): string {\n const per1M = Number.parseFloat(pricePerToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n}\n","import { formatPrice } from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\n\nexport function formatPricing(prompt: string, completion: string): string {\n const fmt = (perToken: string) => {\n const per1M = Number.parseFloat(perToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n }\n return `${fmt(prompt)} / ${fmt(completion)}`\n}\n\n/** `200000` → `\"200k\"`, `1000000` → `\"1.0M\"` */\nexport function formatContextLength(tokens: number): string {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`\n return `${tokens}`\n}\n\n/** `1137` → `\"1.1s\"`, `null` → `\"N/A\"` */\nexport function formatLatency(ms: number | null): string {\n if (ms === null) return 'N/A'\n if (ms < 1000) return `${Math.round(ms)}ms`\n return `${(ms / 1000).toFixed(1)}s`\n}\n\nexport function formatThroughput(tokensPerSec: number | null): string {\n if (tokensPerSec === null) return 'N/A'\n return `${tokensPerSec.toFixed(0)} t/s`\n}\n\nexport function formatModelLabel(m: OpenRouterModel): string {\n return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`\n}\n\nexport function formatModelHint(m: OpenRouterModel): string {\n const parts = [`out ${formatPrice(m.pricing.completion)}`]\n if (m.pricing.input_cache_read && m.pricing.input_cache_read !== '0') {\n parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`)\n }\n return parts.join(' · ')\n}\n","import type { OpenRouterClient } from './client.js'\nimport type { ModelEndpoint, ModelEndpointsResponse } from './types.js'\n\nexport async function fetchModelEndpoints(\n client: OpenRouterClient,\n author: string,\n slug: string,\n): Promise<ModelEndpoint[]> {\n const response = await client.get<ModelEndpointsResponse>(\n `/models/${author}/${slug}/endpoints`,\n )\n return response.data.endpoints ?? []\n}\n\nexport type ProviderOption = {\n providerName: string\n /** Routing slug for `provider.only/order/ignore` (e.g. \"anthropic\", \"google-vertex/global\"). */\n tag: string\n}\n\nexport function getUniqueProviders(endpoints: ModelEndpoint[]): ProviderOption[] {\n const seen = new Set<string>()\n const result: ProviderOption[] = []\n\n for (const ep of endpoints) {\n if (seen.has(ep.tag)) continue\n seen.add(ep.tag)\n result.push({ tag: ep.tag, providerName: ep.provider_name })\n }\n\n result.sort((a, b) => a.providerName.localeCompare(b.providerName))\n return result\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterProvider, OpenRouterProvidersResponse } from './types.js'\n\nconst CACHE_KEY = 'providers'\nconst CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours\n\nexport async function fetchProviders(\n client: OpenRouterClient,\n): Promise<OpenRouterProvider[]> {\n const cached = readCache<OpenRouterProvider[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterProvidersResponse>('/providers')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n","import * as clack from '@clack/prompts'\nimport type { OpenRouterClient } from '../../openrouter/client.js'\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js'\nimport { parseModelAuthor, parseModelSlug } from '../../openrouter/models.js'\nimport { fetchProviders } from '../../openrouter/providers.js'\nimport { formatLatency, formatThroughput } from './format.js'\n\nexport async function fetchProvidersForPattern(\n client: OpenRouterClient,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const s = clack.spinner()\n s.start('Fetching providers...')\n try {\n const providers = await fetchProviders(client)\n const options = providers\n .map(p => ({ value: p.slug, label: p.name }))\n .sort((a, b) => a.label.localeCompare(b.label))\n s.stop(`${providers.length} providers available`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchEndpointsForModel(\n client: OpenRouterClient,\n modelId: string,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const author = parseModelAuthor(modelId)\n const slug = parseModelSlug(modelId)\n\n const s = clack.spinner()\n s.start('Fetching providers for this model...')\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug)\n const unique = getUniqueProviders(endpoints)\n\n const options = unique.map(p => {\n const ep = endpoints.find(e => e.tag === p.tag)\n const latency = ep?.latency_last_30m?.p50 ?? null\n const throughput = ep?.throughput_last_30m?.p50 ?? null\n return {\n value: p.tag,\n label: `${p.providerName} (${p.tag})`,\n hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`,\n }\n })\n\n s.stop(`${unique.length} providers available for this model`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchProvidersForModel(\n client: OpenRouterClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n if (isPattern) return fetchProvidersForPattern(client)\n return fetchEndpointsForModel(client, modelKey)\n}\n\nconst DONE_OPTION = '__done__'\n\nexport async function selectRoutingMode(message: string): Promise<string | symbol> {\n return clack.select({\n message,\n options: [\n { value: 'only', label: 'Use specific providers only' },\n { value: 'order', label: 'Set provider priority order' },\n { value: 'ignore', label: 'Ignore specific providers' },\n { value: 'skip', label: 'Skip provider routing' },\n ],\n })\n}\n\nexport async function selectProvidersByMode(\n mode: string,\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n if (mode === 'only') return selectOnlyProviders(providerOptions)\n if (mode === 'order') return selectOrderedProviders(providerOptions)\n if (mode === 'ignore') return selectIgnoreProviders(providerOptions)\n return null\n}\n\nasync function selectOnlyProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const only = values.length === 1 ? values[0] : values\n return { provider: { only } }\n}\n\nasync function selectOrderedProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const order: string[] = []\n\n for (let i = 1; ; i++) {\n const remaining = providerOptions.filter(p => !order.includes(p.value))\n if (remaining.length === 0) break\n\n const pick = await clack.select({\n message: `Select provider #${i} (or cancel to finish)`,\n options: [...remaining, { value: DONE_OPTION, label: '✓ Done' }],\n })\n\n if (clack.isCancel(pick) || pick === DONE_OPTION) break\n order.push(pick as string)\n }\n\n if (order.length === 0) {\n clack.log.warn('No providers selected')\n return null\n }\n\n const allowFallbacks = await clack.confirm({\n message: 'Allow fallbacks to other providers?',\n initialValue: true,\n })\n\n return {\n provider: {\n order: order.length === 1 ? order[0] : order,\n allowFallbacks: clack.isCancel(allowFallbacks) ? true : (allowFallbacks as boolean),\n },\n }\n}\n\nasync function selectIgnoreProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers to ignore',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const ignore = values.length === 1 ? values[0] : values\n return { provider: { ignore } }\n}\n"],"mappings":"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"}
package/dist/proxy.cjs CHANGED
@@ -25,9 +25,91 @@ let node_os = require("node:os");
25
25
  let node_path = require("node:path");
26
26
  let js_yaml = require("js-yaml");
27
27
  js_yaml = __toESM(js_yaml, 1);
28
+ let zod = require("zod");
28
29
  let consola = require("consola");
29
30
  let _hono_node_server = require("@hono/node-server");
30
31
  let hono = require("hono");
32
+ //#region src/config-schema.ts
33
+ /** Percentile cutoffs for performance thresholds */
34
+ const percentileCutoffsSchema = zod.z.object({
35
+ p50: zod.z.number().positive().optional(),
36
+ p75: zod.z.number().positive().optional(),
37
+ p90: zod.z.number().positive().optional(),
38
+ p99: zod.z.number().positive().optional()
39
+ }).strict();
40
+ /** Provider sorting options */
41
+ const providerSortSchema = zod.z.union([zod.z.enum([
42
+ "price",
43
+ "throughput",
44
+ "latency"
45
+ ]), zod.z.object({
46
+ by: zod.z.enum([
47
+ "price",
48
+ "throughput",
49
+ "latency"
50
+ ]),
51
+ partition: zod.z.enum(["model", "none"]).optional()
52
+ }).strict()]);
53
+ /** Maximum pricing for a request */
54
+ const maxPriceSchema = zod.z.object({
55
+ prompt: zod.z.number().nonnegative().optional(),
56
+ completion: zod.z.number().nonnegative().optional(),
57
+ request: zod.z.number().nonnegative().optional(),
58
+ image: zod.z.number().nonnegative().optional()
59
+ }).strict();
60
+ /** Provider routing configuration */
61
+ const providerConfigSchema = zod.z.object({
62
+ only: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
63
+ order: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
64
+ ignore: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
65
+ allowFallbacks: zod.z.boolean().optional(),
66
+ sort: providerSortSchema.optional(),
67
+ quantizations: zod.z.array(zod.z.string()).optional(),
68
+ maxPrice: maxPriceSchema.optional(),
69
+ requireParameters: zod.z.boolean().optional(),
70
+ dataCollection: zod.z.enum(["allow", "deny"]).optional(),
71
+ zdr: zod.z.boolean().optional(),
72
+ enforceDistillableText: zod.z.boolean().optional(),
73
+ preferredMinThroughput: zod.z.union([zod.z.number().positive(), percentileCutoffsSchema]).optional(),
74
+ preferredMaxLatency: zod.z.union([zod.z.number().positive(), percentileCutoffsSchema]).optional()
75
+ }).strict();
76
+ /** Per-model override: layers on top of global config */
77
+ const modelOverrideSchema = zod.z.object({
78
+ provider: providerConfigSchema.optional(),
79
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
80
+ }).strict();
81
+ /** Schema for validating raw file content — all top-level keys optional */
82
+ const proxyConfigFileSchema = zod.z.object({
83
+ host: zod.z.string().min(1),
84
+ port: zod.z.number().int().min(1).max(65535),
85
+ openrouterKey: zod.z.string(),
86
+ openrouterBaseUrl: zod.z.string().url(),
87
+ verbose: zod.z.boolean(),
88
+ bodyLimit: zod.z.string().min(1),
89
+ provider: providerConfigSchema.optional(),
90
+ attributionReferer: zod.z.string().min(1),
91
+ attributionTitle: zod.z.string().min(1),
92
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
93
+ modelOverrides: zod.z.record(zod.z.string().min(1), modelOverrideSchema).optional()
94
+ }).strict().partial();
95
+ /** Wraps YAML/JSON parse errors with the config file path */
96
+ var ConfigParseError = class extends Error {
97
+ constructor(filePath, cause) {
98
+ super(`Failed to parse config file ${filePath}: ${cause?.message ?? "unknown error"}`, { cause });
99
+ this.name = "ConfigParseError";
100
+ }
101
+ };
102
+ /** Formats zod validation issues into a readable multi-line message */
103
+ var ConfigValidationError = class extends Error {
104
+ constructor(filePath, zodError) {
105
+ const lines = zodError.issues.map((issue) => {
106
+ return ` ${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
107
+ });
108
+ super(`Invalid config in ${filePath}:\n${lines.join("\n")}`);
109
+ this.name = "ConfigValidationError";
110
+ }
111
+ };
112
+ //#endregion
31
113
  //#region src/utils.ts
32
114
  /** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
33
115
  function toArray(value) {
@@ -211,8 +293,15 @@ function findConfigFile(explicitPath) {
211
293
  }
212
294
  function readConfigFile(filePath) {
213
295
  const content = (0, node_fs.readFileSync)(filePath, "utf-8");
214
- if (filePath.endsWith(".json")) return JSON.parse(content);
215
- return js_yaml.load(content);
296
+ let raw;
297
+ try {
298
+ raw = filePath.endsWith(".json") ? JSON.parse(content) : js_yaml.load(content);
299
+ } catch (err) {
300
+ throw new ConfigParseError(filePath, err instanceof Error ? err : void 0);
301
+ }
302
+ const result = proxyConfigFileSchema.safeParse(raw);
303
+ if (!result.success) throw new ConfigValidationError(filePath, result.error);
304
+ return result.data;
216
305
  }
217
306
  //#endregion
218
307
  //#region src/logger.ts
@@ -307,14 +396,15 @@ const INJECT_PATHS = new Set([
307
396
  function shouldInject(method, path) {
308
397
  return method === "POST" && INJECT_PATHS.has(path);
309
398
  }
310
- /** Strip /v1 prefix: /v1/chat/completions → /chat/completions */
311
- function toUpstreamPath(originalUrl) {
312
- if (originalUrl.startsWith("/v1")) return originalUrl.slice(3);
313
- return originalUrl;
399
+ /** Strip /v1 prefix from path: /v1/chat/completions → /chat/completions */
400
+ function toUpstreamPath(pathname) {
401
+ if (pathname.startsWith("/v1")) return pathname.slice(3);
402
+ return pathname;
314
403
  }
315
404
  /** Build full upstream URL from request and config */
316
- function buildUpstreamUrl(originalUrl, config) {
317
- return `${config.openrouterBaseUrl}${toUpstreamPath(originalUrl)}`;
405
+ function buildUpstreamUrl(requestUrl, config) {
406
+ const { pathname } = new URL(requestUrl);
407
+ return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`;
318
408
  }
319
409
  //#endregion
320
410
  //#region src/proxy.ts
@@ -472,6 +562,24 @@ function startProxyServer(config, onReady) {
472
562
  return server;
473
563
  }
474
564
  //#endregion
565
+ Object.defineProperty(exports, "ConfigParseError", {
566
+ enumerable: true,
567
+ get: function() {
568
+ return ConfigParseError;
569
+ }
570
+ });
571
+ Object.defineProperty(exports, "ConfigValidationError", {
572
+ enumerable: true,
573
+ get: function() {
574
+ return ConfigValidationError;
575
+ }
576
+ });
577
+ Object.defineProperty(exports, "__toESM", {
578
+ enumerable: true,
579
+ get: function() {
580
+ return __toESM;
581
+ }
582
+ });
475
583
  Object.defineProperty(exports, "buildProviderRouting", {
476
584
  enumerable: true,
477
585
  get: function() {
@@ -490,6 +598,12 @@ Object.defineProperty(exports, "extractModel", {
490
598
  return extractModel;
491
599
  }
492
600
  });
601
+ Object.defineProperty(exports, "findConfigFile", {
602
+ enumerable: true,
603
+ get: function() {
604
+ return findConfigFile;
605
+ }
606
+ });
493
607
  Object.defineProperty(exports, "loadConfig", {
494
608
  enumerable: true,
495
609
  get: function() {
@@ -508,6 +622,12 @@ Object.defineProperty(exports, "matchScore", {
508
622
  return matchScore;
509
623
  }
510
624
  });
625
+ Object.defineProperty(exports, "readConfigFile", {
626
+ enumerable: true,
627
+ get: function() {
628
+ return readConfigFile;
629
+ }
630
+ });
511
631
  Object.defineProperty(exports, "resolveModelConfig", {
512
632
  enumerable: true,
513
633
  get: function() {
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.cjs","names":["yaml","Hono"],"sources":["../src/utils.ts","../src/config.ts","../src/logger.ts","../src/proxy/headers.ts","../src/proxy/inject.ts","../src/proxy/paths.ts","../src/proxy.ts"],"sourcesContent":["/** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */\nexport function toArray(value: string | string[] | undefined): string[] | undefined {\n if (value === undefined) return undefined\n const arr = Array.isArray(value) ? [...value] : [value]\n return arr.length > 0 ? arr : undefined\n}\n\n/** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */\nexport function tryParseBody(raw: ArrayBuffer): Record<string, unknown> | undefined {\n if (raw.byteLength === 0) return undefined\n try {\n return JSON.parse(new TextDecoder().decode(raw)) as Record<string, unknown>\n } catch {\n return undefined\n }\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport * as yaml from 'js-yaml'\nimport { toArray } from './utils.js'\n\n/** Percentile cutoffs for performance thresholds */\nexport type PercentileCutoffs = {\n p50?: number\n p75?: number\n p90?: number\n p99?: number\n}\n\n/** Provider sorting options */\nexport type ProviderSort =\n | 'price'\n | 'throughput'\n | 'latency'\n | { by: 'price' | 'throughput' | 'latency'; partition?: 'model' | 'none' }\n\n/** Maximum pricing for a request */\nexport type MaxPrice = {\n prompt?: number\n completion?: number\n request?: number\n image?: number\n}\n\nexport type ProviderConfig = {\n /** Allow only these providers (e.g. \"deepinfra\" or [\"anthropic\", \"openai\"]) */\n only?: string | string[]\n /** Try providers in this order (e.g. \"anthropic\" or [\"openai\", \"together\"]) */\n order?: string | string[]\n /** Ignore these providers (mirror of only — skip specific providers) */\n ignore?: string | string[]\n /** Allow fallback to other providers (default: true) */\n allowFallbacks?: boolean\n /** Sort providers by price, throughput, or latency */\n sort?: ProviderSort\n /** Filter by quantization levels (e.g. [\"fp8\", \"int4\"]) */\n quantizations?: string[]\n /** Maximum pricing to accept */\n maxPrice?: MaxPrice\n /** Only use providers that support all request parameters (default: false) */\n requireParameters?: boolean\n /** Control data collection policy: \"allow\" or \"deny\" (default: \"allow\") */\n dataCollection?: 'allow' | 'deny'\n /** Restrict routing to Zero Data Retention endpoints */\n zdr?: boolean\n /** Restrict routing to models that allow text distillation */\n enforceDistillableText?: boolean\n /** Preferred minimum throughput (tokens/sec) */\n preferredMinThroughput?: number | PercentileCutoffs\n /** Preferred maximum latency (seconds) */\n preferredMaxLatency?: number | PercentileCutoffs\n}\n\n/** Per-model override: layers on top of global config */\nexport type ModelOverride = {\n /** Override provider routing for matching models */\n provider?: ProviderConfig\n /** Additional headers to merge for matching models */\n headers?: Record<string, string>\n}\n\n/** Result of merging global config with a model-specific override */\nexport type ResolvedModelConfig = {\n provider?: ProviderConfig\n headers?: Record<string, string>\n}\n\nexport type ProxyConfig = {\n host: string\n port: number\n openrouterKey: string\n openrouterBaseUrl: string\n verbose: boolean\n /** Request body size limit (default: \"50mb\") */\n bodyLimit: string\n /** Provider routing configuration (global default) */\n provider?: ProviderConfig\n /** HTTP-Referer for OpenRouter attribution */\n attributionReferer: string\n /** X-Title for OpenRouter attribution */\n attributionTitle: string\n /** Custom headers to add to proxied requests (global default) */\n headers?: Record<string, string>\n /** Per-model config overrides. Keys are exact model names or prefix patterns (e.g. \"claude-*\") */\n modelOverrides?: Record<string, ModelOverride>\n}\n\nconst DEFAULT_CONFIG: ProxyConfig = {\n host: '0.0.0.0',\n port: 8080,\n openrouterKey: '',\n openrouterBaseUrl: 'https://openrouter.ai/api/v1',\n verbose: false,\n bodyLimit: '50mb',\n attributionReferer: 'http://localhost',\n attributionTitle: 'proxitor',\n}\n\ntype LoadConfigOptions = {\n configPath?: string\n noConfig?: boolean\n host?: string\n openrouterKey?: string\n port?: number\n verbose?: boolean\n}\n\n/** Fields that need toArray normalization (string | string[] → string[] | undefined) */\nconst ARRAY_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'only', apiName: 'only' },\n { key: 'order', apiName: 'order' },\n { key: 'ignore', apiName: 'ignore' },\n { key: 'quantizations', apiName: 'quantizations' },\n] as const\n\n/** Direct camelCase → snake_case field mappings */\nconst DIRECT_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'sort', apiName: 'sort' },\n { key: 'maxPrice', apiName: 'max_price' },\n { key: 'requireParameters', apiName: 'require_parameters' },\n { key: 'dataCollection', apiName: 'data_collection' },\n { key: 'zdr', apiName: 'zdr' },\n { key: 'enforceDistillableText', apiName: 'enforce_distillable_text' },\n { key: 'preferredMinThroughput', apiName: 'preferred_min_throughput' },\n { key: 'preferredMaxLatency', apiName: 'preferred_max_latency' },\n] as const\n\n/** Build the provider routing object for OpenRouter request body injection */\nexport function buildProviderRouting(\n provider?: ProviderConfig,\n): Record<string, unknown> | undefined {\n if (!provider) return undefined\n\n const result: Record<string, unknown> = {}\n\n for (const { key, apiName } of ARRAY_FIELDS) {\n const value = provider[key]\n if (value !== undefined) {\n const normalized = toArray(value as string | string[])\n if (normalized) result[apiName] = normalized\n }\n }\n\n for (const { key, apiName } of DIRECT_FIELDS) {\n const value = provider[key]\n if (value !== undefined) result[apiName] = value\n }\n\n if (result.order) {\n result.allow_fallbacks = provider.allowFallbacks ?? true\n }\n\n return Object.keys(result).length > 0 ? result : undefined\n}\n\n/** Score a pattern against a model name. Higher = better match. -1 = no match. */\nexport function matchScore(pattern: string, modelName: string): number {\n if (pattern === modelName) return modelName.length + 1000\n\n if (pattern.endsWith('*') && modelName.startsWith(pattern.slice(0, -1))) {\n return pattern.length\n }\n\n return -1\n}\n\n/** Resolve the effective config for a given model by merging global defaults with the best-matching override */\nexport function resolveModelConfig(\n config: ProxyConfig,\n modelName?: string,\n): ResolvedModelConfig {\n const result: ResolvedModelConfig = {\n provider: config.provider,\n headers: config.headers ? { ...config.headers } : undefined,\n }\n\n if (!modelName || !config.modelOverrides) return result\n\n let bestPattern: string | null = null\n let bestScore = -1\n\n for (const pattern of Object.keys(config.modelOverrides)) {\n const score = matchScore(pattern, modelName)\n if (score > bestScore) {\n bestScore = score\n bestPattern = pattern\n }\n }\n\n if (bestPattern) {\n const override = config.modelOverrides[bestPattern]\n if (override?.provider !== undefined) {\n result.provider = override.provider\n }\n if (override?.headers) {\n result.headers = { ...(result.headers ?? {}), ...override.headers }\n }\n }\n\n return result\n}\n\nexport async function loadConfig(options: LoadConfigOptions): Promise<ProxyConfig> {\n const config = { ...DEFAULT_CONFIG }\n\n if (!options.noConfig) {\n const configPath = findConfigFile(options.configPath)\n if (configPath) {\n const fileConfig = readConfigFile(configPath)\n Object.assign(config, fileConfig)\n }\n }\n\n if (options.host) config.host = options.host\n if (options.port) config.port = options.port\n if (options.verbose) config.verbose = options.verbose\n\n if (options.openrouterKey) {\n config.openrouterKey = options.openrouterKey\n } else if (!config.openrouterKey) {\n config.openrouterKey = process.env.OPENROUTER_API_KEY ?? ''\n }\n\n if (!config.openrouterKey) {\n throw new Error(\n 'OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.',\n )\n }\n\n return config\n}\n\n/** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */\nfunction getXdgConfigDir(): string {\n const xdgHome = process.env.XDG_CONFIG_HOME\n return xdgHome ? resolve(xdgHome, 'proxitor') : join(homedir(), '.config', 'proxitor')\n}\n\nfunction findConfigFile(explicitPath?: string): string | null {\n if (explicitPath) {\n if (!existsSync(explicitPath)) {\n throw new Error(`Config file not found: ${explicitPath}`)\n }\n return resolve(explicitPath)\n }\n\n const localCandidates = [\n 'proxitor.config.yaml',\n 'proxitor.config.yml',\n 'proxitor.config.json',\n '.proxitor.yaml',\n '.proxitor.yml',\n '.proxitor.json',\n ]\n\n for (const candidate of localCandidates) {\n const fullPath = resolve(candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n const xdgDir = getXdgConfigDir()\n const xdgCandidates = ['config.yaml', 'config.yml', 'config.json']\n\n for (const candidate of xdgCandidates) {\n const fullPath = join(xdgDir, candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n return null\n}\n\nfunction readConfigFile(filePath: string): Partial<ProxyConfig> {\n const content = readFileSync(filePath, 'utf-8')\n\n if (filePath.endsWith('.json')) {\n return JSON.parse(content) as Partial<ProxyConfig>\n }\n\n return yaml.load(content) as Partial<ProxyConfig>\n}\n","import { consola } from 'consola'\n\nexport const logger = consola.withTag('proxitor')\n","import type { ProxyConfig } from '../config.js'\n\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n])\n\n/** Headers to strip from client request before forwarding */\nconst STRIP_REQUEST = new Set(['authorization', 'x-api-key', 'host', 'content-length'])\n\n/** Headers to strip from upstream response before forwarding */\nconst STRIP_RESPONSE = new Set(['content-length', 'content-encoding'])\n\n/** Filter headers by removing hop-by-hop and an additional blocklist */\nfunction filterHeaders(\n incoming: Headers,\n blocklist: ReadonlySet<string>,\n): Record<string, string> {\n const headers: Record<string, string> = {}\n for (const [key, value] of incoming.entries()) {\n const lower = key.toLowerCase()\n if (HOP_BY_HOP.has(lower)) continue\n if (blocklist.has(lower)) continue\n headers[key] = value\n }\n return headers\n}\n\n/** Build request headers for upstream fetch */\nexport function buildRequestHeaders(\n incoming: Headers,\n config: ProxyConfig,\n inject: boolean,\n extraHeaders?: Record<string, string>,\n): Record<string, string> {\n const headers = filterHeaders(incoming, STRIP_REQUEST)\n\n headers.Authorization = `Bearer ${config.openrouterKey}`\n headers['HTTP-Referer'] = config.attributionReferer\n headers['X-Title'] = config.attributionTitle\n headers['Accept-Encoding'] = 'identity'\n\n if (extraHeaders) {\n Object.assign(headers, extraHeaders)\n }\n\n if (inject) {\n headers['Content-Type'] = 'application/json'\n }\n\n return headers\n}\n\n/** Filter response headers and add SSE-friendly defaults */\nexport function buildResponseHeaders(from: Headers): Record<string, string> {\n const headers = filterHeaders(from, STRIP_RESPONSE)\n\n headers['Cache-Control'] = 'no-cache'\n headers['X-Accel-Buffering'] = 'no'\n\n return headers\n}\n","import { tryParseBody } from '../utils.js'\n\n/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */\nexport function extractModel(rawBody: ArrayBuffer): string | undefined {\n const json = tryParseBody(rawBody)\n return typeof json?.model === 'string' ? json.model : undefined\n}\n\n/** Inject provider routing into request body, always overwriting existing value */\nexport function injectProvider(\n rawBody: ArrayBuffer,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer {\n if (rawBody.byteLength === 0) {\n throw new Error('Request body is empty; cannot inject provider')\n }\n\n let json: Record<string, unknown>\n try {\n json = JSON.parse(new TextDecoder().decode(rawBody)) as Record<string, unknown>\n } catch (parseError) {\n throw new Error('Request body is not valid JSON; cannot inject provider', {\n cause: parseError,\n })\n }\n\n const modified = { ...json, provider: providerRouting }\n return new TextEncoder().encode(JSON.stringify(modified)).buffer as ArrayBuffer\n}\n","import type { ProxyConfig } from '../config.js'\n\n/**\n * Paths where provider routing is injected into the request body.\n * All three are OpenRouter-supported endpoints:\n * /v1/chat/completions — OpenAI Chat Completions\n * /v1/responses — OpenAI Responses API\n * /v1/messages — Anthropic Messages API\n */\nexport const INJECT_PATHS = new Set([\n '/v1/chat/completions',\n '/v1/responses',\n '/v1/messages',\n])\n\n/** Check if this request should have provider routing injected */\nexport function shouldInject(method: string, path: string): boolean {\n return method === 'POST' && INJECT_PATHS.has(path)\n}\n\n/** Strip /v1 prefix: /v1/chat/completions → /chat/completions */\nexport function toUpstreamPath(originalUrl: string): string {\n if (originalUrl.startsWith('/v1')) {\n return originalUrl.slice('/v1'.length)\n }\n return originalUrl\n}\n\n/** Build full upstream URL from request and config */\nexport function buildUpstreamUrl(originalUrl: string, config: ProxyConfig): string {\n return `${config.openrouterBaseUrl}${toUpstreamPath(originalUrl)}`\n}\n","import { type HttpBindings, type ServerType, serve } from '@hono/node-server'\nimport { Hono } from 'hono'\nimport { buildProviderRouting, type ProxyConfig, resolveModelConfig } from './config.js'\nimport { logger } from './logger.js'\nimport { buildRequestHeaders, buildResponseHeaders } from './proxy/headers.js'\nimport { extractModel, injectProvider } from './proxy/inject.js'\nimport { buildUpstreamUrl, shouldInject } from './proxy/paths.js'\n\ntype ProxyContext = {\n Variables: {\n config: ProxyConfig\n }\n Bindings: HttpBindings\n}\n\nfunction readRequestBody(\n method: string,\n raw: ArrayBuffer,\n inject: boolean,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer | undefined {\n if (['GET', 'HEAD'].includes(method)) return undefined\n\n if (inject) {\n return injectProvider(raw, providerRouting)\n }\n\n return raw.byteLength > 0 ? raw : undefined\n}\n\nasync function fetchUpstream(\n url: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n): Promise<Response> {\n return fetch(url, {\n method,\n headers,\n body,\n signal,\n duplex: body ? 'half' : undefined,\n })\n}\n\nfunction buildUpstreamResponse(upstream: Response, method: string): Response {\n const headers = buildResponseHeaders(upstream.headers)\n\n if (method === 'HEAD' || !upstream.body) {\n return new Response(null, { status: upstream.status, headers })\n }\n\n return new Response(upstream.body, { status: upstream.status, headers })\n}\n\n/** Read and process the request body, returning an error response on failure */\nasync function readRawBody(\n request: Request,\n): Promise<{ ok: true; body: ArrayBuffer } | { ok: false; response: Response }> {\n try {\n const body = await request.arrayBuffer()\n return { ok: true, body }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to read request body'\n logger.error(message)\n return {\n ok: false,\n response: Response.json(\n { error: { message, type: 'proxy_request_error' } },\n { status: 400 },\n ),\n }\n }\n}\n\ntype ResolvedRequest = {\n inject: boolean\n body: ArrayBuffer | undefined\n modelName: string | undefined\n headers: Record<string, string> | undefined\n error?: Response\n}\n\n/** Resolve per-request config: extract model, resolve overrides, build routing and body */\nfunction resolveRequest(\n rawBody: ArrayBuffer,\n config: ProxyConfig,\n method: string,\n path: string,\n): ResolvedRequest {\n const modelName = extractModel(rawBody)\n const resolved = resolveModelConfig(config, modelName)\n const providerRouting = buildProviderRouting(resolved.provider)\n const inject = shouldInject(method, path) && providerRouting !== undefined\n\n let body: ArrayBuffer | undefined\n try {\n body = readRequestBody(\n method,\n rawBody,\n inject,\n providerRouting as Record<string, unknown>,\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to process request body'\n logger.error(message)\n return {\n inject,\n body: undefined,\n modelName,\n headers: resolved.headers,\n error: new Response(\n JSON.stringify({ error: { message, type: 'proxy_request_error' } }),\n { status: 400, headers: { 'Content-Type': 'application/json' } },\n ),\n }\n }\n\n return { inject, body, modelName, headers: resolved.headers }\n}\n\n/** Execute upstream fetch, returning appropriate error responses on failure */\nasync function executeUpstream(\n upstreamUrl: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n path: string,\n startedAt: number,\n): Promise<Response> {\n let upstream: Response\n try {\n upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal)\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n logger.warn(`Aborted: ${method} ${path}`)\n return new Response(null, { status: 499 })\n }\n\n logger.error('Upstream fetch error:', err)\n return Response.json(\n {\n error: {\n message: 'Proxy failed to reach upstream',\n type: 'proxy_upstream_error',\n },\n },\n { status: 502 },\n )\n }\n\n logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`)\n return buildUpstreamResponse(upstream, method)\n}\n\nexport function createProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const app = new Hono<ProxyContext>()\n\n app.get('/health', c => {\n const globalRouting = buildProviderRouting(config.provider)\n return c.json({\n ok: true,\n upstream: config.openrouterBaseUrl,\n provider: globalRouting ?? 'not configured',\n modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : [],\n })\n })\n\n app.all('*', async c => {\n const method = c.req.method\n const path = new URL(c.req.url).pathname\n const upstreamUrl = buildUpstreamUrl(c.req.url, config)\n const startedAt = Date.now()\n\n const raw = await readRawBody(c.req.raw)\n if (!raw.ok) return raw.response\n\n const resolved = resolveRequest(raw.body, config, method, path)\n if (resolved.error) return resolved.error\n\n const headers = buildRequestHeaders(\n c.req.raw.headers,\n config,\n resolved.inject,\n resolved.headers,\n )\n\n const controller = new AbortController()\n c.req.raw.signal.addEventListener('abort', () => controller.abort())\n\n const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : ''\n logger.info(\n `${method} ${path} → ${upstreamUrl}${resolved.inject ? ' [inject]' : ''}${modelLog}`,\n )\n\n return executeUpstream(\n upstreamUrl,\n method,\n headers,\n resolved.body,\n controller.signal,\n path,\n startedAt,\n )\n })\n\n return serve(\n {\n fetch: app.fetch,\n port: config.port,\n hostname: config.host,\n },\n onReady,\n )\n}\n\n/** Shutdown deadline: force-close after this many ms */\nconst SHUTDOWN_TIMEOUT_MS = 10_000\n\n/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */\nexport function startProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const server = createProxyServer(config, onReady)\n\n let shuttingDown = false\n\n function shutdown(signal: string) {\n if (shuttingDown) return\n shuttingDown = true\n\n logger.info(`${signal} received — draining active connections…`)\n\n const timer = setTimeout(() => {\n logger.warn('Forcing shutdown — drain timeout exceeded')\n process.exit(1)\n }, SHUTDOWN_TIMEOUT_MS)\n\n server.close(() => {\n clearTimeout(timer)\n logger.info('All connections drained — goodbye')\n process.exit(0)\n })\n }\n\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n process.on('SIGINT', () => shutdown('SIGINT'))\n\n return server\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"proxy.cjs","names":["z","yaml","Hono"],"sources":["../src/config-schema.ts","../src/utils.ts","../src/config.ts","../src/logger.ts","../src/proxy/headers.ts","../src/proxy/inject.ts","../src/proxy/paths.ts","../src/proxy.ts"],"sourcesContent":["import { z } from 'zod'\n\n// ---------------------------------------------------------------------------\n// Zod schemas (bottom-up)\n// ---------------------------------------------------------------------------\n\n/** Percentile cutoffs for performance thresholds */\nexport const percentileCutoffsSchema = z\n .object({\n p50: z.number().positive().optional(),\n p75: z.number().positive().optional(),\n p90: z.number().positive().optional(),\n p99: z.number().positive().optional(),\n })\n .strict()\n\n/** Provider sorting options */\nexport const providerSortSchema = z.union([\n z.enum(['price', 'throughput', 'latency']),\n z\n .object({\n by: z.enum(['price', 'throughput', 'latency']),\n partition: z.enum(['model', 'none']).optional(),\n })\n .strict(),\n])\n\n/** Maximum pricing for a request */\nexport const maxPriceSchema = z\n .object({\n prompt: z.number().nonnegative().optional(),\n completion: z.number().nonnegative().optional(),\n request: z.number().nonnegative().optional(),\n image: z.number().nonnegative().optional(),\n })\n .strict()\n\n/** Provider routing configuration */\nexport const providerConfigSchema = z\n .object({\n only: z.union([z.string(), z.array(z.string())]).optional(),\n order: z.union([z.string(), z.array(z.string())]).optional(),\n ignore: z.union([z.string(), z.array(z.string())]).optional(),\n allowFallbacks: z.boolean().optional(),\n sort: providerSortSchema.optional(),\n quantizations: z.array(z.string()).optional(),\n maxPrice: maxPriceSchema.optional(),\n requireParameters: z.boolean().optional(),\n dataCollection: z.enum(['allow', 'deny']).optional(),\n zdr: z.boolean().optional(),\n enforceDistillableText: z.boolean().optional(),\n preferredMinThroughput: z\n .union([z.number().positive(), percentileCutoffsSchema])\n .optional(),\n preferredMaxLatency: z\n .union([z.number().positive(), percentileCutoffsSchema])\n .optional(),\n })\n .strict()\n\n/** Per-model override: layers on top of global config */\nexport const modelOverrideSchema = z\n .object({\n provider: providerConfigSchema.optional(),\n headers: z.record(z.string(), z.string()).optional(),\n })\n .strict()\n\n/** Full proxy configuration */\nexport const proxyConfigSchema = z\n .object({\n host: z.string().min(1),\n port: z.number().int().min(1).max(65535),\n openrouterKey: z.string(),\n openrouterBaseUrl: z.string().url(),\n verbose: z.boolean(),\n bodyLimit: z.string().min(1),\n provider: providerConfigSchema.optional(),\n attributionReferer: z.string().min(1),\n attributionTitle: z.string().min(1),\n headers: z.record(z.string(), z.string()).optional(),\n modelOverrides: z.record(z.string().min(1), modelOverrideSchema).optional(),\n })\n .strict()\n\n/** Schema for validating raw file content — all top-level keys optional */\nexport const proxyConfigFileSchema = proxyConfigSchema.partial()\n\n// ---------------------------------------------------------------------------\n// Derived TypeScript types\n// ---------------------------------------------------------------------------\n\nexport type ProxyConfig = z.infer<typeof proxyConfigSchema>\nexport type ProviderConfig = z.infer<typeof providerConfigSchema>\nexport type ModelOverride = z.infer<typeof modelOverrideSchema>\nexport type MaxPrice = z.infer<typeof maxPriceSchema>\nexport type PercentileCutoffs = z.infer<typeof percentileCutoffsSchema>\nexport type ProviderSort = z.infer<typeof providerSortSchema>\n\n// ---------------------------------------------------------------------------\n// Custom error classes\n// ---------------------------------------------------------------------------\n\n/** Wraps YAML/JSON parse errors with the config file path */\nexport class ConfigParseError extends Error {\n constructor(filePath: string, cause?: Error) {\n super(\n `Failed to parse config file ${filePath}: ${cause?.message ?? 'unknown error'}`,\n { cause },\n )\n this.name = 'ConfigParseError'\n }\n}\n\n/** Formats zod validation issues into a readable multi-line message */\nexport class ConfigValidationError extends Error {\n constructor(filePath: string, zodError: z.ZodError) {\n const lines = zodError.issues.map(issue => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '(root)'\n return ` ${path}: ${issue.message}`\n })\n super(`Invalid config in ${filePath}:\\n${lines.join('\\n')}`)\n this.name = 'ConfigValidationError'\n }\n}\n","/** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */\nexport function toArray(value: string | string[] | undefined): string[] | undefined {\n if (value === undefined) return undefined\n const arr = Array.isArray(value) ? [...value] : [value]\n return arr.length > 0 ? arr : undefined\n}\n\n/** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */\nexport function tryParseBody(raw: ArrayBuffer): Record<string, unknown> | undefined {\n if (raw.byteLength === 0) return undefined\n try {\n return JSON.parse(new TextDecoder().decode(raw)) as Record<string, unknown>\n } catch {\n return undefined\n }\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport * as yaml from 'js-yaml'\nimport {\n ConfigParseError,\n ConfigValidationError,\n type ProviderConfig,\n type ProxyConfig,\n proxyConfigFileSchema,\n} from './config-schema.js'\nimport { toArray } from './utils.js'\n\nexport type {\n MaxPrice,\n ModelOverride,\n PercentileCutoffs,\n ProviderConfig,\n ProviderSort,\n ProxyConfig,\n} from './config-schema.js'\nexport { ConfigParseError, ConfigValidationError } from './config-schema.js'\n\n/** Result of merging global config with a model-specific override */\nexport type ResolvedModelConfig = {\n provider?: ProviderConfig\n headers?: Record<string, string>\n}\n\nconst DEFAULT_CONFIG: ProxyConfig = {\n host: '0.0.0.0',\n port: 8080,\n openrouterKey: '',\n openrouterBaseUrl: 'https://openrouter.ai/api/v1',\n verbose: false,\n bodyLimit: '50mb',\n attributionReferer: 'http://localhost',\n attributionTitle: 'proxitor',\n}\n\ntype LoadConfigOptions = {\n configPath?: string\n noConfig?: boolean\n host?: string\n openrouterKey?: string\n port?: number\n verbose?: boolean\n}\n\n/** Fields that need toArray normalization (string | string[] → string[] | undefined) */\nconst ARRAY_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'only', apiName: 'only' },\n { key: 'order', apiName: 'order' },\n { key: 'ignore', apiName: 'ignore' },\n { key: 'quantizations', apiName: 'quantizations' },\n] as const\n\n/** Direct camelCase → snake_case field mappings */\nconst DIRECT_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'sort', apiName: 'sort' },\n { key: 'maxPrice', apiName: 'max_price' },\n { key: 'requireParameters', apiName: 'require_parameters' },\n { key: 'dataCollection', apiName: 'data_collection' },\n { key: 'zdr', apiName: 'zdr' },\n { key: 'enforceDistillableText', apiName: 'enforce_distillable_text' },\n { key: 'preferredMinThroughput', apiName: 'preferred_min_throughput' },\n { key: 'preferredMaxLatency', apiName: 'preferred_max_latency' },\n] as const\n\n/** Build the provider routing object for OpenRouter request body injection */\nexport function buildProviderRouting(\n provider?: ProviderConfig,\n): Record<string, unknown> | undefined {\n if (!provider) return undefined\n\n const result: Record<string, unknown> = {}\n\n for (const { key, apiName } of ARRAY_FIELDS) {\n const value = provider[key]\n if (value !== undefined) {\n const normalized = toArray(value as string | string[])\n if (normalized) result[apiName] = normalized\n }\n }\n\n for (const { key, apiName } of DIRECT_FIELDS) {\n const value = provider[key]\n if (value !== undefined) result[apiName] = value\n }\n\n if (result.order) {\n result.allow_fallbacks = provider.allowFallbacks ?? true\n }\n\n return Object.keys(result).length > 0 ? result : undefined\n}\n\n/** Score a pattern against a model name. Higher = better match. -1 = no match. */\nexport function matchScore(pattern: string, modelName: string): number {\n if (pattern === modelName) return modelName.length + 1000\n\n if (pattern.endsWith('*') && modelName.startsWith(pattern.slice(0, -1))) {\n return pattern.length\n }\n\n return -1\n}\n\n/** Resolve the effective config for a given model by merging global defaults with the best-matching override */\nexport function resolveModelConfig(\n config: ProxyConfig,\n modelName?: string,\n): ResolvedModelConfig {\n const result: ResolvedModelConfig = {\n provider: config.provider,\n headers: config.headers ? { ...config.headers } : undefined,\n }\n\n if (!modelName || !config.modelOverrides) return result\n\n let bestPattern: string | null = null\n let bestScore = -1\n\n for (const pattern of Object.keys(config.modelOverrides)) {\n const score = matchScore(pattern, modelName)\n if (score > bestScore) {\n bestScore = score\n bestPattern = pattern\n }\n }\n\n if (bestPattern) {\n const override = config.modelOverrides[bestPattern]\n if (override?.provider !== undefined) {\n result.provider = override.provider\n }\n if (override?.headers) {\n result.headers = { ...(result.headers ?? {}), ...override.headers }\n }\n }\n\n return result\n}\n\nexport async function loadConfig(options: LoadConfigOptions): Promise<ProxyConfig> {\n const config = { ...DEFAULT_CONFIG }\n\n if (!options.noConfig) {\n const configPath = findConfigFile(options.configPath)\n if (configPath) {\n const fileConfig = readConfigFile(configPath)\n Object.assign(config, fileConfig)\n }\n }\n\n if (options.host) config.host = options.host\n if (options.port) config.port = options.port\n if (options.verbose) config.verbose = options.verbose\n\n if (options.openrouterKey) {\n config.openrouterKey = options.openrouterKey\n } else if (!config.openrouterKey) {\n config.openrouterKey = process.env.OPENROUTER_API_KEY ?? ''\n }\n\n if (!config.openrouterKey) {\n throw new Error(\n 'OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.',\n )\n }\n\n return config\n}\n\n/** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */\nfunction getXdgConfigDir(): string {\n const xdgHome = process.env.XDG_CONFIG_HOME\n return xdgHome ? resolve(xdgHome, 'proxitor') : join(homedir(), '.config', 'proxitor')\n}\n\nexport function findConfigFile(explicitPath?: string): string | null {\n if (explicitPath) {\n if (!existsSync(explicitPath)) {\n throw new Error(`Config file not found: ${explicitPath}`)\n }\n return resolve(explicitPath)\n }\n\n const localCandidates = [\n 'proxitor.config.yaml',\n 'proxitor.config.yml',\n 'proxitor.config.json',\n '.proxitor.yaml',\n '.proxitor.yml',\n '.proxitor.json',\n ]\n\n for (const candidate of localCandidates) {\n const fullPath = resolve(candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n const xdgDir = getXdgConfigDir()\n const xdgCandidates = ['config.yaml', 'config.yml', 'config.json']\n\n for (const candidate of xdgCandidates) {\n const fullPath = join(xdgDir, candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n return null\n}\n\nexport function readConfigFile(filePath: string): Partial<ProxyConfig> {\n const content = readFileSync(filePath, 'utf-8')\n let raw: unknown\n\n try {\n raw = filePath.endsWith('.json') ? JSON.parse(content) : yaml.load(content)\n } catch (err) {\n // biome-ignore lint/nursery/useErrorCause: cause is propagated inside ConfigParseError\n throw new ConfigParseError(filePath, err instanceof Error ? err : undefined)\n }\n\n const result = proxyConfigFileSchema.safeParse(raw)\n if (!result.success) {\n throw new ConfigValidationError(filePath, result.error)\n }\n\n return result.data\n}\n","import { consola } from 'consola'\n\nexport const logger = consola.withTag('proxitor')\n","import type { ProxyConfig } from '../config.js'\n\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n])\n\n/** Headers to strip from client request before forwarding */\nconst STRIP_REQUEST = new Set(['authorization', 'x-api-key', 'host', 'content-length'])\n\n/** Headers to strip from upstream response before forwarding */\nconst STRIP_RESPONSE = new Set(['content-length', 'content-encoding'])\n\n/** Filter headers by removing hop-by-hop and an additional blocklist */\nfunction filterHeaders(\n incoming: Headers,\n blocklist: ReadonlySet<string>,\n): Record<string, string> {\n const headers: Record<string, string> = {}\n for (const [key, value] of incoming.entries()) {\n const lower = key.toLowerCase()\n if (HOP_BY_HOP.has(lower)) continue\n if (blocklist.has(lower)) continue\n headers[key] = value\n }\n return headers\n}\n\n/** Build request headers for upstream fetch */\nexport function buildRequestHeaders(\n incoming: Headers,\n config: ProxyConfig,\n inject: boolean,\n extraHeaders?: Record<string, string>,\n): Record<string, string> {\n const headers = filterHeaders(incoming, STRIP_REQUEST)\n\n headers.Authorization = `Bearer ${config.openrouterKey}`\n headers['HTTP-Referer'] = config.attributionReferer\n headers['X-Title'] = config.attributionTitle\n headers['Accept-Encoding'] = 'identity'\n\n if (extraHeaders) {\n Object.assign(headers, extraHeaders)\n }\n\n if (inject) {\n headers['Content-Type'] = 'application/json'\n }\n\n return headers\n}\n\n/** Filter response headers and add SSE-friendly defaults */\nexport function buildResponseHeaders(from: Headers): Record<string, string> {\n const headers = filterHeaders(from, STRIP_RESPONSE)\n\n headers['Cache-Control'] = 'no-cache'\n headers['X-Accel-Buffering'] = 'no'\n\n return headers\n}\n","import { tryParseBody } from '../utils.js'\n\n/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */\nexport function extractModel(rawBody: ArrayBuffer): string | undefined {\n const json = tryParseBody(rawBody)\n return typeof json?.model === 'string' ? json.model : undefined\n}\n\n/** Inject provider routing into request body, always overwriting existing value */\nexport function injectProvider(\n rawBody: ArrayBuffer,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer {\n if (rawBody.byteLength === 0) {\n throw new Error('Request body is empty; cannot inject provider')\n }\n\n let json: Record<string, unknown>\n try {\n json = JSON.parse(new TextDecoder().decode(rawBody)) as Record<string, unknown>\n } catch (parseError) {\n throw new Error('Request body is not valid JSON; cannot inject provider', {\n cause: parseError,\n })\n }\n\n const modified = { ...json, provider: providerRouting }\n return new TextEncoder().encode(JSON.stringify(modified)).buffer as ArrayBuffer\n}\n","import type { ProxyConfig } from '../config.js'\n\n/**\n * Paths where provider routing is injected into the request body.\n * All three are OpenRouter-supported endpoints:\n * /v1/chat/completions — OpenAI Chat Completions\n * /v1/responses — OpenAI Responses API\n * /v1/messages — Anthropic Messages API\n */\nexport const INJECT_PATHS = new Set([\n '/v1/chat/completions',\n '/v1/responses',\n '/v1/messages',\n])\n\n/** Check if this request should have provider routing injected */\nexport function shouldInject(method: string, path: string): boolean {\n return method === 'POST' && INJECT_PATHS.has(path)\n}\n\n/** Strip /v1 prefix from path: /v1/chat/completions → /chat/completions */\nexport function toUpstreamPath(pathname: string): string {\n if (pathname.startsWith('/v1')) {\n return pathname.slice('/v1'.length)\n }\n return pathname\n}\n\n/** Build full upstream URL from request and config */\nexport function buildUpstreamUrl(requestUrl: string, config: ProxyConfig): string {\n const { pathname } = new URL(requestUrl)\n return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`\n}\n","import { type HttpBindings, type ServerType, serve } from '@hono/node-server'\nimport { Hono } from 'hono'\nimport { buildProviderRouting, type ProxyConfig, resolveModelConfig } from './config.js'\nimport { logger } from './logger.js'\nimport { buildRequestHeaders, buildResponseHeaders } from './proxy/headers.js'\nimport { extractModel, injectProvider } from './proxy/inject.js'\nimport { buildUpstreamUrl, shouldInject } from './proxy/paths.js'\n\ntype ProxyContext = {\n Variables: {\n config: ProxyConfig\n }\n Bindings: HttpBindings\n}\n\nfunction readRequestBody(\n method: string,\n raw: ArrayBuffer,\n inject: boolean,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer | undefined {\n if (['GET', 'HEAD'].includes(method)) return undefined\n\n if (inject) {\n return injectProvider(raw, providerRouting)\n }\n\n return raw.byteLength > 0 ? raw : undefined\n}\n\nasync function fetchUpstream(\n url: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n): Promise<Response> {\n return fetch(url, {\n method,\n headers,\n body,\n signal,\n duplex: body ? 'half' : undefined,\n })\n}\n\nfunction buildUpstreamResponse(upstream: Response, method: string): Response {\n const headers = buildResponseHeaders(upstream.headers)\n\n if (method === 'HEAD' || !upstream.body) {\n return new Response(null, { status: upstream.status, headers })\n }\n\n return new Response(upstream.body, { status: upstream.status, headers })\n}\n\n/** Read and process the request body, returning an error response on failure */\nasync function readRawBody(\n request: Request,\n): Promise<{ ok: true; body: ArrayBuffer } | { ok: false; response: Response }> {\n try {\n const body = await request.arrayBuffer()\n return { ok: true, body }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to read request body'\n logger.error(message)\n return {\n ok: false,\n response: Response.json(\n { error: { message, type: 'proxy_request_error' } },\n { status: 400 },\n ),\n }\n }\n}\n\ntype ResolvedRequest = {\n inject: boolean\n body: ArrayBuffer | undefined\n modelName: string | undefined\n headers: Record<string, string> | undefined\n error?: Response\n}\n\n/** Resolve per-request config: extract model, resolve overrides, build routing and body */\nfunction resolveRequest(\n rawBody: ArrayBuffer,\n config: ProxyConfig,\n method: string,\n path: string,\n): ResolvedRequest {\n const modelName = extractModel(rawBody)\n const resolved = resolveModelConfig(config, modelName)\n const providerRouting = buildProviderRouting(resolved.provider)\n const inject = shouldInject(method, path) && providerRouting !== undefined\n\n let body: ArrayBuffer | undefined\n try {\n body = readRequestBody(\n method,\n rawBody,\n inject,\n providerRouting as Record<string, unknown>,\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to process request body'\n logger.error(message)\n return {\n inject,\n body: undefined,\n modelName,\n headers: resolved.headers,\n error: new Response(\n JSON.stringify({ error: { message, type: 'proxy_request_error' } }),\n { status: 400, headers: { 'Content-Type': 'application/json' } },\n ),\n }\n }\n\n return { inject, body, modelName, headers: resolved.headers }\n}\n\n/** Execute upstream fetch, returning appropriate error responses on failure */\nasync function executeUpstream(\n upstreamUrl: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n path: string,\n startedAt: number,\n): Promise<Response> {\n let upstream: Response\n try {\n upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal)\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n logger.warn(`Aborted: ${method} ${path}`)\n return new Response(null, { status: 499 })\n }\n\n logger.error('Upstream fetch error:', err)\n return Response.json(\n {\n error: {\n message: 'Proxy failed to reach upstream',\n type: 'proxy_upstream_error',\n },\n },\n { status: 502 },\n )\n }\n\n logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`)\n return buildUpstreamResponse(upstream, method)\n}\n\nexport function createProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const app = new Hono<ProxyContext>()\n\n app.get('/health', c => {\n const globalRouting = buildProviderRouting(config.provider)\n return c.json({\n ok: true,\n upstream: config.openrouterBaseUrl,\n provider: globalRouting ?? 'not configured',\n modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : [],\n })\n })\n\n app.all('*', async c => {\n const method = c.req.method\n const path = new URL(c.req.url).pathname\n const upstreamUrl = buildUpstreamUrl(c.req.url, config)\n const startedAt = Date.now()\n\n const raw = await readRawBody(c.req.raw)\n if (!raw.ok) return raw.response\n\n const resolved = resolveRequest(raw.body, config, method, path)\n if (resolved.error) return resolved.error\n\n const headers = buildRequestHeaders(\n c.req.raw.headers,\n config,\n resolved.inject,\n resolved.headers,\n )\n\n const controller = new AbortController()\n c.req.raw.signal.addEventListener('abort', () => controller.abort())\n\n const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : ''\n logger.info(\n `${method} ${path} → ${upstreamUrl}${resolved.inject ? ' [inject]' : ''}${modelLog}`,\n )\n\n return executeUpstream(\n upstreamUrl,\n method,\n headers,\n resolved.body,\n controller.signal,\n path,\n startedAt,\n )\n })\n\n return serve(\n {\n fetch: app.fetch,\n port: config.port,\n hostname: config.host,\n },\n onReady,\n )\n}\n\n/** Shutdown deadline: force-close after this many ms */\nconst SHUTDOWN_TIMEOUT_MS = 10_000\n\n/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */\nexport function startProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const server = createProxyServer(config, onReady)\n\n let shuttingDown = false\n\n function shutdown(signal: string) {\n if (shuttingDown) return\n shuttingDown = true\n\n logger.info(`${signal} received — draining active connections…`)\n\n const timer = setTimeout(() => {\n logger.warn('Forcing shutdown — drain timeout exceeded')\n process.exit(1)\n }, SHUTDOWN_TIMEOUT_MS)\n\n server.close(() => {\n clearTimeout(timer)\n logger.info('All connections drained — goodbye')\n process.exit(0)\n })\n }\n\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n process.on('SIGINT', () => shutdown('SIGINT'))\n\n return server\n}\n"],"mappings":"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"}
package/dist/proxy.mjs CHANGED
@@ -2,9 +2,91 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import * as yaml from "js-yaml";
5
+ import { z } from "zod";
5
6
  import { consola } from "consola";
6
7
  import { serve } from "@hono/node-server";
7
8
  import { Hono } from "hono";
9
+ //#region src/config-schema.ts
10
+ /** Percentile cutoffs for performance thresholds */
11
+ const percentileCutoffsSchema = z.object({
12
+ p50: z.number().positive().optional(),
13
+ p75: z.number().positive().optional(),
14
+ p90: z.number().positive().optional(),
15
+ p99: z.number().positive().optional()
16
+ }).strict();
17
+ /** Provider sorting options */
18
+ const providerSortSchema = z.union([z.enum([
19
+ "price",
20
+ "throughput",
21
+ "latency"
22
+ ]), z.object({
23
+ by: z.enum([
24
+ "price",
25
+ "throughput",
26
+ "latency"
27
+ ]),
28
+ partition: z.enum(["model", "none"]).optional()
29
+ }).strict()]);
30
+ /** Maximum pricing for a request */
31
+ const maxPriceSchema = z.object({
32
+ prompt: z.number().nonnegative().optional(),
33
+ completion: z.number().nonnegative().optional(),
34
+ request: z.number().nonnegative().optional(),
35
+ image: z.number().nonnegative().optional()
36
+ }).strict();
37
+ /** Provider routing configuration */
38
+ const providerConfigSchema = z.object({
39
+ only: z.union([z.string(), z.array(z.string())]).optional(),
40
+ order: z.union([z.string(), z.array(z.string())]).optional(),
41
+ ignore: z.union([z.string(), z.array(z.string())]).optional(),
42
+ allowFallbacks: z.boolean().optional(),
43
+ sort: providerSortSchema.optional(),
44
+ quantizations: z.array(z.string()).optional(),
45
+ maxPrice: maxPriceSchema.optional(),
46
+ requireParameters: z.boolean().optional(),
47
+ dataCollection: z.enum(["allow", "deny"]).optional(),
48
+ zdr: z.boolean().optional(),
49
+ enforceDistillableText: z.boolean().optional(),
50
+ preferredMinThroughput: z.union([z.number().positive(), percentileCutoffsSchema]).optional(),
51
+ preferredMaxLatency: z.union([z.number().positive(), percentileCutoffsSchema]).optional()
52
+ }).strict();
53
+ /** Per-model override: layers on top of global config */
54
+ const modelOverrideSchema = z.object({
55
+ provider: providerConfigSchema.optional(),
56
+ headers: z.record(z.string(), z.string()).optional()
57
+ }).strict();
58
+ /** Schema for validating raw file content — all top-level keys optional */
59
+ const proxyConfigFileSchema = z.object({
60
+ host: z.string().min(1),
61
+ port: z.number().int().min(1).max(65535),
62
+ openrouterKey: z.string(),
63
+ openrouterBaseUrl: z.string().url(),
64
+ verbose: z.boolean(),
65
+ bodyLimit: z.string().min(1),
66
+ provider: providerConfigSchema.optional(),
67
+ attributionReferer: z.string().min(1),
68
+ attributionTitle: z.string().min(1),
69
+ headers: z.record(z.string(), z.string()).optional(),
70
+ modelOverrides: z.record(z.string().min(1), modelOverrideSchema).optional()
71
+ }).strict().partial();
72
+ /** Wraps YAML/JSON parse errors with the config file path */
73
+ var ConfigParseError = class extends Error {
74
+ constructor(filePath, cause) {
75
+ super(`Failed to parse config file ${filePath}: ${cause?.message ?? "unknown error"}`, { cause });
76
+ this.name = "ConfigParseError";
77
+ }
78
+ };
79
+ /** Formats zod validation issues into a readable multi-line message */
80
+ var ConfigValidationError = class extends Error {
81
+ constructor(filePath, zodError) {
82
+ const lines = zodError.issues.map((issue) => {
83
+ return ` ${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
84
+ });
85
+ super(`Invalid config in ${filePath}:\n${lines.join("\n")}`);
86
+ this.name = "ConfigValidationError";
87
+ }
88
+ };
89
+ //#endregion
8
90
  //#region src/utils.ts
9
91
  /** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
10
92
  function toArray(value) {
@@ -188,8 +270,15 @@ function findConfigFile(explicitPath) {
188
270
  }
189
271
  function readConfigFile(filePath) {
190
272
  const content = readFileSync(filePath, "utf-8");
191
- if (filePath.endsWith(".json")) return JSON.parse(content);
192
- return yaml.load(content);
273
+ let raw;
274
+ try {
275
+ raw = filePath.endsWith(".json") ? JSON.parse(content) : yaml.load(content);
276
+ } catch (err) {
277
+ throw new ConfigParseError(filePath, err instanceof Error ? err : void 0);
278
+ }
279
+ const result = proxyConfigFileSchema.safeParse(raw);
280
+ if (!result.success) throw new ConfigValidationError(filePath, result.error);
281
+ return result.data;
193
282
  }
194
283
  //#endregion
195
284
  //#region src/logger.ts
@@ -284,14 +373,15 @@ const INJECT_PATHS = new Set([
284
373
  function shouldInject(method, path) {
285
374
  return method === "POST" && INJECT_PATHS.has(path);
286
375
  }
287
- /** Strip /v1 prefix: /v1/chat/completions → /chat/completions */
288
- function toUpstreamPath(originalUrl) {
289
- if (originalUrl.startsWith("/v1")) return originalUrl.slice(3);
290
- return originalUrl;
376
+ /** Strip /v1 prefix from path: /v1/chat/completions → /chat/completions */
377
+ function toUpstreamPath(pathname) {
378
+ if (pathname.startsWith("/v1")) return pathname.slice(3);
379
+ return pathname;
291
380
  }
292
381
  /** Build full upstream URL from request and config */
293
- function buildUpstreamUrl(originalUrl, config) {
294
- return `${config.openrouterBaseUrl}${toUpstreamPath(originalUrl)}`;
382
+ function buildUpstreamUrl(requestUrl, config) {
383
+ const { pathname } = new URL(requestUrl);
384
+ return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`;
295
385
  }
296
386
  //#endregion
297
387
  //#region src/proxy.ts
@@ -449,6 +539,6 @@ function startProxyServer(config, onReady) {
449
539
  return server;
450
540
  }
451
541
  //#endregion
452
- export { buildProviderRouting as a, resolveModelConfig as c, logger as i, toArray as l, startProxyServer as n, loadConfig as o, extractModel as r, matchScore as s, createProxyServer as t, tryParseBody as u };
542
+ export { buildProviderRouting as a, matchScore as c, toArray as d, tryParseBody as f, logger as i, readConfigFile as l, ConfigValidationError as m, startProxyServer as n, findConfigFile as o, ConfigParseError as p, extractModel as r, loadConfig as s, createProxyServer as t, resolveModelConfig as u };
453
543
 
454
544
  //# sourceMappingURL=proxy.mjs.map