proxitor 0.3.0 → 0.5.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 (62) hide show
  1. package/README.md +27 -8
  2. package/dist/add.mjs +26 -27
  3. package/dist/add.mjs.map +1 -1
  4. package/dist/browse.mjs +20 -21
  5. package/dist/browse.mjs.map +1 -1
  6. package/dist/cli.mjs +14774 -33
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/config.mjs +6 -5
  9. package/dist/config.mjs.map +1 -1
  10. package/dist/config2.mjs +5 -6
  11. package/dist/config2.mjs.map +1 -1
  12. package/dist/dist.mjs +1325 -0
  13. package/dist/dist.mjs.map +1 -0
  14. package/dist/dist2.mjs +6617 -0
  15. package/dist/dist2.mjs.map +1 -0
  16. package/dist/edit.mjs +17 -18
  17. package/dist/edit.mjs.map +1 -1
  18. package/dist/list.mjs +4 -4
  19. package/dist/list.mjs.map +1 -1
  20. package/dist/prompt.mjs +849 -0
  21. package/dist/prompt.mjs.map +1 -0
  22. package/dist/providers.mjs +16 -16
  23. package/dist/providers.mjs.map +1 -1
  24. package/dist/remove.mjs +10 -11
  25. package/dist/remove.mjs.map +1 -1
  26. package/dist/validate.mjs +9 -9
  27. package/dist/validate.mjs.map +1 -1
  28. package/dist/wizard.mjs +222 -0
  29. package/dist/wizard.mjs.map +1 -0
  30. package/package.json +1 -16
  31. package/dist/add.cjs +0 -139
  32. package/dist/add.cjs.map +0 -1
  33. package/dist/browse.cjs +0 -88
  34. package/dist/browse.cjs.map +0 -1
  35. package/dist/cli.cjs +0 -159
  36. package/dist/cli.cjs.map +0 -1
  37. package/dist/cli.d.cts +0 -1
  38. package/dist/cli.d.mts +0 -1
  39. package/dist/config.cjs +0 -68
  40. package/dist/config.cjs.map +0 -1
  41. package/dist/config2.cjs +0 -75
  42. package/dist/config2.cjs.map +0 -1
  43. package/dist/edit.cjs +0 -82
  44. package/dist/edit.cjs.map +0 -1
  45. package/dist/index.cjs +0 -12
  46. package/dist/index.d.cts +0 -261
  47. package/dist/index.d.cts.map +0 -1
  48. package/dist/index.d.mts +0 -261
  49. package/dist/index.d.mts.map +0 -1
  50. package/dist/index.mjs +0 -2
  51. package/dist/list.cjs +0 -33
  52. package/dist/list.cjs.map +0 -1
  53. package/dist/providers.cjs +0 -376
  54. package/dist/providers.cjs.map +0 -1
  55. package/dist/proxy.cjs +0 -656
  56. package/dist/proxy.cjs.map +0 -1
  57. package/dist/proxy.mjs +0 -544
  58. package/dist/proxy.mjs.map +0 -1
  59. package/dist/remove.cjs +0 -38
  60. package/dist/remove.cjs.map +0 -1
  61. package/dist/validate.cjs +0 -26
  62. package/dist/validate.cjs.map +0 -1
