proxitor 0.9.0-beta.4 → 0.9.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +141 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/prompt.mjs +3 -3
- package/dist/prompt.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { formatWithOptions, styleText } from "node:util";
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join, resolve, sep } from "node:path";
|
|
10
10
|
import * as l$1 from "node:readline";
|
|
11
|
-
import
|
|
11
|
+
import l__default from "node:readline";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { createServer } from "node:net";
|
|
14
14
|
import { STATUS_CODES, createServer as createServer$1 } from "node:http";
|
|
@@ -10809,15 +10809,10 @@ function applyOverride(result, override) {
|
|
|
10809
10809
|
...override.headers
|
|
10810
10810
|
};
|
|
10811
10811
|
if (override.cacheControl !== void 0) result.cacheControl = override.cacheControl;
|
|
10812
|
-
if (override.cacheControlTtl
|
|
10813
|
-
else if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
|
|
10812
|
+
if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl ?? void 0;
|
|
10814
10813
|
if (override.sessionId !== void 0) result.sessionId = override.sessionId;
|
|
10815
10814
|
}
|
|
10816
|
-
/**
|
|
10817
|
-
* Throw if a URL ends with /v1 — a common misconfiguration after the URL routing change.
|
|
10818
|
-
* Paths like /v1/chat/completions are now forwarded as-is, so including /v1 in the base
|
|
10819
|
-
* URL would produce doubled paths like /v1/v1/chat/completions.
|
|
10820
|
-
*/
|
|
10815
|
+
/** Reject base URLs ending in /v1 — paths are forwarded as-is, so /v1 suffix causes doubled paths. */
|
|
10821
10816
|
function throwIfV1Suffix(url, field) {
|
|
10822
10817
|
const { pathname } = new URL(url);
|
|
10823
10818
|
if (pathname.endsWith("/v1") || pathname.endsWith("/v1/")) throw new Error(`${field} "${url}" ends with /v1 — paths are now forwarded as-is, so this would produce doubled paths like /v1/v1/chat/completions. Remove the /v1 suffix (use "${url.replace(/\/v1\/?$/, "")}")`);
|
|
@@ -10834,9 +10829,8 @@ async function loadConfig(options) {
|
|
|
10834
10829
|
...options.host !== void 0 ? { host: options.host } : {},
|
|
10835
10830
|
...options.port !== void 0 ? { port: options.port } : {},
|
|
10836
10831
|
...options.verbose !== void 0 ? { verbose: options.verbose } : {},
|
|
10837
|
-
|
|
10832
|
+
openrouterKey: options.openrouterKey || fileConfig.openrouterKey || process.env.OPENROUTER_API_KEY || ""
|
|
10838
10833
|
};
|
|
10839
|
-
if (!merged.openrouterKey) merged.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
|
|
10840
10834
|
const result = proxyConfigSchema.safeParse(merged);
|
|
10841
10835
|
if (!result.success) throw new ConfigValidationError("(merged config)", result.error);
|
|
10842
10836
|
if (!result.data.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
|
|
@@ -10864,30 +10858,16 @@ const XDG_CONFIG_CANDIDATES = [
|
|
|
10864
10858
|
function getConfigSearchPaths() {
|
|
10865
10859
|
return [...LOCAL_CONFIG_CANDIDATES.map((c) => resolve(c)), ...XDG_CONFIG_CANDIDATES.map((c) => join(getXdgConfigDir(), c))];
|
|
10866
10860
|
}
|
|
10867
|
-
/**
|
|
10868
|
-
* Return the path of the first existing config file, or `null` if none was found.
|
|
10869
|
-
* Use this when "no config" is a valid outcome (e.g. wizard, doctor, validate).
|
|
10870
|
-
*/
|
|
10861
|
+
/** Returns first existing config file, or null. Use when "no config" is valid (wizard, doctor, validate). */
|
|
10871
10862
|
function tryFindConfigFile(explicitPath) {
|
|
10872
10863
|
if (explicitPath) {
|
|
10873
10864
|
if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${explicitPath}`);
|
|
10874
10865
|
return resolve(explicitPath);
|
|
10875
10866
|
}
|
|
10876
|
-
for (const
|
|
10877
|
-
const fullPath = resolve(candidate);
|
|
10878
|
-
if (existsSync(fullPath)) return fullPath;
|
|
10879
|
-
}
|
|
10880
|
-
const xdgDir = getXdgConfigDir();
|
|
10881
|
-
for (const candidate of XDG_CONFIG_CANDIDATES) {
|
|
10882
|
-
const fullPath = join(xdgDir, candidate);
|
|
10883
|
-
if (existsSync(fullPath)) return fullPath;
|
|
10884
|
-
}
|
|
10867
|
+
for (const fullPath of getConfigSearchPaths()) if (existsSync(fullPath)) return fullPath;
|
|
10885
10868
|
return null;
|
|
10886
10869
|
}
|
|
10887
|
-
/**
|
|
10888
|
-
* Resolve a config file path, throwing {@link MissingConfigError} if discovery
|
|
10889
|
-
* (with no explicit path) fails. Use this when the config is required.
|
|
10890
|
-
*/
|
|
10870
|
+
/** Like tryFindConfigFile but throws MissingConfigError when no config is found. */
|
|
10891
10871
|
function findConfigFile(explicitPath) {
|
|
10892
10872
|
const found = tryFindConfigFile(explicitPath);
|
|
10893
10873
|
if (found) return found;
|
|
@@ -11463,7 +11443,7 @@ var V = class {
|
|
|
11463
11443
|
this.state = "cancel", this.close();
|
|
11464
11444
|
}, { once: true });
|
|
11465
11445
|
}
|
|
11466
|
-
this.rl =
|
|
11446
|
+
this.rl = l__default.createInterface({
|
|
11467
11447
|
input: this.input,
|
|
11468
11448
|
tabSize: 2,
|
|
11469
11449
|
prompt: "",
|
|
@@ -19607,7 +19587,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
|
|
|
19607
19587
|
const e = i(true);
|
|
19608
19588
|
return Object.keys(e);
|
|
19609
19589
|
}
|
|
19610
|
-
}), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f
|
|
19590
|
+
}), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f = [
|
|
19611
19591
|
["APPVEYOR"],
|
|
19612
19592
|
[
|
|
19613
19593
|
"AWS_AMPLIFY",
|
|
@@ -19699,7 +19679,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
|
|
|
19699
19679
|
]
|
|
19700
19680
|
];
|
|
19701
19681
|
function b() {
|
|
19702
|
-
if (globalThis.process?.env) for (const e of f
|
|
19682
|
+
if (globalThis.process?.env) for (const e of f) {
|
|
19703
19683
|
const s = e[1] || e[0];
|
|
19704
19684
|
if (globalThis.process?.env[s]) return {
|
|
19705
19685
|
name: e[0].toLowerCase(),
|
|
@@ -21025,25 +21005,21 @@ var OpenRouterClient = class {
|
|
|
21025
21005
|
};
|
|
21026
21006
|
//#endregion
|
|
21027
21007
|
//#region src/openrouter/data-client.ts
|
|
21028
|
-
const OPENROUTER_FALLBACK_URL = OPENROUTER_API_URL;
|
|
21029
21008
|
const MODELS_CACHE_TTL_MS = 300 * 1e3;
|
|
21030
21009
|
const modelsCache = /* @__PURE__ */ new Map();
|
|
21031
|
-
function
|
|
21010
|
+
function isValidArrayDataResponse(data) {
|
|
21032
21011
|
return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
|
|
21033
21012
|
}
|
|
21013
|
+
function isValidProvidersResponse(data) {
|
|
21014
|
+
return isValidArrayDataResponse(data);
|
|
21015
|
+
}
|
|
21034
21016
|
function isValidModelsResponse(data) {
|
|
21035
|
-
return
|
|
21017
|
+
return isValidArrayDataResponse(data);
|
|
21036
21018
|
}
|
|
21037
21019
|
function isValidEndpointsResponse(data) {
|
|
21038
21020
|
return typeof data === "object" && data !== null && "data" in data && typeof data.data === "object" && data.data !== null && "endpoints" in data.data && Array.isArray(data.data.endpoints);
|
|
21039
21021
|
}
|
|
21040
|
-
/**
|
|
21041
|
-
* Client for fetching provider/model data with automatic fallback to OpenRouter.
|
|
21042
|
-
*
|
|
21043
|
-
* When the primary API (openrouterDataUrl or openrouterBaseUrl) doesn't support
|
|
21044
|
-
* OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api
|
|
21045
|
-
* which hosts public, unauthenticated endpoints for /providers, /models, etc.
|
|
21046
|
-
*/
|
|
21022
|
+
/** Fetches provider/model data with fallback to OpenRouter. */
|
|
21047
21023
|
var OpenRouterDataClient = class {
|
|
21048
21024
|
primaryClient;
|
|
21049
21025
|
fallbackClient;
|
|
@@ -21052,9 +21028,9 @@ var OpenRouterDataClient = class {
|
|
|
21052
21028
|
cacheKey;
|
|
21053
21029
|
constructor(config) {
|
|
21054
21030
|
const primaryUrl = config.openrouterDataUrl ?? config.openrouterBaseUrl;
|
|
21055
|
-
this.skipFallback = primaryUrl ===
|
|
21031
|
+
this.skipFallback = primaryUrl === OPENROUTER_API_URL;
|
|
21056
21032
|
this.primaryClient = new OpenRouterClient(config.apiKey, primaryUrl, config.authType);
|
|
21057
|
-
this.fallbackClient = new OpenRouterClient(
|
|
21033
|
+
this.fallbackClient = new OpenRouterClient(OPENROUTER_API_URL);
|
|
21058
21034
|
this.onFallback = config.onFallback;
|
|
21059
21035
|
const keyHash = createHash("sha256").update(config.apiKey).digest("hex").slice(0, 8);
|
|
21060
21036
|
this.cacheKey = `${primaryUrl}|${config.authType}|${keyHash}`;
|
|
@@ -21065,7 +21041,6 @@ var OpenRouterDataClient = class {
|
|
|
21065
21041
|
async fetchModels() {
|
|
21066
21042
|
const cached = modelsCache.get(this.cacheKey);
|
|
21067
21043
|
if (cached && Date.now() - cached.at < MODELS_CACHE_TTL_MS) return cached.models;
|
|
21068
|
-
if (cached) modelsCache.delete(this.cacheKey);
|
|
21069
21044
|
const models = (await this.withFallback("/v1/models", () => this.primaryClient.get("/v1/models"), isValidModelsResponse)).data.data;
|
|
21070
21045
|
modelsCache.set(this.cacheKey, {
|
|
21071
21046
|
at: Date.now(),
|
|
@@ -21077,10 +21052,7 @@ var OpenRouterDataClient = class {
|
|
|
21077
21052
|
const path = `/v1/models/${author}/${slug}/endpoints`;
|
|
21078
21053
|
return (await this.withFallback(path, () => this.primaryClient.get(path), isValidEndpointsResponse)).data.data.endpoints ?? [];
|
|
21079
21054
|
}
|
|
21080
|
-
/**
|
|
21081
|
-
* Try primary, validate response, fallback on failure.
|
|
21082
|
-
* Network errors get 1 retry before fallback.
|
|
21083
|
-
*/
|
|
21055
|
+
/** Primary with retry, then fallback. */
|
|
21084
21056
|
async withFallback(path, primaryFn, validate) {
|
|
21085
21057
|
if (this.skipFallback) {
|
|
21086
21058
|
const data = await primaryFn();
|
|
@@ -21118,24 +21090,19 @@ function isNetworkError(error) {
|
|
|
21118
21090
|
const message = error.message.toLowerCase();
|
|
21119
21091
|
return message.includes("fetch") || message.includes("network") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("timeout") || message.includes("aborted") || error.name === "TypeError";
|
|
21120
21092
|
}
|
|
21121
|
-
/**
|
|
21122
|
-
* Best-effort probe of the upstream API. Non-blocking — used by the wizard
|
|
21123
|
-
* to give early feedback on key validity without preventing save.
|
|
21124
|
-
*/
|
|
21093
|
+
/** Probes upstream to validate key and count models. */
|
|
21125
21094
|
async function probeUpstream(baseUrl, apiKey, authType, timeoutMs = 3e3) {
|
|
21095
|
+
if (!apiKey) return {
|
|
21096
|
+
ok: false,
|
|
21097
|
+
reason: "No API key provided"
|
|
21098
|
+
};
|
|
21126
21099
|
const url = `${baseUrl.replace(/\/$/, "")}/v1/models`;
|
|
21127
21100
|
const controller = new AbortController();
|
|
21128
21101
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21129
21102
|
try {
|
|
21130
|
-
if (!apiKey) return {
|
|
21131
|
-
ok: false,
|
|
21132
|
-
reason: "No API key provided"
|
|
21133
|
-
};
|
|
21134
|
-
const headers = {};
|
|
21135
|
-
headers.Authorization = formatAuthHeader(apiKey, authType);
|
|
21136
21103
|
const res = await fetch(url, {
|
|
21137
21104
|
signal: controller.signal,
|
|
21138
|
-
headers
|
|
21105
|
+
headers: { Authorization: formatAuthHeader(apiKey, authType) }
|
|
21139
21106
|
});
|
|
21140
21107
|
if (res.status === 401 || res.status === 403) return {
|
|
21141
21108
|
ok: false,
|
|
@@ -21544,7 +21511,7 @@ async function runConfigMenu(client) {
|
|
|
21544
21511
|
}
|
|
21545
21512
|
//#endregion
|
|
21546
21513
|
//#region src/version.ts
|
|
21547
|
-
const version = "0.9.0-beta.
|
|
21514
|
+
const version = "0.9.0-beta.5";
|
|
21548
21515
|
//#endregion
|
|
21549
21516
|
//#region src/commands/doctor.ts
|
|
21550
21517
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
@@ -24515,6 +24482,19 @@ function filterHeaders(incoming, blocklist) {
|
|
|
24515
24482
|
}
|
|
24516
24483
|
return headers;
|
|
24517
24484
|
}
|
|
24485
|
+
/**
|
|
24486
|
+
* Canonicalize a header record to lowercase keys.
|
|
24487
|
+
*
|
|
24488
|
+
* HTTP header names are case-insensitive (RFC 9110 §5.1), but a plain object
|
|
24489
|
+
* treats `Content-Type` and `content-type` as distinct keys. Lowercasing folds
|
|
24490
|
+
* case-variant keys into one so the merged record can never carry two headers
|
|
24491
|
+
* that differ only by case. Returns a new object; does not mutate the input.
|
|
24492
|
+
*/
|
|
24493
|
+
function lowercaseKeys(record) {
|
|
24494
|
+
const result = {};
|
|
24495
|
+
for (const [key, value] of Object.entries(record)) result[key.toLowerCase()] = value;
|
|
24496
|
+
return result;
|
|
24497
|
+
}
|
|
24518
24498
|
/** Filter response headers and add SSE-friendly defaults */
|
|
24519
24499
|
function buildResponseHeaders(from) {
|
|
24520
24500
|
const headers = filterHeaders(from, STRIP_RESPONSE);
|
|
@@ -24524,48 +24504,65 @@ function buildResponseHeaders(from) {
|
|
|
24524
24504
|
}
|
|
24525
24505
|
//#endregion
|
|
24526
24506
|
//#region src/proxy/middleware/build-upstream-req.ts
|
|
24507
|
+
const encoder = new TextEncoder();
|
|
24527
24508
|
const PROTO_POLLUTION_KEYS = new Set([
|
|
24528
24509
|
"__proto__",
|
|
24529
24510
|
"constructor",
|
|
24530
24511
|
"prototype"
|
|
24531
24512
|
]);
|
|
24532
|
-
function resolveForwardBody(
|
|
24533
|
-
if (
|
|
24534
|
-
return
|
|
24513
|
+
function resolveForwardBody(opts) {
|
|
24514
|
+
if (opts.bodyMutated && opts.parsedBody) try {
|
|
24515
|
+
return encoder.encode(JSON.stringify(opts.parsedBody)).buffer;
|
|
24535
24516
|
} catch (err) {
|
|
24536
|
-
logger.warn(withReq(
|
|
24537
|
-
return
|
|
24517
|
+
logger.warn(withReq(opts.reqId, `Failed to re-serialize mutated body (${err instanceof Error ? err.message : "unknown"}); forwarding raw body as-is`));
|
|
24518
|
+
return opts.rawBody;
|
|
24538
24519
|
}
|
|
24539
|
-
return
|
|
24520
|
+
return opts.rawBody;
|
|
24540
24521
|
}
|
|
24541
|
-
function
|
|
24542
|
-
|
|
24543
|
-
|
|
24544
|
-
|
|
24545
|
-
|
|
24522
|
+
function proxyHeaders(config) {
|
|
24523
|
+
return {
|
|
24524
|
+
Authorization: formatAuthHeader(config.openrouterKey, config.authType),
|
|
24525
|
+
"HTTP-Referer": config.attributionReferer,
|
|
24526
|
+
"X-OpenRouter-Title": config.attributionTitle,
|
|
24527
|
+
"Accept-Encoding": "identity"
|
|
24528
|
+
};
|
|
24546
24529
|
}
|
|
24547
|
-
function
|
|
24548
|
-
|
|
24530
|
+
function sanitizeExtraHeaders(extraHeaders) {
|
|
24531
|
+
const safe = {};
|
|
24532
|
+
if (!extraHeaders) return safe;
|
|
24549
24533
|
for (const [key, value] of Object.entries(extraHeaders)) {
|
|
24550
24534
|
if (PROTO_POLLUTION_KEYS.has(key)) continue;
|
|
24551
|
-
|
|
24535
|
+
safe[key] = value;
|
|
24552
24536
|
}
|
|
24537
|
+
return safe;
|
|
24538
|
+
}
|
|
24539
|
+
function withSessionId(headers, sessionId) {
|
|
24540
|
+
const { "x-claude-code-session-id": _omit, ...rest } = headers;
|
|
24541
|
+
return {
|
|
24542
|
+
...rest,
|
|
24543
|
+
"x-session-id": sessionId
|
|
24544
|
+
};
|
|
24553
24545
|
}
|
|
24554
|
-
function
|
|
24555
|
-
|
|
24556
|
-
|
|
24546
|
+
function withJsonContentType(headers) {
|
|
24547
|
+
return {
|
|
24548
|
+
...headers,
|
|
24549
|
+
"content-type": "application/json"
|
|
24550
|
+
};
|
|
24557
24551
|
}
|
|
24558
24552
|
const buildUpstreamReq = createMiddleware(async (c, next) => {
|
|
24559
|
-
c.set("forwardBody", resolveForwardBody(
|
|
24560
|
-
|
|
24561
|
-
|
|
24562
|
-
|
|
24563
|
-
|
|
24564
|
-
|
|
24565
|
-
|
|
24566
|
-
|
|
24567
|
-
|
|
24568
|
-
|
|
24553
|
+
c.set("forwardBody", resolveForwardBody({
|
|
24554
|
+
reqId: c.var.reqId,
|
|
24555
|
+
bodyMutated: c.var.bodyMutated,
|
|
24556
|
+
parsedBody: c.var.parsedBody,
|
|
24557
|
+
rawBody: c.var.rawBody
|
|
24558
|
+
}));
|
|
24559
|
+
let headers = lowercaseKeys({
|
|
24560
|
+
...filterHeaders(c.req.raw.headers, STRIP_REQUEST),
|
|
24561
|
+
...proxyHeaders(c.var.config),
|
|
24562
|
+
...sanitizeExtraHeaders(c.var.resolvedConfig.headers)
|
|
24563
|
+
});
|
|
24564
|
+
if (c.var.effectiveSessionId !== void 0) headers = withSessionId(headers, c.var.effectiveSessionId);
|
|
24565
|
+
if (c.var.bodyMutated) headers = withJsonContentType(headers);
|
|
24569
24566
|
c.set("upstreamHeaders", headers);
|
|
24570
24567
|
await next();
|
|
24571
24568
|
});
|
|
@@ -24687,10 +24684,7 @@ function buildUpstreamResponseWithLogging(upstream, method, reqId) {
|
|
|
24687
24684
|
}
|
|
24688
24685
|
//#endregion
|
|
24689
24686
|
//#region src/proxy/utils/error.ts
|
|
24690
|
-
/**
|
|
24691
|
-
* OpenRouter error format:
|
|
24692
|
-
* { error: { code, message, metadata: { raw, provider_name } } }
|
|
24693
|
-
*/
|
|
24687
|
+
/** OpenRouter error: { error: { code, message, metadata: { raw, provider_name } } } */
|
|
24694
24688
|
function formatMetadata(meta) {
|
|
24695
24689
|
const parts = [];
|
|
24696
24690
|
if (meta.provider_name) parts.push(`provider=${meta.provider_name}`);
|
|
@@ -24718,6 +24712,7 @@ function extractErrorDetail(bodyText) {
|
|
|
24718
24712
|
}
|
|
24719
24713
|
//#endregion
|
|
24720
24714
|
//#region src/proxy/middleware/forward-request.ts
|
|
24715
|
+
const DUPLEX_HALF = { duplex: "half" };
|
|
24721
24716
|
function buildErrorResponse(err, ctx) {
|
|
24722
24717
|
if (err instanceof TypeError) {
|
|
24723
24718
|
logger.error(withReq(ctx.reqId, "Upstream fetch error:"), err);
|
|
@@ -24754,14 +24749,14 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
24754
24749
|
method,
|
|
24755
24750
|
path,
|
|
24756
24751
|
startedAt,
|
|
24757
|
-
upstreamShort: upstreamUrl.replace(/^https?:\/\//, ""),
|
|
24758
|
-
modelLog: c.var.modelName ? ` model=${c.var.modelName}` : "",
|
|
24759
24752
|
bodyMutated: c.var.bodyMutated
|
|
24760
24753
|
};
|
|
24761
24754
|
const controller = new AbortController();
|
|
24762
24755
|
const onClientAbort = () => controller.abort();
|
|
24763
24756
|
c.req.raw.signal.addEventListener("abort", onClientAbort);
|
|
24764
|
-
|
|
24757
|
+
const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
|
|
24758
|
+
const modelLog = c.var.modelName ? ` model=${c.var.modelName}` : "";
|
|
24759
|
+
logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${modelLog}`));
|
|
24765
24760
|
let upstream;
|
|
24766
24761
|
try {
|
|
24767
24762
|
upstream = await fetch(upstreamUrl, {
|
|
@@ -24769,7 +24764,7 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
24769
24764
|
headers: upstreamHeaders,
|
|
24770
24765
|
body: forwardBody,
|
|
24771
24766
|
signal: controller.signal,
|
|
24772
|
-
...forwardBody ?
|
|
24767
|
+
...forwardBody ? DUPLEX_HALF : {}
|
|
24773
24768
|
});
|
|
24774
24769
|
} catch (err) {
|
|
24775
24770
|
return buildErrorResponse(err, ctx);
|
|
@@ -24813,16 +24808,11 @@ function shouldInjectCacheControl(mode, modelName, path) {
|
|
|
24813
24808
|
if (mode === "always") return true;
|
|
24814
24809
|
return isAnthropicEndpoint(modelName, path);
|
|
24815
24810
|
}
|
|
24816
|
-
/**
|
|
24817
|
-
* Build cache_control value for injection.
|
|
24818
|
-
* Merges existing cache_control with configured TTL.
|
|
24819
|
-
* If TTL is configured and the endpoint is Anthropic, it always overrides.
|
|
24820
|
-
*/
|
|
24821
24811
|
function buildCacheControl(existing, ttl, isAnthropic) {
|
|
24822
|
-
const
|
|
24823
|
-
if (!("type" in
|
|
24824
|
-
if (ttl && isAnthropic)
|
|
24825
|
-
return
|
|
24812
|
+
const base = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
24813
|
+
if (!("type" in base)) base.type = "ephemeral";
|
|
24814
|
+
if (ttl && isAnthropic) base.ttl = ttl;
|
|
24815
|
+
return base;
|
|
24826
24816
|
}
|
|
24827
24817
|
//#endregion
|
|
24828
24818
|
//#region src/proxy/middleware/inject-cache-control.ts
|
|
@@ -24860,6 +24850,18 @@ const injectProvider = createMiddleware(async (c, next) => {
|
|
|
24860
24850
|
//#endregion
|
|
24861
24851
|
//#region src/proxy/utils/session-id.ts
|
|
24862
24852
|
const PROXY_SESSION_ID = crypto.randomUUID();
|
|
24853
|
+
function firstContent(messages, ...roles) {
|
|
24854
|
+
if (!Array.isArray(messages)) return void 0;
|
|
24855
|
+
return messages.find((m) => roles.includes(m.role ?? "") && m.content != null)?.content;
|
|
24856
|
+
}
|
|
24857
|
+
/** Returns '' for non-serializable values (e.g. BigInt). */
|
|
24858
|
+
function safeStringify(value) {
|
|
24859
|
+
try {
|
|
24860
|
+
return JSON.stringify(value);
|
|
24861
|
+
} catch {
|
|
24862
|
+
return "";
|
|
24863
|
+
}
|
|
24864
|
+
}
|
|
24863
24865
|
function extractConversationFingerprint(parsedBody, path) {
|
|
24864
24866
|
let system;
|
|
24865
24867
|
let user;
|
|
@@ -24868,30 +24870,15 @@ function extractConversationFingerprint(parsedBody, path) {
|
|
|
24868
24870
|
system = parsedBody.instructions;
|
|
24869
24871
|
user = parsedBody.input;
|
|
24870
24872
|
break;
|
|
24871
|
-
case "messages":
|
|
24873
|
+
case "messages":
|
|
24872
24874
|
system = parsedBody.system;
|
|
24873
|
-
|
|
24874
|
-
if (Array.isArray(messages)) user = messages.find((m) => m.role === "user" && m.content != null)?.content;
|
|
24875
|
+
user = firstContent(parsedBody.messages, "user");
|
|
24875
24876
|
break;
|
|
24876
|
-
|
|
24877
|
-
|
|
24878
|
-
|
|
24879
|
-
if (Array.isArray(messages)) {
|
|
24880
|
-
const firstSystem = messages.find((m) => (m.role === "system" || m.role === "developer") && m.content != null);
|
|
24881
|
-
const firstUser = messages.find((m) => m.role === "user" && m.content != null);
|
|
24882
|
-
system = firstSystem?.content;
|
|
24883
|
-
user = firstUser?.content;
|
|
24884
|
-
}
|
|
24885
|
-
}
|
|
24877
|
+
default:
|
|
24878
|
+
system = firstContent(parsedBody.messages, "system", "developer");
|
|
24879
|
+
user = firstContent(parsedBody.messages, "user");
|
|
24886
24880
|
}
|
|
24887
24881
|
if (system == null && user == null) return null;
|
|
24888
|
-
const safeStringify = (value) => {
|
|
24889
|
-
try {
|
|
24890
|
-
return JSON.stringify(value);
|
|
24891
|
-
} catch {
|
|
24892
|
-
return "";
|
|
24893
|
-
}
|
|
24894
|
-
};
|
|
24895
24882
|
const hash = createHash("sha256");
|
|
24896
24883
|
hash.update(String(parsedBody.model ?? ""));
|
|
24897
24884
|
if (system != null) hash.update(safeStringify(system));
|
|
@@ -25065,14 +25052,10 @@ function startProxyServer(config, onReady) {
|
|
|
25065
25052
|
//#endregion
|
|
25066
25053
|
//#region src/cli-commands.ts
|
|
25067
25054
|
/**
|
|
25068
|
-
*
|
|
25069
|
-
*
|
|
25070
|
-
* Kept in a separate module from `cli.ts` so tests can import the commands
|
|
25071
|
-
* without triggering the top-level `run()` invocation that `cli.ts` performs
|
|
25072
|
-
* on import.
|
|
25055
|
+
* CLI command tree. Separated from `cli.ts` so tests can import commands
|
|
25056
|
+
* without triggering `run()`.
|
|
25073
25057
|
*/
|
|
25074
|
-
|
|
25075
|
-
const configArgs$1 = {
|
|
25058
|
+
const configArgs = {
|
|
25076
25059
|
configPath: (0, import_cjs.option)({
|
|
25077
25060
|
long: "config",
|
|
25078
25061
|
short: "c",
|
|
@@ -25087,16 +25070,14 @@ const configArgs$1 = {
|
|
|
25087
25070
|
description: "OpenRouter API key (overrides config file & env)"
|
|
25088
25071
|
})
|
|
25089
25072
|
};
|
|
25090
|
-
/** `--json` flag for commands that can produce structured output. */
|
|
25091
25073
|
const jsonFlag = { json: (0, import_cjs.flag)({
|
|
25092
25074
|
long: "json",
|
|
25093
25075
|
description: "Output as JSON instead of formatted text"
|
|
25094
25076
|
}) };
|
|
25095
|
-
/** Build an OpenRouterDataClient from already-parsed args. */
|
|
25096
25077
|
async function makeClient(args) {
|
|
25097
25078
|
const cfg = await loadConfig({
|
|
25098
|
-
configPath: args.configPath
|
|
25099
|
-
openrouterKey: args.openrouterKey
|
|
25079
|
+
configPath: args.configPath,
|
|
25080
|
+
openrouterKey: args.openrouterKey
|
|
25100
25081
|
});
|
|
25101
25082
|
return new OpenRouterDataClient({
|
|
25102
25083
|
openrouterBaseUrl: cfg.openrouterBaseUrl,
|
|
@@ -25136,7 +25117,7 @@ const startCommand = (0, import_cjs.command)({
|
|
|
25136
25117
|
}
|
|
25137
25118
|
],
|
|
25138
25119
|
args: {
|
|
25139
|
-
configPath: configArgs
|
|
25120
|
+
configPath: configArgs.configPath,
|
|
25140
25121
|
port: (0, import_cjs.option)({
|
|
25141
25122
|
long: "port",
|
|
25142
25123
|
short: "p",
|
|
@@ -25156,7 +25137,7 @@ const startCommand = (0, import_cjs.command)({
|
|
|
25156
25137
|
long: "no-config",
|
|
25157
25138
|
description: "Skip config file discovery"
|
|
25158
25139
|
}),
|
|
25159
|
-
openrouterKey: configArgs
|
|
25140
|
+
openrouterKey: configArgs.openrouterKey,
|
|
25160
25141
|
verbose: (0, import_cjs.flag)({
|
|
25161
25142
|
long: "verbose",
|
|
25162
25143
|
description: "Enable verbose logging"
|
|
@@ -25165,11 +25146,11 @@ const startCommand = (0, import_cjs.command)({
|
|
|
25165
25146
|
handler: async ({ configPath, port, host, noConfig, openrouterKey, verbose }) => {
|
|
25166
25147
|
try {
|
|
25167
25148
|
const cfg = await loadConfig({
|
|
25168
|
-
configPath
|
|
25149
|
+
configPath,
|
|
25169
25150
|
noConfig,
|
|
25170
25151
|
port,
|
|
25171
25152
|
host,
|
|
25172
|
-
openrouterKey
|
|
25153
|
+
openrouterKey,
|
|
25173
25154
|
verbose
|
|
25174
25155
|
});
|
|
25175
25156
|
startProxyServer(cfg, () => {
|
|
@@ -25207,7 +25188,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25207
25188
|
add: (0, import_cjs.command)({
|
|
25208
25189
|
name: "add",
|
|
25209
25190
|
description: "Add a model override (interactive)",
|
|
25210
|
-
args: { ...configArgs
|
|
25191
|
+
args: { ...configArgs },
|
|
25211
25192
|
handler: async (args) => {
|
|
25212
25193
|
await addOverrideCommand({
|
|
25213
25194
|
client: await makeClient(args),
|
|
@@ -25218,7 +25199,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25218
25199
|
edit: (0, import_cjs.command)({
|
|
25219
25200
|
name: "edit",
|
|
25220
25201
|
description: "Edit an existing model override (interactive)",
|
|
25221
|
-
args: { ...configArgs
|
|
25202
|
+
args: { ...configArgs },
|
|
25222
25203
|
handler: async (args) => {
|
|
25223
25204
|
await editOverrideCommand(await makeClient(args), args.configPath);
|
|
25224
25205
|
}
|
|
@@ -25226,7 +25207,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25226
25207
|
remove: (0, import_cjs.command)({
|
|
25227
25208
|
name: "remove",
|
|
25228
25209
|
description: "Remove one or more model overrides (interactive)",
|
|
25229
|
-
args: { ...configArgs
|
|
25210
|
+
args: { ...configArgs },
|
|
25230
25211
|
handler: async (args) => {
|
|
25231
25212
|
await removeOverrideCommand({ configPath: args.configPath });
|
|
25232
25213
|
}
|
|
@@ -25235,7 +25216,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25235
25216
|
name: "list",
|
|
25236
25217
|
description: "List all model overrides",
|
|
25237
25218
|
args: {
|
|
25238
|
-
...configArgs
|
|
25219
|
+
...configArgs,
|
|
25239
25220
|
...jsonFlag
|
|
25240
25221
|
},
|
|
25241
25222
|
handler: async (args) => {
|
|
@@ -25248,7 +25229,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25248
25229
|
browse: (0, import_cjs.command)({
|
|
25249
25230
|
name: "browse",
|
|
25250
25231
|
description: "Browse OpenRouter models (interactive)",
|
|
25251
|
-
args: { ...configArgs
|
|
25232
|
+
args: { ...configArgs },
|
|
25252
25233
|
handler: async (args) => {
|
|
25253
25234
|
await browseModelsCommand(await makeClient(args));
|
|
25254
25235
|
}
|
|
@@ -25257,7 +25238,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25257
25238
|
name: "validate",
|
|
25258
25239
|
description: "Validate the current config (exit 0 ok, 1 invalid)",
|
|
25259
25240
|
args: {
|
|
25260
|
-
...configArgs
|
|
25241
|
+
...configArgs,
|
|
25261
25242
|
...jsonFlag
|
|
25262
25243
|
},
|
|
25263
25244
|
handler: async (args) => {
|
|
@@ -25272,7 +25253,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25272
25253
|
name: "show",
|
|
25273
25254
|
description: "Show resolved configuration (merged from defaults + file + env + flags)",
|
|
25274
25255
|
args: {
|
|
25275
|
-
...configArgs
|
|
25256
|
+
...configArgs,
|
|
25276
25257
|
...jsonFlag
|
|
25277
25258
|
},
|
|
25278
25259
|
handler: async (args) => {
|
|
@@ -25286,7 +25267,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25286
25267
|
wizard: (0, import_cjs.command)({
|
|
25287
25268
|
name: "wizard",
|
|
25288
25269
|
description: "Run interactive setup wizard",
|
|
25289
|
-
args: { ...configArgs
|
|
25270
|
+
args: { ...configArgs },
|
|
25290
25271
|
handler: async (args) => {
|
|
25291
25272
|
await runWizard({ configPath: args.configPath });
|
|
25292
25273
|
}
|
|
@@ -25294,7 +25275,7 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
25294
25275
|
menu: (0, import_cjs.command)({
|
|
25295
25276
|
name: "menu",
|
|
25296
25277
|
description: "Open interactive configuration menu",
|
|
25297
|
-
args: { ...configArgs
|
|
25278
|
+
args: { ...configArgs },
|
|
25298
25279
|
handler: async (args) => {
|
|
25299
25280
|
await runConfigMenu(await makeClient(args));
|
|
25300
25281
|
}
|
|
@@ -25319,10 +25300,7 @@ const doctorCli = (0, import_cjs.command)({
|
|
|
25319
25300
|
}
|
|
25320
25301
|
],
|
|
25321
25302
|
args: {
|
|
25322
|
-
|
|
25323
|
-
long: "json",
|
|
25324
|
-
description: "Output as JSON instead of formatted text"
|
|
25325
|
-
}),
|
|
25303
|
+
...jsonFlag,
|
|
25326
25304
|
offline: (0, import_cjs.flag)({
|
|
25327
25305
|
long: "offline",
|
|
25328
25306
|
description: "Skip network checks (upstream, npm)"
|
|
@@ -25356,8 +25334,14 @@ const rootCli = (0, import_cjs.subcommands)({
|
|
|
25356
25334
|
});
|
|
25357
25335
|
//#endregion
|
|
25358
25336
|
//#region src/cli.ts
|
|
25337
|
+
const INFO_FLAGS = [
|
|
25338
|
+
"--help",
|
|
25339
|
+
"-h",
|
|
25340
|
+
"--version",
|
|
25341
|
+
"-v"
|
|
25342
|
+
];
|
|
25359
25343
|
const userArgs = process.argv.slice(2);
|
|
25360
|
-
const isInfo = userArgs.
|
|
25344
|
+
const isInfo = userArgs.some((a) => INFO_FLAGS.includes(a));
|
|
25361
25345
|
if (!isInfo) (0, import_main.config)({ quiet: true });
|
|
25362
25346
|
async function handleStartupError(err) {
|
|
25363
25347
|
if (err instanceof MissingConfigError && process.stdin.isTTY) {
|
|
@@ -25399,17 +25383,11 @@ const finalArgv = needsDefault && !isInfo ? [
|
|
|
25399
25383
|
"proxitor",
|
|
25400
25384
|
"start",
|
|
25401
25385
|
...userArgs
|
|
25402
|
-
] :
|
|
25403
|
-
|
|
25404
|
-
const
|
|
25405
|
-
const
|
|
25406
|
-
"
|
|
25407
|
-
"-h",
|
|
25408
|
-
"--version",
|
|
25409
|
-
"-v"
|
|
25410
|
-
].includes(a));
|
|
25411
|
-
if (!needsDefault && firstNonFlag === "config" && !configHasInfo) {
|
|
25412
|
-
if (!configSub || !KNOWN_CONFIG_SUBS.has(configSub)) finalArgv.splice(finalArgv.lastIndexOf("config") + 1, 0, "menu");
|
|
25386
|
+
] : process.argv;
|
|
25387
|
+
if (!needsDefault && firstNonFlag === "config") {
|
|
25388
|
+
const configArgs = userArgs.slice(userArgs.indexOf("config") + 1);
|
|
25389
|
+
const configSub = configArgs.find((a) => !a.startsWith("-"));
|
|
25390
|
+
if (!configArgs.some((a) => INFO_FLAGS.includes(a)) && (!configSub || !KNOWN_CONFIG_SUBS.has(configSub))) finalArgv.splice(finalArgv.lastIndexOf("config") + 1, 0, "menu");
|
|
25413
25391
|
}
|
|
25414
25392
|
(0, import_cjs.run)((0, import_cjs.binary)(rootCli), finalArgv).catch((err) => void handleStartupError(err));
|
|
25415
25393
|
//#endregion
|