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
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.mjs","names":[],"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.mjs","names":[],"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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"}
@@ -0,0 +1,38 @@
1
+ const require_proxy = require("./proxy.cjs");
2
+ const require_config = require("./config.cjs");
3
+ let _clack_prompts = require("@clack/prompts");
4
+ _clack_prompts = require_proxy.__toESM(_clack_prompts, 1);
5
+ //#region src/commands/config/remove.ts
6
+ /** Run the interactive "Remove model override" flow. */
7
+ async function removeOverrideCommand() {
8
+ _clack_prompts.intro("Remove Model Override");
9
+ const configPath = require_config.requireConfigPath();
10
+ const overrides = require_config.getModelOverrides(configPath);
11
+ const keys = Object.keys(overrides);
12
+ if (keys.length === 0) {
13
+ _clack_prompts.log.warn("No model overrides found.");
14
+ _clack_prompts.outro("");
15
+ return;
16
+ }
17
+ const selected = await _clack_prompts.multiselect({
18
+ message: "Select overrides to remove",
19
+ options: keys.map((k) => ({
20
+ value: k,
21
+ label: k
22
+ })),
23
+ required: true
24
+ });
25
+ if ((0, _clack_prompts.isCancel)(selected)) return;
26
+ const toRemove = selected;
27
+ const confirmed = await _clack_prompts.confirm({ message: `Remove ${toRemove.length} override(s)?` });
28
+ if ((0, _clack_prompts.isCancel)(confirmed) || !confirmed) {
29
+ _clack_prompts.outro("Cancelled");
30
+ return;
31
+ }
32
+ for (const key of toRemove) require_config.removeModelOverride(configPath, key);
33
+ _clack_prompts.outro(`✓ ${toRemove.length} override(s) removed`);
34
+ }
35
+ //#endregion
36
+ exports.removeOverrideCommand = removeOverrideCommand;
37
+
38
+ //# sourceMappingURL=remove.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove.cjs","names":["requireConfigPath","getModelOverrides","clack"],"sources":["../src/commands/config/remove.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport { getModelOverrides, removeModelOverride, requireConfigPath } from './config.js'\n\n/** Run the interactive \"Remove model override\" flow. */\nexport async function removeOverrideCommand(): Promise<void> {\n clack.intro('Remove Model Override')\n\n const configPath = requireConfigPath()\n const overrides = getModelOverrides(configPath)\n const keys = Object.keys(overrides)\n\n if (keys.length === 0) {\n clack.log.warn('No model overrides found.')\n clack.outro('')\n return\n }\n\n const selected = await clack.multiselect({\n message: 'Select overrides to remove',\n options: keys.map(k => ({\n value: k,\n label: k,\n })),\n required: true,\n })\n\n if (isCancel(selected)) return\n\n const toRemove = selected as string[]\n\n const confirmed = await clack.confirm({\n message: `Remove ${toRemove.length} override(s)?`,\n })\n\n if (isCancel(confirmed) || !confirmed) {\n clack.outro('Cancelled')\n return\n }\n\n for (const key of toRemove) {\n removeModelOverride(configPath, key)\n }\n\n clack.outro(`✓ ${toRemove.length} override(s) removed`)\n}\n"],"mappings":";;;;;;AAKA,eAAsB,wBAAuC;CAC3D,eAAM,MAAM,uBAAuB;CAEnC,MAAM,aAAaA,eAAAA,kBAAkB;CACrC,MAAM,YAAYC,eAAAA,kBAAkB,UAAU;CAC9C,MAAM,OAAO,OAAO,KAAK,SAAS;CAElC,IAAI,KAAK,WAAW,GAAG;EACrB,eAAM,IAAI,KAAK,2BAA2B;EAC1C,eAAM,MAAM,EAAE;EACd;CACF;CAEA,MAAM,WAAW,MAAMC,eAAM,YAAY;EACvC,SAAS;EACT,SAAS,KAAK,KAAI,OAAM;GACtB,OAAO;GACP,OAAO;EACT,EAAE;EACF,UAAU;CACZ,CAAC;CAED,KAAA,GAAA,eAAA,UAAa,QAAQ,GAAG;CAExB,MAAM,WAAW;CAEjB,MAAM,YAAY,MAAMA,eAAM,QAAQ,EACpC,SAAS,UAAU,SAAS,OAAO,eACrC,CAAC;CAED,KAAA,GAAA,eAAA,UAAa,SAAS,KAAK,CAAC,WAAW;EACrC,eAAM,MAAM,WAAW;EACvB;CACF;CAEA,KAAK,MAAM,OAAO,UAChB,eAAA,oBAAoB,YAAY,GAAG;CAGrC,eAAM,MAAM,KAAK,SAAS,OAAO,qBAAqB;AACxD"}
@@ -0,0 +1,37 @@
1
+ import { n as removeModelOverride, r as requireConfigPath, t as getModelOverrides } from "./config.mjs";
2
+ import * as clack from "@clack/prompts";
3
+ import { isCancel } from "@clack/prompts";
4
+ //#region src/commands/config/remove.ts
5
+ /** Run the interactive "Remove model override" flow. */
6
+ async function removeOverrideCommand() {
7
+ clack.intro("Remove Model Override");
8
+ const configPath = requireConfigPath();
9
+ const overrides = getModelOverrides(configPath);
10
+ const keys = Object.keys(overrides);
11
+ if (keys.length === 0) {
12
+ clack.log.warn("No model overrides found.");
13
+ clack.outro("");
14
+ return;
15
+ }
16
+ const selected = await clack.multiselect({
17
+ message: "Select overrides to remove",
18
+ options: keys.map((k) => ({
19
+ value: k,
20
+ label: k
21
+ })),
22
+ required: true
23
+ });
24
+ if (isCancel(selected)) return;
25
+ const toRemove = selected;
26
+ const confirmed = await clack.confirm({ message: `Remove ${toRemove.length} override(s)?` });
27
+ if (isCancel(confirmed) || !confirmed) {
28
+ clack.outro("Cancelled");
29
+ return;
30
+ }
31
+ for (const key of toRemove) removeModelOverride(configPath, key);
32
+ clack.outro(`✓ ${toRemove.length} override(s) removed`);
33
+ }
34
+ //#endregion
35
+ export { removeOverrideCommand };
36
+
37
+ //# sourceMappingURL=remove.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove.mjs","names":[],"sources":["../src/commands/config/remove.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport { getModelOverrides, removeModelOverride, requireConfigPath } from './config.js'\n\n/** Run the interactive \"Remove model override\" flow. */\nexport async function removeOverrideCommand(): Promise<void> {\n clack.intro('Remove Model Override')\n\n const configPath = requireConfigPath()\n const overrides = getModelOverrides(configPath)\n const keys = Object.keys(overrides)\n\n if (keys.length === 0) {\n clack.log.warn('No model overrides found.')\n clack.outro('')\n return\n }\n\n const selected = await clack.multiselect({\n message: 'Select overrides to remove',\n options: keys.map(k => ({\n value: k,\n label: k,\n })),\n required: true,\n })\n\n if (isCancel(selected)) return\n\n const toRemove = selected as string[]\n\n const confirmed = await clack.confirm({\n message: `Remove ${toRemove.length} override(s)?`,\n })\n\n if (isCancel(confirmed) || !confirmed) {\n clack.outro('Cancelled')\n return\n }\n\n for (const key of toRemove) {\n removeModelOverride(configPath, key)\n }\n\n clack.outro(`✓ ${toRemove.length} override(s) removed`)\n}\n"],"mappings":";;;;;AAKA,eAAsB,wBAAuC;CAC3D,MAAM,MAAM,uBAAuB;CAEnC,MAAM,aAAa,kBAAkB;CACrC,MAAM,YAAY,kBAAkB,UAAU;CAC9C,MAAM,OAAO,OAAO,KAAK,SAAS;CAElC,IAAI,KAAK,WAAW,GAAG;EACrB,MAAM,IAAI,KAAK,2BAA2B;EAC1C,MAAM,MAAM,EAAE;EACd;CACF;CAEA,MAAM,WAAW,MAAM,MAAM,YAAY;EACvC,SAAS;EACT,SAAS,KAAK,KAAI,OAAM;GACtB,OAAO;GACP,OAAO;EACT,EAAE;EACF,UAAU;CACZ,CAAC;CAED,IAAI,SAAS,QAAQ,GAAG;CAExB,MAAM,WAAW;CAEjB,MAAM,YAAY,MAAM,MAAM,QAAQ,EACpC,SAAS,UAAU,SAAS,OAAO,eACrC,CAAC;CAED,IAAI,SAAS,SAAS,KAAK,CAAC,WAAW;EACrC,MAAM,MAAM,WAAW;EACvB;CACF;CAEA,KAAK,MAAM,OAAO,UAChB,oBAAoB,YAAY,GAAG;CAGrC,MAAM,MAAM,KAAK,SAAS,OAAO,qBAAqB;AACxD"}
@@ -0,0 +1,26 @@
1
+ const require_proxy = require("./proxy.cjs");
2
+ let _clack_prompts = require("@clack/prompts");
3
+ _clack_prompts = require_proxy.__toESM(_clack_prompts, 1);
4
+ //#region src/commands/config/validate.ts
5
+ /** Run config validation and display results. */
6
+ async function validateConfigCommand() {
7
+ _clack_prompts.intro("Validate Config");
8
+ const configPath = require_proxy.findConfigFile();
9
+ if (!configPath) {
10
+ _clack_prompts.log.error("No config file found.");
11
+ _clack_prompts.outro("");
12
+ return;
13
+ }
14
+ _clack_prompts.log.info(`Checking ${configPath}...`);
15
+ try {
16
+ require_proxy.readConfigFile(configPath);
17
+ _clack_prompts.log.success("Config is valid ✓");
18
+ } catch (error) {
19
+ _clack_prompts.log.error(String(error));
20
+ }
21
+ _clack_prompts.outro("");
22
+ }
23
+ //#endregion
24
+ exports.validateConfigCommand = validateConfigCommand;
25
+
26
+ //# sourceMappingURL=validate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.cjs","names":["findConfigFile"],"sources":["../src/commands/config/validate.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { findConfigFile, readConfigFile } from '../../config.js'\n\n/** Run config validation and display results. */\nexport async function validateConfigCommand(): Promise<void> {\n clack.intro('Validate Config')\n\n const configPath = findConfigFile()\n if (!configPath) {\n clack.log.error('No config file found.')\n clack.outro('')\n return\n }\n\n clack.log.info(`Checking ${configPath}...`)\n\n try {\n readConfigFile(configPath)\n clack.log.success('Config is valid ✓')\n } catch (error) {\n clack.log.error(String(error))\n }\n\n clack.outro('')\n}\n"],"mappings":";;;;;AAIA,eAAsB,wBAAuC;CAC3D,eAAM,MAAM,iBAAiB;CAE7B,MAAM,aAAaA,cAAAA,eAAe;CAClC,IAAI,CAAC,YAAY;EACf,eAAM,IAAI,MAAM,uBAAuB;EACvC,eAAM,MAAM,EAAE;EACd;CACF;CAEA,eAAM,IAAI,KAAK,YAAY,WAAW,IAAI;CAE1C,IAAI;EACF,cAAA,eAAe,UAAU;EACzB,eAAM,IAAI,QAAQ,mBAAmB;CACvC,SAAS,OAAO;EACd,eAAM,IAAI,MAAM,OAAO,KAAK,CAAC;CAC/B;CAEA,eAAM,MAAM,EAAE;AAChB"}
@@ -0,0 +1,25 @@
1
+ import { l as readConfigFile, o as findConfigFile } from "./proxy.mjs";
2
+ import * as clack from "@clack/prompts";
3
+ //#region src/commands/config/validate.ts
4
+ /** Run config validation and display results. */
5
+ async function validateConfigCommand() {
6
+ clack.intro("Validate Config");
7
+ const configPath = findConfigFile();
8
+ if (!configPath) {
9
+ clack.log.error("No config file found.");
10
+ clack.outro("");
11
+ return;
12
+ }
13
+ clack.log.info(`Checking ${configPath}...`);
14
+ try {
15
+ readConfigFile(configPath);
16
+ clack.log.success("Config is valid ✓");
17
+ } catch (error) {
18
+ clack.log.error(String(error));
19
+ }
20
+ clack.outro("");
21
+ }
22
+ //#endregion
23
+ export { validateConfigCommand };
24
+
25
+ //# sourceMappingURL=validate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.mjs","names":[],"sources":["../src/commands/config/validate.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { findConfigFile, readConfigFile } from '../../config.js'\n\n/** Run config validation and display results. */\nexport async function validateConfigCommand(): Promise<void> {\n clack.intro('Validate Config')\n\n const configPath = findConfigFile()\n if (!configPath) {\n clack.log.error('No config file found.')\n clack.outro('')\n return\n }\n\n clack.log.info(`Checking ${configPath}...`)\n\n try {\n readConfigFile(configPath)\n clack.log.success('Config is valid ✓')\n } catch (error) {\n clack.log.error(String(error))\n }\n\n clack.outro('')\n}\n"],"mappings":";;;;AAIA,eAAsB,wBAAuC;CAC3D,MAAM,MAAM,iBAAiB;CAE7B,MAAM,aAAa,eAAe;CAClC,IAAI,CAAC,YAAY;EACf,MAAM,IAAI,MAAM,uBAAuB;EACvC,MAAM,MAAM,EAAE;EACd;CACF;CAEA,MAAM,IAAI,KAAK,YAAY,WAAW,IAAI;CAE1C,IAAI;EACF,eAAe,UAAU;EACzB,MAAM,IAAI,QAAQ,mBAAmB;CACvC,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,OAAO,KAAK,CAAC;CAC/B;CAEA,MAAM,MAAM,EAAE;AAChB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proxitor",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight proxy for routing CLI requests (claude-code, codex) to OpenRouter",
6
6
  "files": [
@@ -44,13 +44,16 @@
44
44
  },
