proxitor 0.6.2 → 0.8.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 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
@@ -10594,7 +10594,7 @@ const proxyConfigSchema = object({
10594
10594
  verbose: boolean().default(false),
10595
10595
  bodyLimit: string$1().min(1).default("50mb"),
10596
10596
  provider: providerConfigSchema.optional(),
10597
- attributionReferer: string$1().min(1).default("http://localhost"),
10597
+ attributionReferer: string$1().min(1).default("https://github.com/neiromaster/proxitor"),
10598
10598
  attributionTitle: string$1().min(1).default("proxitor"),
10599
10599
  headers: record(string$1(), string$1()).optional(),
10600
10600
  modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
@@ -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,17 +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
14822
  async function readRawBody(request, reqId) {
14720
14823
  try {
14721
14824
  return {
@@ -14832,7 +14935,7 @@ async function executeUpstream(upstreamUrl, method, headers, body, signal, path,
14832
14935
  });
14833
14936
  }
14834
14937
  logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
14835
- return buildUpstreamResponse(upstream, method);
14938
+ return buildUpstreamResponseWithLogging(upstream, method, reqId);
14836
14939
  }
14837
14940
  function createProxyServer(config, onReady) {
14838
14941
  const app = new Hono();
@@ -14893,7 +14996,7 @@ function startProxyServer(config, onReady) {
14893
14996
  }
14894
14997
  //#endregion
14895
14998
  //#region src/version.ts
14896
- const version = "0.6.2";
14999
+ const version = "0.8.0";
14897
15000
  //#endregion
14898
15001
  //#region src/cli.ts
14899
15002
  const argv = process.argv.slice(2);