@codeproxy/cli 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,782 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var http = require('http');
5
+ var stream = require('stream');
6
+ var path = require('path');
7
+ var core = require('@codeproxy/core');
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var http__default = /*#__PURE__*/_interopDefault(http);
12
+
13
+ // src/server/cli.ts
14
+ function fmtTime(date) {
15
+ return date.toLocaleTimeString("en-US", { hour12: false });
16
+ }
17
+ function fmtDuration(ms) {
18
+ if (ms < 1e3) {
19
+ return `${Math.round(ms)}ms`;
20
+ }
21
+ if (ms < 6e4) {
22
+ return `${(ms / 1e3).toFixed(1)}s`;
23
+ }
24
+ const minutes = Math.floor(ms / 6e4);
25
+ const seconds = Math.round(ms % 6e4 / 1e3);
26
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
27
+ }
28
+ async function startProxy(options) {
29
+ const host = options.host ?? "127.0.0.1";
30
+ const port = options.port ?? 8787;
31
+ const cors = options.cors ?? true;
32
+ const logger = options.logger === null ? null : options.logger ?? console;
33
+ const durationHistory = [];
34
+ function updateRollingAverage(ms) {
35
+ durationHistory.push(ms);
36
+ if (durationHistory.length > 50) {
37
+ durationHistory.shift();
38
+ }
39
+ return durationHistory.reduce((sum, val) => sum + val, 0) / durationHistory.length;
40
+ }
41
+ const activeRequests = /* @__PURE__ */ new Map();
42
+ let statusTimerId = null;
43
+ function drawStatusLine() {
44
+ if (activeRequests.size === 0) {
45
+ return;
46
+ }
47
+ const parts = Array.from(activeRequests.entries()).map(([, req]) => {
48
+ const elapsed = Date.now() - req.startTime;
49
+ return `[${fmtDuration(elapsed)}]`;
50
+ });
51
+ process.stdout.write(`\r\x1B[K\u23F3 ${parts.join(", ")}`);
52
+ }
53
+ const requestTracker = {
54
+ add(method, url) {
55
+ const id = `${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
56
+ activeRequests.set(id, { method, url, startTime: Date.now() });
57
+ drawStatusLine();
58
+ if (!statusTimerId) {
59
+ statusTimerId = setInterval(drawStatusLine, 150);
60
+ }
61
+ return id;
62
+ },
63
+ remove(id) {
64
+ activeRequests.delete(id);
65
+ if (activeRequests.size === 0) {
66
+ process.stdout.write("\r\x1B[K");
67
+ if (statusTimerId) {
68
+ clearInterval(statusTimerId);
69
+ statusTimerId = null;
70
+ }
71
+ }
72
+ }
73
+ };
74
+ const requestInfo = { method: "", url: "", startTime: 0, resultLog: "" };
75
+ const upstreamCapture = {};
76
+ const baseFetch = options.fetch ?? globalThis.fetch;
77
+ const capturingFetch = async (input, init) => {
78
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
79
+ const method = (init?.method ?? input?.method ?? "GET").toUpperCase();
80
+ const reqHeaders = headersInitToObject(init?.headers);
81
+ let reqBody = void 0;
82
+ if (init?.body != null) {
83
+ if (typeof init.body === "string") {
84
+ reqBody = tryParseJson(init.body);
85
+ } else if (init.body instanceof ArrayBuffer) {
86
+ reqBody = tryParseJson(new TextDecoder().decode(init.body));
87
+ } else if (ArrayBuffer.isView(init.body)) {
88
+ reqBody = tryParseJson(new TextDecoder().decode(init.body));
89
+ } else {
90
+ reqBody = String(init.body);
91
+ }
92
+ }
93
+ upstreamCapture.request = { url, method, headers: reqHeaders, body: reqBody };
94
+ const resp = await baseFetch(input, init);
95
+ if (!resp.ok) {
96
+ const clone = resp.clone();
97
+ const text = await clone.text().catch(() => "");
98
+ upstreamCapture.response = {
99
+ status: resp.status,
100
+ statusText: resp.statusText,
101
+ headers: headersToObject(resp.headers),
102
+ body: tryParseJson(text)
103
+ };
104
+ } else {
105
+ upstreamCapture.response = void 0;
106
+ }
107
+ return resp;
108
+ };
109
+ const apiFetch = core.createResponsesFetch({
110
+ upstreamFormat: options.upstreamFormat,
111
+ baseUrl: options.baseUrl,
112
+ apiVersion: options.apiVersion,
113
+ model: options.model,
114
+ defaultHeaders: options.defaultHeaders,
115
+ timeoutMs: options.timeoutMs,
116
+ dropImages: options.dropImages,
117
+ fallbackUpstream: options.fallbackUpstream,
118
+ fetch: capturingFetch,
119
+ passthroughFetch: async () => new Response(JSON.stringify({ error: { message: "Not found" } }), {
120
+ status: 404,
121
+ headers: { "content-type": "application/json" }
122
+ }),
123
+ onCacheStats: (stats) => {
124
+ const durationMs = requestInfo.startTime ? Date.now() - requestInfo.startTime : 0;
125
+ const billedTokens = stats.inputTokens + stats.outputTokens - stats.cachedTokens;
126
+ const parts = [
127
+ `total=${stats.totalTokens}`,
128
+ `input=${stats.inputTokens}`,
129
+ `output=${stats.outputTokens}`,
130
+ `cached=${stats.cachedTokens}`,
131
+ `billed=${billedTokens}`
132
+ ];
133
+ if (stats.cacheCreationTokens > 0) {
134
+ parts.push(`cache_creation=${stats.cacheCreationTokens}`);
135
+ }
136
+ const avg = updateRollingAverage(durationMs);
137
+ const ratio = avg > 0 ? durationMs / avg : 1;
138
+ const color = ratio < 0.8 ? "\x1B[32m" : ratio < 1.5 ? "\x1B[33m" : "\x1B[31m";
139
+ const reset = "\x1B[0m";
140
+ const logMsg = `[${fmtTime(/* @__PURE__ */ new Date())}] -> 200 (${color}${fmtDuration(durationMs)}${reset} avg=${fmtDuration(Math.round(avg))}) [${parts.join(", ")}]`;
141
+ requestInfo.resultLog = stats.cachedTokens < 1024 && billedTokens > 0 ? `\u26A0\uFE0F NO CACHE -- ${logMsg}` : logMsg;
142
+ if (options.onCacheStats) {
143
+ options.onCacheStats({
144
+ ...stats,
145
+ method: requestInfo.method || void 0,
146
+ url: requestInfo.url || void 0,
147
+ durationMs: durationMs || void 0
148
+ });
149
+ }
150
+ }
151
+ });
152
+ const server = http__default.default.createServer(async (req, res) => {
153
+ const start = Date.now();
154
+ requestInfo.method = req.method ?? "POST";
155
+ requestInfo.url = req.url ?? "/v1/responses";
156
+ requestInfo.startTime = start;
157
+ const abortController = new AbortController();
158
+ const timeoutMs = options.timeoutMs;
159
+ let timeoutTimer;
160
+ if (timeoutMs && timeoutMs > 0) {
161
+ timeoutTimer = setTimeout(() => {
162
+ logger?.warn(`[timeout] request exceeded ${timeoutMs}ms, aborting`);
163
+ abortController.abort();
164
+ res.destroy();
165
+ req.destroy();
166
+ }, timeoutMs);
167
+ }
168
+ try {
169
+ await handleRequest(req, res, {
170
+ apiFetch,
171
+ cors,
172
+ logger,
173
+ method: req.method ?? "POST",
174
+ url: req.url ?? "/",
175
+ upstreamCapture,
176
+ requestInfo,
177
+ requestTracker,
178
+ signal: abortController.signal
179
+ });
180
+ } catch (err) {
181
+ logger?.error("[proxy-error]", err);
182
+ try {
183
+ if (!res.headersSent) {
184
+ res.writeHead(500, { "content-type": "application/json" });
185
+ res.end(JSON.stringify({ error: { message: "Internal server error" } }));
186
+ }
187
+ } catch {
188
+ }
189
+ } finally {
190
+ if (timeoutTimer) {
191
+ clearTimeout(timeoutTimer);
192
+ }
193
+ }
194
+ });
195
+ return new Promise((resolve3, reject) => {
196
+ server.listen(port, host, () => {
197
+ const actualPort = (() => {
198
+ const addr = server.address();
199
+ return addr.port;
200
+ })();
201
+ const url = `http://${host}:${actualPort}`;
202
+ logger?.log(`Proxy listening on ${url}`);
203
+ logger?.log(`Upstream format: ${options.upstreamFormat}`);
204
+ logger?.log(`Upstream URL: ${options.baseUrl}`);
205
+ resolve3({
206
+ host,
207
+ port: actualPort,
208
+ url,
209
+ server,
210
+ close: () => new Promise((res) => {
211
+ server.close((err) => {
212
+ if (err) {
213
+ logger?.warn("Error closing server:", err);
214
+ }
215
+ res();
216
+ });
217
+ })
218
+ });
219
+ });
220
+ server.once("error", reject);
221
+ });
222
+ }
223
+ async function handleRequest(req, res, opts) {
224
+ if (opts.cors) {
225
+ setCorsHeaders(res);
226
+ }
227
+ if (req.method === "OPTIONS") {
228
+ res.writeHead(204);
229
+ res.end();
230
+ return;
231
+ }
232
+ const method = req.method ?? "GET";
233
+ const urlPath = req.url ?? "/";
234
+ const headers = flattenIncomingHeaders(req.headers);
235
+ let body;
236
+ if (method !== "GET" && method !== "HEAD") {
237
+ body = await readIncomingBody(req);
238
+ }
239
+ if (!/^\/v1\/responses\/?(?:\?|$)/.test(urlPath)) {
240
+ res.writeHead(404, { "content-type": "application/json" });
241
+ res.end(JSON.stringify({ error: { message: `Not found: ${method} ${urlPath}` } }));
242
+ return;
243
+ }
244
+ const requestBodyText = body ? body.toString("utf8") : void 0;
245
+ const requestStart = Date.now();
246
+ const requestId = opts.requestTracker.add(method, urlPath);
247
+ try {
248
+ const response = await opts.apiFetch(`http://local${urlPath}`, {
249
+ method,
250
+ headers,
251
+ body: body ? new Uint8Array(body) : void 0,
252
+ signal: opts.signal
253
+ });
254
+ const responseBodyText = response.body ? await response.clone().text() : "";
255
+ opts.requestTracker.remove(requestId);
256
+ if (opts.logger) {
257
+ if (response.status >= 400) {
258
+ process.stdout.write(
259
+ `\r\x1B[K<-- ${response.status} (${fmtDuration(Date.now() - requestStart)})
260
+ `
261
+ );
262
+ } else if (opts.requestInfo.resultLog) {
263
+ process.stdout.write(`\r\x1B[K${opts.requestInfo.resultLog}
264
+ `);
265
+ } else {
266
+ process.stdout.write(
267
+ `\r\x1B[K<-- ${response.status} (${fmtDuration(Date.now() - requestStart)})
268
+ `
269
+ );
270
+ }
271
+ }
272
+ if (response.status >= 400) {
273
+ try {
274
+ const filePath = saveErrorDump({
275
+ method: opts.method,
276
+ url: opts.url,
277
+ clientRequest: {
278
+ headers,
279
+ body: tryParseJson(requestBodyText ?? "")
280
+ },
281
+ upstreamRequest: opts.upstreamCapture.request,
282
+ upstreamResponse: opts.upstreamCapture.response,
283
+ proxyResponse: {
284
+ status: response.status,
285
+ headers: headersToObject(response.headers),
286
+ body: tryParseJson(responseBodyText)
287
+ }
288
+ });
289
+ opts.logger?.error(`[proxy-failure] full exchange saved to ${filePath}`);
290
+ } catch (dumpErr) {
291
+ opts.logger?.error("[proxy-failure] failed to persist error dump", dumpErr);
292
+ }
293
+ }
294
+ const outHeaders = {};
295
+ response.headers.forEach((value, key) => {
296
+ outHeaders[key] = value;
297
+ });
298
+ if (opts.cors) {
299
+ Object.assign(outHeaders, corsHeaders());
300
+ }
301
+ res.writeHead(response.status, outHeaders);
302
+ if (!response.body) {
303
+ res.end();
304
+ return;
305
+ }
306
+ const typedBody = response.body;
307
+ const nodeStream = stream.Readable.fromWeb(typedBody);
308
+ nodeStream.pipe(res);
309
+ await new Promise((resolve3, reject) => {
310
+ nodeStream.once("end", resolve3);
311
+ nodeStream.once("error", reject);
312
+ res.once("close", resolve3);
313
+ });
314
+ } catch (err) {
315
+ opts.requestTracker.remove(requestId);
316
+ throw err;
317
+ }
318
+ }
319
+ function readIncomingBody(req) {
320
+ return new Promise((resolve3, reject) => {
321
+ const chunks = [];
322
+ req.on("data", (chunk) => {
323
+ const buf = chunk;
324
+ chunks.push(buf);
325
+ });
326
+ req.on("end", () => resolve3(Buffer.concat(chunks)));
327
+ req.on("error", reject);
328
+ });
329
+ }
330
+ function flattenIncomingHeaders(headers) {
331
+ const out = {};
332
+ for (const [key, value] of Object.entries(headers)) {
333
+ if (value == null) {
334
+ continue;
335
+ }
336
+ out[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : String(value);
337
+ }
338
+ return out;
339
+ }
340
+ function headersToObject(headers) {
341
+ const out = {};
342
+ headers.forEach((value, key) => {
343
+ out[key] = value;
344
+ });
345
+ return out;
346
+ }
347
+ function setCorsHeaders(res) {
348
+ const headers = corsHeaders();
349
+ for (const [key, value] of Object.entries(headers)) {
350
+ res.setHeader(key, value);
351
+ }
352
+ }
353
+ function corsHeaders() {
354
+ return {
355
+ "access-control-allow-origin": "*",
356
+ "access-control-allow-methods": "GET,POST,OPTIONS",
357
+ "access-control-allow-headers": "authorization,content-type,x-api-key,anthropic-version,anthropic-beta,anthropic-dangerous-direct-browser-access",
358
+ "access-control-expose-headers": "content-type"
359
+ };
360
+ }
361
+ function tryParseJson(str) {
362
+ if (!str) {
363
+ return str ?? null;
364
+ }
365
+ try {
366
+ return JSON.parse(str);
367
+ } catch {
368
+ return str;
369
+ }
370
+ }
371
+ function headersInitToObject(headersInit) {
372
+ const out = {};
373
+ if (!headersInit) {
374
+ return out;
375
+ }
376
+ if (typeof Headers !== "undefined" && headersInit instanceof Headers) {
377
+ headersInit.forEach((value, key) => {
378
+ out[key.toLowerCase()] = value;
379
+ });
380
+ return out;
381
+ }
382
+ if (Array.isArray(headersInit)) {
383
+ for (const [key, value] of headersInit) {
384
+ out[String(key).toLowerCase()] = String(value);
385
+ }
386
+ return out;
387
+ }
388
+ for (const [key, value] of Object.entries(headersInit)) {
389
+ out[key.toLowerCase()] = String(value);
390
+ }
391
+ return out;
392
+ }
393
+ function saveErrorDump(dump) {
394
+ const dir = path.resolve(process.cwd(), "logs");
395
+ fs.mkdirSync(dir, { recursive: true });
396
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
397
+ const status = dump.upstreamResponse?.status ?? dump.proxyResponse.status;
398
+ const filename = `proxy-error-${ts}-${status}.json`;
399
+ const filePath = path.join(dir, filename);
400
+ const payload = {
401
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
402
+ ...dump
403
+ };
404
+ redactAuth(payload.clientRequest?.headers);
405
+ redactAuth(payload.upstreamRequest?.headers);
406
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
407
+ return filePath;
408
+ }
409
+ function redactAuth(headers) {
410
+ if (!headers) {
411
+ return;
412
+ }
413
+ for (const key of Object.keys(headers)) {
414
+ const lowerKey = key.toLowerCase();
415
+ if (lowerKey === "authorization" || lowerKey === "x-api-key" || lowerKey === "api-key" || lowerKey === "cookie") {
416
+ headers[key] = "[REDACTED]";
417
+ }
418
+ }
419
+ }
420
+ function validateConfig(config) {
421
+ if (typeof config !== "object" || config === null) {
422
+ return { valid: false, error: "Config must be an object" };
423
+ }
424
+ const cfg = config;
425
+ if (typeof cfg.version !== "string") {
426
+ return { valid: false, error: "Config must have a version string" };
427
+ }
428
+ if (typeof cfg.currentUpstream !== "string") {
429
+ return { valid: false, error: "Config must have a currentUpstream string" };
430
+ }
431
+ if (typeof cfg.upstreams !== "object" || cfg.upstreams === null) {
432
+ return { valid: false, error: "Config must have an upstreams object" };
433
+ }
434
+ const upstreams = cfg.upstreams;
435
+ if (!(cfg.currentUpstream in upstreams)) {
436
+ return {
437
+ valid: false,
438
+ error: `currentUpstream "${cfg.currentUpstream}" not found in upstreams`
439
+ };
440
+ }
441
+ for (const [name, upstream] of Object.entries(upstreams)) {
442
+ const result = validateUpstreamConfig(upstream);
443
+ if (!result.valid) {
444
+ return {
445
+ valid: false,
446
+ error: `Upstream "${name}" is invalid: ${result.error}`
447
+ };
448
+ }
449
+ }
450
+ return { valid: true };
451
+ }
452
+ function validateUpstreamConfig(upstream) {
453
+ if (typeof upstream !== "object" || upstream === null) {
454
+ return { valid: false, error: "Upstream config must be an object" };
455
+ }
456
+ const cfg = upstream;
457
+ if (cfg.format !== void 0) {
458
+ if (typeof cfg.format !== "string") {
459
+ return { valid: false, error: "format must be a string if provided" };
460
+ }
461
+ const validFormats = ["anthropic", "openai-chat"];
462
+ if (!validFormats.includes(cfg.format)) {
463
+ return {
464
+ valid: false,
465
+ error: `Invalid format: ${cfg.format}. Must be one of: ${validFormats.join(", ")}`
466
+ };
467
+ }
468
+ }
469
+ if (typeof cfg.baseUrl !== "string") {
470
+ return { valid: false, error: "baseUrl is required and must be a string" };
471
+ }
472
+ if (cfg.apiVersion !== void 0 && typeof cfg.apiVersion !== "string") {
473
+ return { valid: false, error: "apiVersion must be a string if provided" };
474
+ }
475
+ if (cfg.apiKey !== void 0 && typeof cfg.apiKey !== "string") {
476
+ return { valid: false, error: "apiKey must be a string if provided" };
477
+ }
478
+ if (cfg.model !== void 0 && typeof cfg.model !== "string") {
479
+ return { valid: false, error: "model must be a string if provided" };
480
+ }
481
+ if (cfg.dropImages !== void 0 && typeof cfg.dropImages !== "boolean") {
482
+ return { valid: false, error: "dropImages must be a boolean if provided" };
483
+ }
484
+ if (cfg.fallback !== void 0 && typeof cfg.fallback !== "string") {
485
+ return { valid: false, error: "fallback must be a string if provided" };
486
+ }
487
+ if (cfg.headers !== void 0 && (typeof cfg.headers !== "object" || cfg.headers === null)) {
488
+ return { valid: false, error: "headers must be an object if provided" };
489
+ }
490
+ if (cfg.reasoningEffort !== void 0 && typeof cfg.reasoningEffort !== "string") {
491
+ return { valid: false, error: "reasoningEffort must be a string if provided" };
492
+ }
493
+ return { valid: true };
494
+ }
495
+ function getCurrentUpstreamConfig(config) {
496
+ const upstream = config.upstreams[config.currentUpstream];
497
+ if (!upstream) {
498
+ return null;
499
+ }
500
+ return upstream;
501
+ }
502
+
503
+ // src/server/cli.ts
504
+ function parseArgs(argv) {
505
+ const out = {};
506
+ for (let i = 0; i < argv.length; i++) {
507
+ const arg = argv[i];
508
+ const take = () => argv[++i];
509
+ switch (arg) {
510
+ case "-h":
511
+ case "--help":
512
+ out.help = true;
513
+ break;
514
+ case "-p":
515
+ case "--port":
516
+ out.port = Number(take());
517
+ break;
518
+ case "--host":
519
+ out.host = take();
520
+ break;
521
+ case "--upstream-format":
522
+ out.upstreamFormat = take();
523
+ break;
524
+ case "--base-url":
525
+ out.baseUrl = take();
526
+ break;
527
+ case "--config":
528
+ out.config = take();
529
+ break;
530
+ case "--api-version":
531
+ out.apiVersion = take();
532
+ break;
533
+ case "--apikey":
534
+ out.apikey = take();
535
+ break;
536
+ case "--model":
537
+ out.model = take();
538
+ break;
539
+ case "--drop-images":
540
+ out.dropImages = true;
541
+ break;
542
+ case "--no-cors":
543
+ out.cors = false;
544
+ break;
545
+ default:
546
+ if (arg.startsWith("--upstream-format=")) {
547
+ out.upstreamFormat = arg.slice("--upstream-format=".length);
548
+ } else if (arg.startsWith("--port=")) {
549
+ out.port = Number(arg.slice("--port=".length));
550
+ } else if (arg.startsWith("--host=")) {
551
+ out.host = arg.slice("--host=".length);
552
+ } else if (arg.startsWith("--base-url=")) {
553
+ out.baseUrl = arg.slice("--base-url=".length);
554
+ } else if (arg.startsWith("--api-version=")) {
555
+ out.apiVersion = arg.slice("--api-version=".length);
556
+ } else if (arg.startsWith("--apikey=")) {
557
+ out.apikey = arg.slice("--apikey=".length);
558
+ } else if (arg.startsWith("--model=")) {
559
+ out.model = arg.slice("--model=".length);
560
+ } else if (arg.startsWith("--config=")) {
561
+ out.config = arg.slice("--config=".length);
562
+ } else {
563
+ console.error(`Unknown argument: ${arg}`);
564
+ out.help = true;
565
+ }
566
+ }
567
+ }
568
+ return out;
569
+ }
570
+ function printHelp() {
571
+ console.log(`codeproxy - local Responses API proxy
572
+
573
+ Usage:
574
+ codeproxy --base-url <url> [options]
575
+ codeproxy --config <file> [options]
576
+
577
+ Options:
578
+ --base-url <url> Upstream endpoint URL (required when not using --config)
579
+ --upstream-format <fmt> Upstream API format: anthropic | openai-chat
580
+ (optional; inferred from --base-url when omitted:
581
+ */messages or *anthropic* \u2192 anthropic,
582
+ */chat/completions \u2192 openai-chat)
583
+ --config <file> Use a config file instead of CLI flags
584
+ --host <host> Bind host (default: 127.0.0.1)
585
+ -p, --port <port> Bind port (default: 8787; 0 = random)
586
+ --api-version <ver> Override anthropic-version header (anthropic only)
587
+ --apikey <key> Override upstream Authorization: Bearer <key>
588
+ --model <name> Override the model field in incoming requests
589
+ --no-cors Disable CORS headers
590
+ -h, --help Show help
591
+
592
+ Config File Mode:
593
+ When using --config, upstream settings are loaded from the config file.
594
+ Command-line options can override config values.
595
+
596
+ Config file format (JSON):
597
+ {
598
+ "version": "1.0",
599
+ "currentUpstream": "my-claude",
600
+ "upstreams": {
601
+ "my-claude": {
602
+ "baseUrl": "https://api.anthropic.com/v1/messages",
603
+ "apiKey": "your-api-key",
604
+ "model": "claude-sonnet-4-5"
605
+ },
606
+ "my-openai": {
607
+ "baseUrl": "https://api.openai.com/v1/chat/completions",
608
+ "apiKey": "your-openai-key"
609
+ }
610
+ }
611
+ }
612
+
613
+ "format" is optional; inferred from baseUrl when omitted.
614
+
615
+ Auth is caller-driven: send Authorization: Bearer <key> (or the upstream's
616
+ native header) when calling the proxy. Nothing is stored server-side.
617
+
618
+ Examples:
619
+ codeproxy --upstream-format anthropic --base-url https://api.anthropic.com/v1/messages
620
+ codeproxy --upstream-format openai-chat --base-url https://api.openai.com/v1/chat/completions
621
+ codeproxy --config my-config.json
622
+ codeproxy --upstream-format anthropic --base-url https://api.anthropic.com/v1/messages --apikey <key>
623
+ `);
624
+ }
625
+ async function loadConfigFile(configPath) {
626
+ if (!fs.existsSync(configPath)) {
627
+ console.error(`Config file not found: ${configPath}`);
628
+ process.exit(1);
629
+ }
630
+ try {
631
+ const content = fs.readFileSync(configPath, "utf-8");
632
+ const parsed = JSON.parse(content);
633
+ return parsed;
634
+ } catch (error) {
635
+ console.error(`Failed to load config from ${configPath}:`, error);
636
+ process.exit(1);
637
+ }
638
+ }
639
+ async function loadConfigAndApplyOverrides(configPath, overrides) {
640
+ const config = await loadConfigFile(configPath);
641
+ const validation = validateConfig(config);
642
+ if (!validation.valid) {
643
+ console.error(`Invalid config file: ${validation.error}`);
644
+ process.exit(1);
645
+ }
646
+ const upstreamConfig = getCurrentUpstreamConfig(config);
647
+ if (!upstreamConfig) {
648
+ console.error(`Current upstream "${config.currentUpstream}" not found in config`);
649
+ process.exit(1);
650
+ }
651
+ console.log(`Loaded config from: ${configPath}`);
652
+ console.log(
653
+ `Current upstream: ${config.currentUpstream}${upstreamConfig.format ? ` (${upstreamConfig.format})` : ""}`
654
+ );
655
+ console.log(`Model: ${upstreamConfig.model || "(not set)"}`);
656
+ const mergedEffort = upstreamConfig.reasoningEffort ?? config.reasoningEffort;
657
+ if (mergedEffort) {
658
+ console.log(`Reasoning effort: ${mergedEffort}`);
659
+ }
660
+ const mergedHeaders = { ...config.headers ?? {} };
661
+ if (upstreamConfig.headers) {
662
+ Object.assign(mergedHeaders, upstreamConfig.headers);
663
+ }
664
+ if (mergedHeaders.authorization) {
665
+ mergedHeaders.authorization = '"[REDACTED]"';
666
+ }
667
+ if (Object.keys(mergedHeaders).length > 0) {
668
+ console.log(`Headers: ${JSON.stringify(mergedHeaders)}`);
669
+ }
670
+ const options = {
671
+ upstreamFormat: upstreamConfig.format,
672
+ baseUrl: overrides.baseUrl || upstreamConfig.baseUrl,
673
+ apiVersion: overrides.apiVersion || upstreamConfig.apiVersion,
674
+ model: overrides.model || upstreamConfig.model,
675
+ host: overrides.host || upstreamConfig.host,
676
+ port: overrides.port !== void 0 ? overrides.port : (
677
+ // eslint-disable-next-line no-restricted-syntax -- access dynamic config key
678
+ config.port ? (
679
+ // eslint-disable-next-line no-restricted-syntax -- access dynamic config key
680
+ Number(config.port)
681
+ ) : upstreamConfig.port ? Number(upstreamConfig.port) : void 0
682
+ ),
683
+ timeoutMs: upstreamConfig.timeoutMs ?? config.timeoutMs,
684
+ dropImages: upstreamConfig.dropImages,
685
+ cors: overrides.cors,
686
+ reasoning_effort: upstreamConfig.reasoningEffort ?? config.reasoningEffort,
687
+ thinking: upstreamConfig.thinking ?? config.thinking
688
+ };
689
+ const defaultHeaders = { ...config.headers ?? {} };
690
+ if (upstreamConfig.headers) {
691
+ Object.assign(defaultHeaders, upstreamConfig.headers);
692
+ }
693
+ if (upstreamConfig.apiKey) {
694
+ defaultHeaders.authorization = `Bearer ${upstreamConfig.apiKey}`;
695
+ }
696
+ if (overrides.apikey) {
697
+ defaultHeaders.authorization = `Bearer ${overrides.apikey}`;
698
+ }
699
+ if (Object.keys(defaultHeaders).length > 0) {
700
+ options.defaultHeaders = defaultHeaders;
701
+ }
702
+ if (upstreamConfig.fallback) {
703
+ const fbConfig = config.upstreams[upstreamConfig.fallback];
704
+ if (fbConfig) {
705
+ const fbHeaders = { ...config.headers ?? {} };
706
+ if (fbConfig.headers) {
707
+ Object.assign(fbHeaders, fbConfig.headers);
708
+ }
709
+ if (fbConfig.apiKey) {
710
+ fbHeaders.authorization = `Bearer ${fbConfig.apiKey}`;
711
+ }
712
+ const fbFormat = fbConfig.format;
713
+ options.fallbackUpstream = {
714
+ baseUrl: fbConfig.baseUrl,
715
+ upstreamFormat: fbFormat,
716
+ model: fbConfig.model ?? options.model,
717
+ defaultHeaders: Object.keys(fbHeaders).length > 0 ? fbHeaders : void 0,
718
+ apiVersion: fbConfig.apiVersion ?? options.apiVersion,
719
+ reasoning_effort: fbConfig.reasoningEffort ?? options.reasoning_effort,
720
+ thinking: fbConfig.thinking ?? options.thinking
721
+ };
722
+ console.log(
723
+ `Fallback upstream: ${upstreamConfig.fallback}${fbConfig.model ? ` (${fbConfig.model})` : ""}`
724
+ );
725
+ } else {
726
+ console.warn(`Warning: fallback upstream "${upstreamConfig.fallback}" not found in config`);
727
+ }
728
+ }
729
+ return options;
730
+ }
731
+ async function main() {
732
+ const args = parseArgs(process.argv.slice(2));
733
+ if (args.help) {
734
+ printHelp();
735
+ process.exit(0);
736
+ }
737
+ let options;
738
+ if (args.config) {
739
+ options = await loadConfigAndApplyOverrides(args.config, args);
740
+ } else if (args.baseUrl) {
741
+ const upstreamFormat = args.upstreamFormat;
742
+ options = {
743
+ upstreamFormat,
744
+ baseUrl: args.baseUrl,
745
+ host: args.host,
746
+ port: args.port,
747
+ apiVersion: args.apiVersion,
748
+ model: args.model,
749
+ defaultHeaders: args.apikey ? { authorization: `Bearer ${args.apikey}` } : void 0,
750
+ dropImages: args.dropImages,
751
+ cors: args.cors
752
+ };
753
+ } else {
754
+ console.error("Error: Either --config <file> or --base-url <url> is required");
755
+ console.error("");
756
+ printHelp();
757
+ process.exit(1);
758
+ }
759
+ const proxy = await startProxy(options);
760
+ const shutdown = async (signal) => {
761
+ console.log(`
762
+ Received ${signal}, shutting down...`);
763
+ try {
764
+ await proxy.close();
765
+ } finally {
766
+ process.exit(0);
767
+ }
768
+ };
769
+ process.on("SIGINT", () => void shutdown("SIGINT"));
770
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
771
+ }
772
+ if (process.argv[1]?.endsWith("cli.js") || process.argv[1]?.endsWith("cli.ts")) {
773
+ void main();
774
+ }
775
+
776
+ exports.loadConfigAndApplyOverrides = loadConfigAndApplyOverrides;
777
+ exports.loadConfigFile = loadConfigFile;
778
+ exports.main = main;
779
+ exports.parseArgs = parseArgs;
780
+ exports.printHelp = printHelp;
781
+ //# sourceMappingURL=cli.cjs.map
782
+ //# sourceMappingURL=cli.cjs.map