45
45
  "homepage": "https://github.com/neiromaster/proxitor#readme",
46
46
  "dependencies": {
47
+ "@clack/prompts": "^1.5.0",
47
48
  "@hono/node-server": "^2.0.4",
48
- "cac": "^7.0.0",
49
+ "cmd-ts": "^0.15.0",
49
50
  "conf": "^15.1.0",
50
51
  "consola": "^3.4.2",
51
52
  "dotenv": "^17.4.2",
52
53
  "hono": "^4.12.23",
53
- "js-yaml": "^4.2.0"
54
+ "js-yaml": "^4.2.0",
55
+ "yaml": "^2.9.0",
56
+ "zod": "^4.4.3"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@biomejs/biome": "^2.4.16",
@@ -66,6 +69,8 @@
66
69
  "scripts": {
67
70
  "build": "tsdown",
68
71
  "dev": "tsdown --watch",
72
+ "start": "node dist/cli.mjs",
73
+ "start:dev": "pnpm run build && pnpm run start",
69
74
  "lint": "biome lint ./src",
70
75
  "lint:fix": "biome lint --fix ./src",
71
76
  "format": "biome format --write ./src",
@@ -75,6 +80,8 @@
75
80
  "test": "vitest run",
76
81
  "test:watch": "vitest",
77
82
  "test:coverage": "vitest run --coverage",
83
+ "test:integration": "vitest run --reporter verbose tests/integration/",
84
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
78
85
  "check": "npm run typecheck && npm run check:biome && npm run test",
79
86
  "version-packages": "changeset version",
80
87
  "release": "changeset publish"