proxitor 0.6.1 → 0.7.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.
- package/README.md +18 -0
- package/dist/cli.mjs +167 -19
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -256,6 +256,24 @@ provider:
|
|
|
256
256
|
curl http://localhost:8828/health
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
### Cache usage logging
|
|
260
|
+
|
|
261
|
+
Proxitor automatically logs cache token usage from upstream responses — both non-streaming JSON and streaming SSE. No configuration needed.
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
[abc123] Cache read: 50000, write: 25000 tokens
|
|
265
|
+
[def456] Cache: no cached tokens
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Supports both provider formats:
|
|
269
|
+
|
|
270
|
+
| Provider format | Fields |
|
|
271
|
+
|---|---|
|
|
272
|
+
| Anthropic | `usage.cache_read_input_tokens` / `usage.cache_creation_input_tokens` |
|
|
273
|
+
| OpenAI / OpenRouter | `usage.prompt_tokens_details.cached_tokens` / `cache_write_tokens` |
|
|
274
|
+
|
|
275
|
+
When both formats are present (e.g., OpenRouter relaying an Anthropic response), Anthropic fields take priority.
|
|
276
|
+
|
|
259
277
|
---
|
|
260
278
|
|
|
261
279
|
## Interactive Config Manager
|
package/dist/cli.mjs
CHANGED
|
@@ -14626,7 +14626,7 @@ function buildRequestHeaders(incoming, config, inject, extraHeaders) {
|
|
|
14626
14626
|
const headers = filterHeaders(incoming, STRIP_REQUEST);
|
|
14627
14627
|
headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
|
|
14628
14628
|
headers["HTTP-Referer"] = config.attributionReferer;
|
|
14629
|
-
headers["X-Title"] = config.attributionTitle;
|
|
14629
|
+
headers["X-OpenRouter-Title"] = config.attributionTitle;
|
|
14630
14630
|
headers["Accept-Encoding"] = "identity";
|
|
14631
14631
|
if (extraHeaders) Object.assign(headers, extraHeaders);
|
|
14632
14632
|
if (inject) headers["Content-Type"] = "application/json";
|
|
@@ -14640,6 +14640,120 @@ function buildResponseHeaders(from) {
|
|
|
14640
14640
|
return headers;
|
|
14641
14641
|
}
|
|
14642
14642
|
//#endregion
|
|
14643
|
+
//#region src/proxy/cache-logging.ts
|
|
14644
|
+
function extractCacheUsage(bodyText) {
|
|
14645
|
+
try {
|
|
14646
|
+
const parsed = JSON.parse(bodyText);
|
|
14647
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
14648
|
+
const usage = parsed.usage;
|
|
14649
|
+
if (typeof usage !== "object" || usage === null) return void 0;
|
|
14650
|
+
const result = {
|
|
14651
|
+
cacheRead: 0,
|
|
14652
|
+
cacheCreate: 0
|
|
14653
|
+
};
|
|
14654
|
+
if (typeof usage.cache_read_input_tokens === "number") result.cacheRead = usage.cache_read_input_tokens;
|
|
14655
|
+
if (typeof usage.cache_creation_input_tokens === "number") result.cacheCreate = usage.cache_creation_input_tokens;
|
|
14656
|
+
const details = usage.prompt_tokens_details;
|
|
14657
|
+
if (typeof details === "object" && details !== null) {
|
|
14658
|
+
if (typeof details.cached_tokens === "number" && details.cached_tokens > 0 && result.cacheRead === 0) result.cacheRead = details.cached_tokens;
|
|
14659
|
+
if (typeof details.cache_write_tokens === "number" && details.cache_write_tokens > 0 && result.cacheCreate === 0) result.cacheCreate = details.cache_write_tokens;
|
|
14660
|
+
}
|
|
14661
|
+
return result;
|
|
14662
|
+
} catch {
|
|
14663
|
+
return;
|
|
14664
|
+
}
|
|
14665
|
+
}
|
|
14666
|
+
function applyAnthropicFields(u, result) {
|
|
14667
|
+
let found = false;
|
|
14668
|
+
if (typeof u.cache_read_input_tokens === "number" && u.cache_read_input_tokens > 0) {
|
|
14669
|
+
result.cacheRead = u.cache_read_input_tokens;
|
|
14670
|
+
found = true;
|
|
14671
|
+
}
|
|
14672
|
+
if (typeof u.cache_creation_input_tokens === "number" && u.cache_creation_input_tokens > 0) {
|
|
14673
|
+
result.cacheCreate = u.cache_creation_input_tokens;
|
|
14674
|
+
found = true;
|
|
14675
|
+
}
|
|
14676
|
+
return found;
|
|
14677
|
+
}
|
|
14678
|
+
function applyOpenAIFields(details, result) {
|
|
14679
|
+
let found = false;
|
|
14680
|
+
if (typeof details.cached_tokens === "number" && details.cached_tokens > 0) {
|
|
14681
|
+
result.cacheRead = details.cached_tokens;
|
|
14682
|
+
found = true;
|
|
14683
|
+
}
|
|
14684
|
+
if (typeof details.cache_write_tokens === "number" && details.cache_write_tokens > 0) {
|
|
14685
|
+
result.cacheCreate = details.cache_write_tokens;
|
|
14686
|
+
found = true;
|
|
14687
|
+
}
|
|
14688
|
+
return found;
|
|
14689
|
+
}
|
|
14690
|
+
function extractFromEvent(parsed, result) {
|
|
14691
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
14692
|
+
const usage = (parsed.message ?? parsed).usage;
|
|
14693
|
+
if (typeof usage !== "object" || usage === null) return false;
|
|
14694
|
+
const u = usage;
|
|
14695
|
+
let found = false;
|
|
14696
|
+
found = applyAnthropicFields(u, result) || found;
|
|
14697
|
+
const details = u.prompt_tokens_details;
|
|
14698
|
+
if (typeof details === "object" && details !== null) found = applyOpenAIFields(details, result) || found;
|
|
14699
|
+
return found;
|
|
14700
|
+
}
|
|
14701
|
+
function extractCacheUsageFromSSE(fullText) {
|
|
14702
|
+
const result = {
|
|
14703
|
+
cacheRead: 0,
|
|
14704
|
+
cacheCreate: 0
|
|
14705
|
+
};
|
|
14706
|
+
let found = false;
|
|
14707
|
+
for (const line of fullText.split("\n")) {
|
|
14708
|
+
if (!line.startsWith("data:")) continue;
|
|
14709
|
+
const payload = line.slice(5).trim();
|
|
14710
|
+
if (payload === "[DONE]") continue;
|
|
14711
|
+
try {
|
|
14712
|
+
if (extractFromEvent(JSON.parse(payload), result)) found = true;
|
|
14713
|
+
} catch {}
|
|
14714
|
+
}
|
|
14715
|
+
return found ? result : void 0;
|
|
14716
|
+
}
|
|
14717
|
+
function formatCacheUsage(usage, reqId) {
|
|
14718
|
+
const parts = [];
|
|
14719
|
+
if (usage.cacheRead > 0) parts.push(`read: ${usage.cacheRead}`);
|
|
14720
|
+
if (usage.cacheCreate > 0) parts.push(`write: ${usage.cacheCreate}`);
|
|
14721
|
+
logger.info(withReq(reqId, parts.length > 0 ? `Cache ${parts.join(", ")} tokens` : "Cache: no cached tokens"));
|
|
14722
|
+
}
|
|
14723
|
+
function createLoggingStream(contentType, reqId) {
|
|
14724
|
+
const chunks = [];
|
|
14725
|
+
return new TransformStream({
|
|
14726
|
+
transform(chunk, controller) {
|
|
14727
|
+
controller.enqueue(chunk);
|
|
14728
|
+
chunks.push(chunk);
|
|
14729
|
+
},
|
|
14730
|
+
flush() {
|
|
14731
|
+
try {
|
|
14732
|
+
const decoder = new TextDecoder();
|
|
14733
|
+
const text = chunks.reduce((acc, chunk) => acc + decoder.decode(chunk, { stream: true }), "") + decoder.decode();
|
|
14734
|
+
const usage = contentType.toLowerCase().includes("text/event-stream") ? extractCacheUsageFromSSE(text) : extractCacheUsage(text);
|
|
14735
|
+
if (usage) formatCacheUsage(usage, reqId);
|
|
14736
|
+
} catch (err) {
|
|
14737
|
+
logger.debug(withReq(reqId, `Cache usage extraction failed: ${err instanceof Error ? err.message : err}`));
|
|
14738
|
+
}
|
|
14739
|
+
}
|
|
14740
|
+
});
|
|
14741
|
+
}
|
|
14742
|
+
function buildUpstreamResponseWithLogging(upstream, method, reqId) {
|
|
14743
|
+
const headers = buildResponseHeaders(upstream.headers);
|
|
14744
|
+
if (method === "HEAD" || !upstream.body) return new Response(null, {
|
|
14745
|
+
status: upstream.status,
|
|
14746
|
+
headers
|
|
14747
|
+
});
|
|
14748
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
14749
|
+
const lower = contentType.toLowerCase();
|
|
14750
|
+
const body = lower.includes("application/json") || lower.includes("text/event-stream") ? upstream.body.pipeThrough(createLoggingStream(contentType, reqId)) : upstream.body;
|
|
14751
|
+
return new Response(body, {
|
|
14752
|
+
status: upstream.status,
|
|
14753
|
+
headers
|
|
14754
|
+
});
|
|
14755
|
+
}
|
|
14756
|
+
//#endregion
|
|
14643
14757
|
//#region src/proxy/inject.ts
|
|
14644
14758
|
/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */
|
|
14645
14759
|
function extractModel(rawBody) {
|
|
@@ -14705,18 +14819,6 @@ async function fetchUpstream(url, method, headers, body, signal) {
|
|
|
14705
14819
|
duplex: body ? "half" : void 0
|
|
14706
14820
|
});
|
|
14707
14821
|
}
|
|
14708
|
-
function buildUpstreamResponse(upstream, method) {
|
|
14709
|
-
const headers = buildResponseHeaders(upstream.headers);
|
|
14710
|
-
if (method === "HEAD" || !upstream.body) return new Response(null, {
|
|
14711
|
-
status: upstream.status,
|
|
14712
|
-
headers
|
|
14713
|
-
});
|
|
14714
|
-
return new Response(upstream.body, {
|
|
14715
|
-
status: upstream.status,
|
|
14716
|
-
headers
|
|
14717
|
-
});
|
|
14718
|
-
}
|
|
14719
|
-
/** Read and process the request body, returning an error response on failure */
|
|
14720
14822
|
async function readRawBody(request, reqId) {
|
|
14721
14823
|
try {
|
|
14722
14824
|
return {
|
|
@@ -14735,7 +14837,6 @@ async function readRawBody(request, reqId) {
|
|
|
14735
14837
|
};
|
|
14736
14838
|
}
|
|
14737
14839
|
}
|
|
14738
|
-
/** Resolve per-request config: extract model, resolve overrides, build routing and body */
|
|
14739
14840
|
function resolveRequest(rawBody, config, method, path, reqId) {
|
|
14740
14841
|
const modelName = extractModel(rawBody);
|
|
14741
14842
|
const resolved = resolveModelConfig(config, modelName);
|
|
@@ -14768,7 +14869,41 @@ function resolveRequest(rawBody, config, method, path, reqId) {
|
|
|
14768
14869
|
headers: resolved.headers
|
|
14769
14870
|
};
|
|
14770
14871
|
}
|
|
14771
|
-
/**
|
|
14872
|
+
/**
|
|
14873
|
+
* Extract a readable error detail from an upstream response body.
|
|
14874
|
+
*
|
|
14875
|
+
* OpenRouter error format:
|
|
14876
|
+
* { error: { code: 400, message: "...", metadata: { raw: "...", provider_name: "..." } } }
|
|
14877
|
+
*
|
|
14878
|
+
* - `error.message` — human-readable summary
|
|
14879
|
+
* - `error.metadata.provider_name` — which provider caused it (null = OpenRouter itself)
|
|
14880
|
+
* - `error.metadata.raw` — the original provider error (most specific cause)
|
|
14881
|
+
*/
|
|
14882
|
+
function formatMetadata(meta) {
|
|
14883
|
+
const parts = [];
|
|
14884
|
+
if (meta.provider_name) parts.push(`provider=${meta.provider_name}`);
|
|
14885
|
+
if (meta.raw) {
|
|
14886
|
+
const raw = typeof meta.raw === "string" ? meta.raw : JSON.stringify(meta.raw);
|
|
14887
|
+
parts.push(raw);
|
|
14888
|
+
}
|
|
14889
|
+
return parts;
|
|
14890
|
+
}
|
|
14891
|
+
function extractErrorDetail(bodyText) {
|
|
14892
|
+
try {
|
|
14893
|
+
const parsed = JSON.parse(bodyText);
|
|
14894
|
+
if (typeof parsed !== "object" || parsed === null) return bodyText;
|
|
14895
|
+
const err = parsed.error;
|
|
14896
|
+
if (typeof err === "object" && err !== null && err.message) {
|
|
14897
|
+
const parts = [];
|
|
14898
|
+
if (err.code != null) parts.push(String(err.code));
|
|
14899
|
+
parts.push(String(err.message));
|
|
14900
|
+
if (err.metadata && typeof err.metadata === "object") parts.push(...formatMetadata(err.metadata));
|
|
14901
|
+
return parts.join(" | ");
|
|
14902
|
+
}
|
|
14903
|
+
if (parsed.message) return String(parsed.message);
|
|
14904
|
+
} catch {}
|
|
14905
|
+
return bodyText;
|
|
14906
|
+
}
|
|
14772
14907
|
async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt, reqId) {
|
|
14773
14908
|
let upstream;
|
|
14774
14909
|
try {
|
|
@@ -14784,8 +14919,23 @@ async function executeUpstream(upstreamUrl, method, headers, body, signal, path,
|
|
|
14784
14919
|
type: "proxy_upstream_error"
|
|
14785
14920
|
} }, { status: 502 });
|
|
14786
14921
|
}
|
|
14922
|
+
if (upstream.status >= 400) {
|
|
14923
|
+
const bodyText = await upstream.text();
|
|
14924
|
+
const detail = extractErrorDetail(bodyText);
|
|
14925
|
+
const truncated = detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
|
|
14926
|
+
(upstream.status >= 500 ? logger.error : logger.warn)(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms): ${truncated}`));
|
|
14927
|
+
const responseHeaders = buildResponseHeaders(upstream.headers);
|
|
14928
|
+
if (method === "HEAD") return new Response(null, {
|
|
14929
|
+
status: upstream.status,
|
|
14930
|
+
headers: responseHeaders
|
|
14931
|
+
});
|
|
14932
|
+
return new Response(bodyText, {
|
|
14933
|
+
status: upstream.status,
|
|
14934
|
+
headers: responseHeaders
|
|
14935
|
+
});
|
|
14936
|
+
}
|
|
14787
14937
|
logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
|
|
14788
|
-
return
|
|
14938
|
+
return buildUpstreamResponseWithLogging(upstream, method, reqId);
|
|
14789
14939
|
}
|
|
14790
14940
|
function createProxyServer(config, onReady) {
|
|
14791
14941
|
const app = new Hono();
|
|
@@ -14822,9 +14972,7 @@ function createProxyServer(config, onReady) {
|
|
|
14822
14972
|
hostname: config.host
|
|
14823
14973
|
}, onReady);
|
|
14824
14974
|
}
|
|
14825
|
-
/** Shutdown deadline: force-close after this many ms */
|
|
14826
14975
|
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
14827
|
-
/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */
|
|
14828
14976
|
function startProxyServer(config, onReady) {
|
|
14829
14977
|
const server = createProxyServer(config, onReady);
|
|
14830
14978
|
let shuttingDown = false;
|
|
@@ -14848,7 +14996,7 @@ function startProxyServer(config, onReady) {
|
|
|
14848
14996
|
}
|
|
14849
14997
|
//#endregion
|
|
14850
14998
|
//#region src/version.ts
|
|
14851
|
-
const version = "0.
|
|
14999
|
+
const version = "0.7.0";
|
|
14852
15000
|
//#endregion
|
|
14853
15001
|
//#region src/cli.ts
|
|
14854
15002
|
const argv = process.argv.slice(2);
|