@@ -1 +0,0 @@
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 DELETED
@@ -1,544 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import * as yaml from "js-yaml";
5
- import { z } from "zod";
6
- import { consola } from "consola";
7
- import { serve } from "@hono/node-server";
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
90
- //#region src/utils.ts
91
- /** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
92
- function toArray(value) {
93
- if (value === void 0) return void 0;
94
- const arr = Array.isArray(value) ? [...value] : [value];
95
- return arr.length > 0 ? arr : void 0;
96
- }
97
- /** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */
98
- function tryParseBody(raw) {
99
- if (raw.byteLength === 0) return void 0;
100
- try {
101
- return JSON.parse(new TextDecoder().decode(raw));
102
- } catch {
103
- return;
104
- }
105
- }
106
- //#endregion
107
- //#region src/config.ts
108
- const DEFAULT_CONFIG = {
109
- host: "0.0.0.0",
110
- port: 8080,
111
- openrouterKey: "",
112
- openrouterBaseUrl: "https://openrouter.ai/api/v1",
113
- verbose: false,
114
- bodyLimit: "50mb",
115
- attributionReferer: "http://localhost",
116
- attributionTitle: "proxitor"
117
- };
118
- /** Fields that need toArray normalization (string | string[] → string[] | undefined) */
119
- const ARRAY_FIELDS = [
120
- {
121
- key: "only",
122
- apiName: "only"
123
- },
124
- {
125
- key: "order",
126
- apiName: "order"
127
- },
128
- {
129
- key: "ignore",
130
- apiName: "ignore"
131
- },
132
- {
133
- key: "quantizations",
134
- apiName: "quantizations"
135
- }
136
- ];
137
- /** Direct camelCase → snake_case field mappings */
138
- const DIRECT_FIELDS = [
139
- {
140
- key: "sort",
141
- apiName: "sort"
142
- },
143
- {
144
- key: "maxPrice",
145
- apiName: "max_price"
146
- },
147
- {
148
- key: "requireParameters",
149
- apiName: "require_parameters"
150
- },
151
- {
152
- key: "dataCollection",
153
- apiName: "data_collection"
154
- },
155
- {
156
- key: "zdr",
157
- apiName: "zdr"
158
- },
159
- {
160
- key: "enforceDistillableText",
161
- apiName: "enforce_distillable_text"
162
- },
163
- {
164
- key: "preferredMinThroughput",
165
- apiName: "preferred_min_throughput"
166
- },
167
- {
168
- key: "preferredMaxLatency",
169
- apiName: "preferred_max_latency"
170
- }
171
- ];
172
- /** Build the provider routing object for OpenRouter request body injection */
173
- function buildProviderRouting(provider) {
174
- if (!provider) return void 0;
175
- const result = {};
176
- for (const { key, apiName } of ARRAY_FIELDS) {
177
- const value = provider[key];
178
- if (value !== void 0) {
179
- const normalized = toArray(value);
180
- if (normalized) result[apiName] = normalized;
181
- }
182
- }
183
- for (const { key, apiName } of DIRECT_FIELDS) {
184
- const value = provider[key];
185
- if (value !== void 0) result[apiName] = value;
186
- }
187
- if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
188
- return Object.keys(result).length > 0 ? result : void 0;
189
- }
190
- /** Score a pattern against a model name. Higher = better match. -1 = no match. */
191
- function matchScore(pattern, modelName) {
192
- if (pattern === modelName) return modelName.length + 1e3;
193
- if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
194
- return -1;
195
- }
196
- /** Resolve the effective config for a given model by merging global defaults with the best-matching override */
197
- function resolveModelConfig(config, modelName) {
198
- const result = {
199
- provider: config.provider,
200
- headers: config.headers ? { ...config.headers } : void 0
201
- };
202
- if (!modelName || !config.modelOverrides) return result;
203
- let bestPattern = null;
204
- let bestScore = -1;
205
- for (const pattern of Object.keys(config.modelOverrides)) {
206
- const score = matchScore(pattern, modelName);
207
- if (score > bestScore) {
208
- bestScore = score;
209
- bestPattern = pattern;
210
- }
211
- }
212
- if (bestPattern) {
213
- const override = config.modelOverrides[bestPattern];
214
- if (override?.provider !== void 0) result.provider = override.provider;
215
- if (override?.headers) result.headers = {
216
- ...result.headers ?? {},
217
- ...override.headers
218
- };
219
- }
220
- return result;
221
- }
222
- async function loadConfig(options) {
223
- const config = { ...DEFAULT_CONFIG };
224
- if (!options.noConfig) {
225
- const configPath = findConfigFile(options.configPath);
226
- if (configPath) {
227
- const fileConfig = readConfigFile(configPath);
228
- Object.assign(config, fileConfig);
229
- }
230
- }
231
- if (options.host) config.host = options.host;
232
- if (options.port) config.port = options.port;
233
- if (options.verbose) config.verbose = options.verbose;
234
- if (options.openrouterKey) config.openrouterKey = options.openrouterKey;
235
- else if (!config.openrouterKey) config.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
236
- if (!config.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.");
237
- return config;
238
- }
239
- /** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */
240
- function getXdgConfigDir() {
241
- const xdgHome = process.env.XDG_CONFIG_HOME;
242
- return xdgHome ? resolve(xdgHome, "proxitor") : join(homedir(), ".config", "proxitor");
243
- }
244
- function findConfigFile(explicitPath) {
245
- if (explicitPath) {
246
- if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${explicitPath}`);
247
- return resolve(explicitPath);
248
- }
249
- for (const candidate of [
250
- "proxitor.config.yaml",
251
- "proxitor.config.yml",
252
- "proxitor.config.json",
253
- ".proxitor.yaml",
254
- ".proxitor.yml",
255
- ".proxitor.json"
256
- ]) {
257
- const fullPath = resolve(candidate);
258
- if (existsSync(fullPath)) return fullPath;
259
- }
260
- const xdgDir = getXdgConfigDir();
261
- for (const candidate of [
262
- "config.yaml",
263
- "config.yml",
264
- "config.json"
265
- ]) {
266
- const fullPath = join(xdgDir, candidate);
267
- if (existsSync(fullPath)) return fullPath;
268
- }
269
- return null;
270
- }
271
- function readConfigFile(filePath) {
272
- const content = readFileSync(filePath, "utf-8");
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;
282
- }
283
- //#endregion
284
- //#region src/logger.ts
285
- const logger = consola.withTag("proxitor");
286
- //#endregion
287
- //#region src/proxy/headers.ts
288
- const HOP_BY_HOP = new Set([
289
- "connection",
290
- "keep-alive",
291
- "proxy-authenticate",
292
- "proxy-authorization",
293
- "te",
294
- "trailer",
295
- "transfer-encoding",
296
- "upgrade"
297
- ]);
298
- /** Headers to strip from client request before forwarding */
299
- const STRIP_REQUEST = new Set([
300
- "authorization",
301
- "x-api-key",
302
- "host",
303
- "content-length"
304
- ]);
305
- /** Headers to strip from upstream response before forwarding */
306
- const STRIP_RESPONSE = new Set(["content-length", "content-encoding"]);
307
- /** Filter headers by removing hop-by-hop and an additional blocklist */
308
- function filterHeaders(incoming, blocklist) {
309
- const headers = {};
310
- for (const [key, value] of incoming.entries()) {
311
- const lower = key.toLowerCase();
312
- if (HOP_BY_HOP.has(lower)) continue;
313
- if (blocklist.has(lower)) continue;
314
- headers[key] = value;
315
- }
316
- return headers;
317
- }
318
- /** Build request headers for upstream fetch */
319
- function buildRequestHeaders(incoming, config, inject, extraHeaders) {
320
- const headers = filterHeaders(incoming, STRIP_REQUEST);
321
- headers.Authorization = `Bearer ${config.openrouterKey}`;
322
- headers["HTTP-Referer"] = config.attributionReferer;
323
- headers["X-Title"] = config.attributionTitle;
324
- headers["Accept-Encoding"] = "identity";
325
- if (extraHeaders) Object.assign(headers, extraHeaders);
326
- if (inject) headers["Content-Type"] = "application/json";
327
- return headers;
328
- }
329
- /** Filter response headers and add SSE-friendly defaults */
330
- function buildResponseHeaders(from) {
331
- const headers = filterHeaders(from, STRIP_RESPONSE);
332
- headers["Cache-Control"] = "no-cache";
333
- headers["X-Accel-Buffering"] = "no";
334
- return headers;
335
- }
336
- //#endregion
337
- //#region src/proxy/inject.ts
338
- /** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */
339
- function extractModel(rawBody) {
340
- const json = tryParseBody(rawBody);
341
- return typeof json?.model === "string" ? json.model : void 0;
342
- }
343
- /** Inject provider routing into request body, always overwriting existing value */
344
- function injectProvider(rawBody, providerRouting) {
345
- if (rawBody.byteLength === 0) throw new Error("Request body is empty; cannot inject provider");
346
- let json;
347
- try {
348
- json = JSON.parse(new TextDecoder().decode(rawBody));
349
- } catch (parseError) {
350
- throw new Error("Request body is not valid JSON; cannot inject provider", { cause: parseError });
351
- }
352
- const modified = {
353
- ...json,
354
- provider: providerRouting
355
- };
356
- return new TextEncoder().encode(JSON.stringify(modified)).buffer;
357
- }
358
- //#endregion
359
- //#region src/proxy/paths.ts
360
- /**
361
- * Paths where provider routing is injected into the request body.
362
- * All three are OpenRouter-supported endpoints:
363
- * /v1/chat/completions — OpenAI Chat Completions
364
- * /v1/responses — OpenAI Responses API
365
- * /v1/messages — Anthropic Messages API
366
- */
367
- const INJECT_PATHS = new Set([
368
- "/v1/chat/completions",
369
- "/v1/responses",
370
- "/v1/messages"
371
- ]);
372
- /** Check if this request should have provider routing injected */
373
- function shouldInject(method, path) {
374
- return method === "POST" && INJECT_PATHS.has(path);
375
- }
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;
380
- }
381
- /** Build full upstream URL from request and config */
382
- function buildUpstreamUrl(requestUrl, config) {
383
- const { pathname } = new URL(requestUrl);
384
- return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`;
385
- }
386
- //#endregion
387
- //#region src/proxy.ts
388
- function readRequestBody(method, raw, inject, providerRouting) {
389
- if (["GET", "HEAD"].includes(method)) return void 0;
390
- if (inject) return injectProvider(raw, providerRouting);
391
- return raw.byteLength > 0 ? raw : void 0;
392
- }
393
- async function fetchUpstream(url, method, headers, body, signal) {
394
- return fetch(url, {
395
- method,
396
- headers,
397
- body,
398
- signal,
399
- duplex: body ? "half" : void 0
400
- });
401
- }
402
- function buildUpstreamResponse(upstream, method) {
403
- const headers = buildResponseHeaders(upstream.headers);
404
- if (method === "HEAD" || !upstream.body) return new Response(null, {
405
- status: upstream.status,
406
- headers
407
- });
408
- return new Response(upstream.body, {
409
- status: upstream.status,
410
- headers
411
- });
412
- }
413
- /** Read and process the request body, returning an error response on failure */
414
- async function readRawBody(request) {
415
- try {
416
- return {
417
- ok: true,
418
- body: await request.arrayBuffer()
419
- };
420
- } catch (err) {
421
- const message = err instanceof Error ? err.message : "Failed to read request body";
422
- logger.error(message);
423
- return {
424
- ok: false,
425
- response: Response.json({ error: {
426
- message,
427
- type: "proxy_request_error"
428
- } }, { status: 400 })
429
- };
430
- }
431
- }
432
- /** Resolve per-request config: extract model, resolve overrides, build routing and body */
433
- function resolveRequest(rawBody, config, method, path) {
434
- const modelName = extractModel(rawBody);
435
- const resolved = resolveModelConfig(config, modelName);
436
- const providerRouting = buildProviderRouting(resolved.provider);
437
- const inject = shouldInject(method, path) && providerRouting !== void 0;
438
- let body;
439
- try {
440
- body = readRequestBody(method, rawBody, inject, providerRouting);
441
- } catch (err) {
442
- const message = err instanceof Error ? err.message : "Failed to process request body";
443
- logger.error(message);
444
- return {
445
- inject,
446
- body: void 0,
447
- modelName,
448
- headers: resolved.headers,
449
- error: new Response(JSON.stringify({ error: {
450
- message,
451
- type: "proxy_request_error"
452
- } }), {
453
- status: 400,
454
- headers: { "Content-Type": "application/json" }
455
- })
456
- };
457
- }
458
- return {
459
- inject,
460
- body,
461
- modelName,
462
- headers: resolved.headers
463
- };
464
- }
465
- /** Execute upstream fetch, returning appropriate error responses on failure */
466
- async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt) {
467
- let upstream;
468
- try {
469
- upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal);
470
- } catch (err) {
471
- if (err instanceof DOMException && err.name === "AbortError") {
472
- logger.warn(`Aborted: ${method} ${path}`);
473
- return new Response(null, { status: 499 });
474
- }
475
- logger.error("Upstream fetch error:", err);
476
- return Response.json({ error: {
477
- message: "Proxy failed to reach upstream",
478
- type: "proxy_upstream_error"
479
- } }, { status: 502 });
480
- }
481
- logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`);
482
- return buildUpstreamResponse(upstream, method);
483
- }
484
- function createProxyServer(config, onReady) {
485
- const app = new Hono();
486
- app.get("/health", (c) => {
487
- const globalRouting = buildProviderRouting(config.provider);
488
- return c.json({
489
- ok: true,
490
- upstream: config.openrouterBaseUrl,
491
- provider: globalRouting ?? "not configured",
492
- modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : []
493
- });
494
- });
495
- app.all("*", async (c) => {
496
- const method = c.req.method;
497
- const path = new URL(c.req.url).pathname;
498
- const upstreamUrl = buildUpstreamUrl(c.req.url, config);
499
- const startedAt = Date.now();
500
- const raw = await readRawBody(c.req.raw);
501
- if (!raw.ok) return raw.response;
502
- const resolved = resolveRequest(raw.body, config, method, path);
503
- if (resolved.error) return resolved.error;
504
- const headers = buildRequestHeaders(c.req.raw.headers, config, resolved.inject, resolved.headers);
505
- const controller = new AbortController();
506
- c.req.raw.signal.addEventListener("abort", () => controller.abort());
507
- const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : "";
508
- logger.info(`${method} ${path} → ${upstreamUrl}${resolved.inject ? " [inject]" : ""}${modelLog}`);
509
- return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt);
510
- });
511
- return serve({
512
- fetch: app.fetch,
513
- port: config.port,
514
- hostname: config.host
515
- }, onReady);
516
- }
517
- /** Shutdown deadline: force-close after this many ms */
518
- const SHUTDOWN_TIMEOUT_MS = 1e4;
519
- /** Start the proxy with graceful shutdown on SIGTERM/SIGINT */
520
- function startProxyServer(config, onReady) {
521
- const server = createProxyServer(config, onReady);
522
- let shuttingDown = false;
523
- function shutdown(signal) {
524
- if (shuttingDown) return;
525
- shuttingDown = true;
526
- logger.info(`${signal} received — draining active connections…`);
527
- const timer = setTimeout(() => {
528
- logger.warn("Forcing shutdown — drain timeout exceeded");
529
- process.exit(1);
530
- }, SHUTDOWN_TIMEOUT_MS);
531
- server.close(() => {
532
- clearTimeout(timer);
533
- logger.info("All connections drained — goodbye");
534
- process.exit(0);
535
- });
536
- }
537
- process.on("SIGTERM", () => shutdown("SIGTERM"));
538
- process.on("SIGINT", () => shutdown("SIGINT"));
539
- return server;
540
- }
541
- //#endregion
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 };
543
-
544
- //# sourceMappingURL=proxy.mjs.map