@rynfar/meridian 1.29.1 → 1.30.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
@@ -7,31 +7,34 @@
7
7
  <a href="https://www.npmjs.com/package/@rynfar/meridian"><img src="https://img.shields.io/npm/v/@rynfar/meridian?style=flat-square&color=8b5cf6&label=npm" alt="npm"></a>
8
8
  <a href="#"><img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-a78bfa?style=flat-square" alt="Platform"></a>
9
9
  <a href="#"><img src="https://img.shields.io/badge/license-MIT-c4b5fd?style=flat-square" alt="License"></a>
10
+ <a href="https://discord.gg/7vNVFYBz"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white" alt="Discord"></a>
10
11
  </p>
11
12
 
12
13
  ---
13
14
 
14
- Meridian bridges the Claude Code SDK to the standard Anthropic API. No OAuth interception. No binary patches. No hacks. Just pure, documented SDK calls. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, OpenClaw, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, with session management, streaming, and prompt caching handled natively by the SDK.
15
+ Meridian bridges the Claude Code SDK to the standard Anthropic API. No OAuth interception. No binary patches. No hacks. Just pure, documented SDK calls. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, with session management, streaming, and prompt caching handled natively by the SDK.
15
16
 
16
- > [!IMPORTANT]
17
- > ### Meridian is unaffected by the April 5, 2025 third-party blocks
17
+ > [!NOTE]
18
+ > ### How Meridian works with Anthropic
18
19
  >
19
- > On April 5, 2025, Anthropic began blocking third-party tools that bypass Claude Code by intercepting OAuth tokens and replaying them against internal API endpoints. Tools that extract `~/.claude/` credentials, proxy raw OAuth bearer tokens, or patch Claude Code binaries to redirect traffic may no longer function.
20
+ > Meridian is built entirely on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk). Every request flows through `query()` the same documented function Anthropic provides for programmatic access. No OAuth tokens are extracted, no binaries are patched, nothing is reverse-engineered.
20
21
  >
21
- > **Meridian does not do any of this.** Its architecture is fundamentally different:
22
+ > Because we use the SDK, Anthropic remains in full control of prompt caching, context window management, compaction, rate limiting, and authentication. Meridian doesn't bypass these mechanisms — it depends on them. Max subscription tokens flow through the correct channel, governed by the same guardrails Anthropic built into Claude Code.
22
23
  >
23
- > - **SDK-native.** Every request calls [`query()`](https://docs.anthropic.com/en/docs/claude-code/sdk) from `@anthropic-ai/claude-agent-sdk` the same function Anthropic documents for programmatic Claude Code access. No OAuth tokens are extracted, intercepted, or replayed.
24
- > - **Real Claude Code sessions.** The SDK spawns the actual Claude Code process, manages its own authentication, and handles all communication with Anthropic's servers. Meridian's traffic doesn't *look like* Claude Code — it *is* Claude Code.
25
- > - **Documented API surface only.** Session resume, MCP tool servers, agent definitions, thinking configuration, permission modes, tool blocking — every feature Meridian uses is a published, documented SDK option. Nothing is reverse-engineered or patched.
26
- > - **Native benefits and controls preserved.** Prompt caching, conversation persistence, context window management, and compaction all function exactly as they do in Claude Code — because the SDK manages them directly. This means Anthropic's engineering investments in efficiency and their rate-limiting controls work as designed. Max subscription tokens flow through the correct channel, governed by the same guardrails Anthropic built into Claude Code. Meridian doesn't bypass these mechanisms; it depends on them.
24
+ > What Meridian adds is a **presentation and interoperability layer**. We translate Claude Code's output into the standard Anthropic API format so developers can connect the editors, terminals, and workflows they prefer. The SDK does the work; Meridian formats the result.
27
25
  >
28
- > A small number of adjustments were made in response to the April 5th changes notably stripping `anthropic-beta` headers that could trigger unintended Extra Usage billing on Max subscriptions ([#281](https://github.com/rynfar/meridian/issues/281)). We are also evaluating system prompt handling to ensure nothing conflicts with Claude Code's expectations. These are compatibility adjustments, not workarounds. Our philosophy is to let Claude Code be the foundation and never fight the SDK — we work with it and add our own layer on top.
26
+ > If you're looking for a tool that circumvents usage limits or bypasses Anthropic's controls, this project is not for you. We play nice with the SDK because we believe that's how developers can continue to choose their own frontends while respecting Anthropic's platform.
27
+
28
+ > [!WARNING]
29
+ > ### Why Meridian does not support OpenClaw
30
+ >
31
+ > There is technically a way to make Meridian work with OpenClaw, but we're not interested in pursuing it.
29
32
  >
30
- > **Our position is straightforward.** Anthropic asks developers to use Claude Code as the harness for Max subscription access we do. We call their SDK, respect its authentication flow, use its documented features, and operate within its designed boundaries. We are not circumventing Claude Code; we are building on top of it.
33
+ > The reason Claude Max offers generous usage limits is because Anthropic can justify it through Claude Code their harness, their optimizations, their control. OpenClaw blows through that with autonomous workflows that Anthropic has little ability to manage or optimize. Using Opus to check an email when a local model would handle it fine isn't efficient use it's waste that degrades the plan for everyone.
31
34
  >
32
- > What Meridian adds is a **presentation and interoperability layer**. We translate Claude Code's output into the standard Anthropic API format so developers can connect the editors, terminals, and workflows they prefer. The SDK does the work; Meridian formats the result. Developers should have the right to choose their own interface and integrate with their own tooling that's not circumvention, it's the reason SDKs exist.
35
+ > I built Meridian because I believe developers should have the right to use the frontend of their choice. But that right comes with a responsibility: don't wreck the subscription for the rest of us. Sloppy autonomous agents that burn through Claude Max tokens are directly counter-productive to developers like me who depend on the plan being sustainable.
33
36
  >
34
- > For Meridian to stop working, Anthropic would need to restrict the Claude Code SDK itself or remove documented features that legitimate SDK consumers depend on. We don't believe that's the intent. We're building within Anthropic's ecosystem and constraints because we genuinely value their tools and models. We simply want the freedom to choose how we experience them — and we hope Anthropic sees that as the kind of ecosystem engagement their SDK was designed to enable.
37
+ > Meridian's philosophy is simple play nice with the SDK, let Anthropic optimize how they see fit, and use the frontend you want within the constraints of Claude Code. OpenClaw is not just a frontend; it's an autonomous system that abuses the Max plan. We won't be supporting it.
35
38
 
36
39
  ## Quick Start
37
40
 
@@ -77,7 +80,7 @@ The Claude Code SDK provides programmatic access to Claude. But your favorite co
77
80
  - **Passthrough mode** — forward tool calls to the client instead of executing internally
78
81
  - **Multimodal** — images, documents, and file attachments pass through to Claude
79
82
  - **Multi-profile** — switch between Claude accounts instantly, no restart needed
80
- - **Telemetry dashboard** — real-time performance metrics at `/telemetry`
83
+ - **Telemetry dashboard** — real-time performance metrics at `/telemetry`, including token usage and prompt cache efficiency ([`MONITORING.md`](MONITORING.md))
81
84
 
82
85
  ## Multi-Profile Support
83
86
 
@@ -290,29 +293,6 @@ MERIDIAN_DEFAULT_AGENT=pi meridian
290
293
 
291
294
  Pi mimics Claude Code's User-Agent, so automatic detection isn't possible. The `MERIDIAN_DEFAULT_AGENT` env var tells Meridian to use the pi adapter for all unrecognized requests. If you run other agents alongside pi, use the `x-meridian-agent: pi` header instead (requires pi-ai support for custom headers).
292
295
 
293
- ### OpenClaw
294
-
295
- OpenClaw uses `@mariozechner/pi-ai` under the hood, so the pi adapter handles it with no additional code. Add a provider override in `~/.openclaw/openclaw.json`:
296
-
297
- ```json
298
- {
299
- "models": {
300
- "providers": {
301
- "anthropic": {
302
- "baseUrl": "http://127.0.0.1:3456",
303
- "apiKey": "dummy",
304
- "models": [
305
- { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (Meridian)" },
306
- { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (Meridian)" }
307
- ]
308
- }
309
- }
310
- }
311
- }
312
- ```
313
-
314
- Then start Meridian with the pi adapter: `MERIDIAN_DEFAULT_AGENT=pi meridian`
315
-
316
296
  ### Any Anthropic-compatible tool
317
297
 
318
298
  ```bash
@@ -331,7 +311,6 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
331
311
  | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
332
312
  | [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
333
313
  | [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see above) — requires `MERIDIAN_DEFAULT_AGENT=pi` |
334
- | [OpenClaw](https://github.com/openclaw/openclaw) | ✅ Verified | Provider config (see above) — uses pi adapter via `MERIDIAN_DEFAULT_AGENT=pi` |
335
314
  | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
336
315
 
337
316
  Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
@@ -529,7 +508,7 @@ You haven't run `meridian setup`. Without the plugin, OpenCode requests won't ha
529
508
 
530
509
  ## Contributing
531
510
 
532
- Issues and PRs welcome. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for module structure and dependency rules, [`CLAUDE.md`](CLAUDE.md) for coding guidelines, and [`E2E.md`](E2E.md) for end-to-end test procedures.
511
+ Issues and PRs welcome. Join the [Discord](https://discord.gg/7vNVFYBz) to discuss ideas before opening issues. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for module structure and dependency rules, [`CLAUDE.md`](CLAUDE.md) for coding guidelines, [`E2E.md`](E2E.md) for end-to-end test procedures, and [`MONITORING.md`](MONITORING.md) for understanding token usage and prompt cache health.
533
512
 
534
513
  ## License
535
514
 
Binary file
@@ -0,0 +1,54 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
+ <rect width="512" height="512" rx="112" fill="#1C1830"/>
3
+
4
+ <!-- Antenna -->
5
+ <line x1="256" y1="72" x2="256" y2="115" stroke="#A78BFA" stroke-width="8" stroke-linecap="round"/>
6
+ <circle cx="256" cy="64" r="14" fill="#C4B5FD"/>
7
+ <!-- Antenna signal arcs -->
8
+ <path d="M228 52 A32 32 0 0 1 284 52" fill="none" stroke="#C4B5FD" stroke-width="3" opacity="0.4" stroke-linecap="round"/>
9
+ <path d="M216 40 A44 44 0 0 1 296 40" fill="none" stroke="#C4B5FD" stroke-width="2.5" opacity="0.25" stroke-linecap="round"/>
10
+
11
+ <!-- Robot head -->
12
+ <rect x="128" y="115" width="256" height="200" rx="40" fill="#2D2750"/>
13
+ <rect x="128" y="115" width="256" height="200" rx="40" fill="none" stroke="#8B7CF6" stroke-width="4"/>
14
+
15
+ <!-- Eyes — screens with Meridian purple glow -->
16
+ <rect x="170" y="165" width="68" height="52" rx="14" fill="#1C1830" stroke="#8B7CF6" stroke-width="3"/>
17
+ <rect x="274" y="165" width="68" height="52" rx="14" fill="#1C1830" stroke="#8B7CF6" stroke-width="3"/>
18
+ <!-- Eye glow -->
19
+ <circle cx="204" cy="191" r="16" fill="#8B7CF6" opacity="0.8"/>
20
+ <circle cx="308" cy="191" r="16" fill="#8B7CF6" opacity="0.8"/>
21
+ <!-- Eye highlights -->
22
+ <circle cx="210" cy="185" r="5" fill="#C4B5FD" opacity="0.6"/>
23
+ <circle cx="314" cy="185" r="5" fill="#C4B5FD" opacity="0.6"/>
24
+
25
+ <!-- Mouth — friendly LED strip -->
26
+ <rect x="196" y="248" width="120" height="24" rx="12" fill="#1C1830" stroke="#8B7CF6" stroke-width="2"/>
27
+ <circle cx="220" cy="260" r="5" fill="#C4B5FD" opacity="0.7"/>
28
+ <circle cx="244" cy="260" r="5" fill="#8B7CF6"/>
29
+ <circle cx="268" cy="260" r="5" fill="#C4B5FD" opacity="0.7"/>
30
+ <circle cx="292" cy="260" r="5" fill="#8B7CF6" opacity="0.5"/>
31
+
32
+ <!-- Ear bolts -->
33
+ <circle cx="118" cy="200" r="16" fill="#2D2750" stroke="#8B7CF6" stroke-width="3"/>
34
+ <circle cx="118" cy="200" r="6" fill="#8B7CF6"/>
35
+ <circle cx="394" cy="200" r="16" fill="#2D2750" stroke="#8B7CF6" stroke-width="3"/>
36
+ <circle cx="394" cy="200" r="6" fill="#8B7CF6"/>
37
+
38
+ <!-- Neck -->
39
+ <rect x="228" y="315" width="56" height="28" rx="6" fill="#2D2750" stroke="#8B7CF6" stroke-width="2"/>
40
+
41
+ <!-- Body / chest plate -->
42
+ <rect x="148" y="343" width="216" height="120" rx="30" fill="#2D2750" stroke="#8B7CF6" stroke-width="4"/>
43
+
44
+ <!-- Meridian logo on chest — the globe axis + arcs -->
45
+ <line x1="256" y1="365" x2="256" y2="443" stroke="#8B7CF6" stroke-width="6" stroke-linecap="round"/>
46
+ <!-- Chest latitude arcs -->
47
+ <path d="M212 382 A48 48 0 0 1 300 382" fill="none" stroke="#C4B5FD" stroke-width="3" opacity="0.5"/>
48
+ <path d="M212 426 A48 48 0 0 0 300 426" fill="none" stroke="#C4B5FD" stroke-width="3" opacity="0.5"/>
49
+ <!-- Chest poles -->
50
+ <circle cx="256" cy="365" r="8" fill="#C4B5FD"/>
51
+ <circle cx="256" cy="443" r="8" fill="#C4B5FD"/>
52
+ <!-- Chest center node -->
53
+ <circle cx="256" cy="404" r="6" fill="#8B7CF6"/>
54
+ </svg>
Binary file
Binary file
@@ -6220,7 +6220,8 @@ function jsonSchemaToZod(schema) {
6220
6220
  function createPassthroughMcpServer(tools) {
6221
6221
  const server = createSdkMcpServer({ name: PASSTHROUGH_MCP_NAME });
6222
6222
  const toolNames = [];
6223
- for (const tool of tools) {
6223
+ const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
6224
+ for (const tool of sortedTools) {
6224
6225
  try {
6225
6226
  const zodSchema = tool.input_schema?.properties ? jsonSchemaToZod(tool.input_schema) : exports_external.object({});
6226
6227
  const shape = zodSchema instanceof exports_external.ZodObject ? zodSchema.shape : { input: exports_external.any() };
@@ -6286,6 +6287,16 @@ class TelemetryStore {
6286
6287
  }
6287
6288
  return results;
6288
6289
  }
6290
+ getLastForSession(sdkSessionId) {
6291
+ for (let i = 0;i < this.count; i++) {
6292
+ const idx = (this.head - 1 - i + this.capacity) % this.capacity;
6293
+ const metric = this.buffer[idx];
6294
+ if (metric && metric.sdkSessionId === sdkSessionId && metric.error === null) {
6295
+ return metric;
6296
+ }
6297
+ }
6298
+ return;
6299
+ }
6289
6300
  summarize(windowMs = 60 * 60 * 1000) {
6290
6301
  const since = Date.now() - windowMs;
6291
6302
  const metrics = this.getRecent({ limit: this.capacity, since });
@@ -6302,7 +6313,15 @@ class TelemetryStore {
6302
6313
  upstreamDuration: emptyPhase,
6303
6314
  totalDuration: emptyPhase,
6304
6315
  byModel: {},
6305
- byMode: {}
6316
+ byMode: {},
6317
+ tokenUsage: {
6318
+ totalInputTokens: 0,
6319
+ totalOutputTokens: 0,
6320
+ totalCacheReadTokens: 0,
6321
+ totalCacheCreationTokens: 0,
6322
+ avgCacheHitRate: 0,
6323
+ cacheMissOnResumeCount: 0
6324
+ }
6306
6325
  };
6307
6326
  }
6308
6327
  const errorCount = metrics.filter((m) => m.error !== null).length;
@@ -6328,6 +6347,26 @@ class TelemetryStore {
6328
6347
  entry.count++;
6329
6348
  entry.totalMs += m.totalDurationMs;
6330
6349
  }
6350
+ let totalInputTokens = 0;
6351
+ let totalOutputTokens = 0;
6352
+ let totalCacheReadTokens = 0;
6353
+ let totalCacheCreationTokens = 0;
6354
+ let cacheHitRateSum = 0;
6355
+ let cacheHitRateCount = 0;
6356
+ let cacheMissOnResumeCount = 0;
6357
+ for (const m of metrics) {
6358
+ totalInputTokens += m.inputTokens ?? 0;
6359
+ totalOutputTokens += m.outputTokens ?? 0;
6360
+ totalCacheReadTokens += m.cacheReadInputTokens ?? 0;
6361
+ totalCacheCreationTokens += m.cacheCreationInputTokens ?? 0;
6362
+ if (m.cacheHitRate !== undefined) {
6363
+ cacheHitRateSum += m.cacheHitRate;
6364
+ cacheHitRateCount++;
6365
+ }
6366
+ if (m.isResume && m.cacheHitRate !== undefined && m.cacheHitRate === 0) {
6367
+ cacheMissOnResumeCount++;
6368
+ }
6369
+ }
6331
6370
  return {
6332
6371
  windowMs,
6333
6372
  totalRequests: metrics.length,
@@ -6339,7 +6378,15 @@ class TelemetryStore {
6339
6378
  upstreamDuration: computePercentiles(upstreams),
6340
6379
  totalDuration: computePercentiles(totals),
6341
6380
  byModel: Object.fromEntries(Object.entries(byModel).map(([k, v]) => [k, { count: v.count, avgTotalMs: Math.round(v.totalMs / v.count) }])),
6342
- byMode: Object.fromEntries(Object.entries(byMode).map(([k, v]) => [k, { count: v.count, avgTotalMs: Math.round(v.totalMs / v.count) }]))
6381
+ byMode: Object.fromEntries(Object.entries(byMode).map(([k, v]) => [k, { count: v.count, avgTotalMs: Math.round(v.totalMs / v.count) }])),
6382
+ tokenUsage: {
6383
+ totalInputTokens,
6384
+ totalOutputTokens,
6385
+ totalCacheReadTokens,
6386
+ totalCacheCreationTokens,
6387
+ avgCacheHitRate: cacheHitRateCount > 0 ? Math.round(cacheHitRateSum / cacheHitRateCount * 100) / 100 : 0,
6388
+ cacheMissOnResumeCount
6389
+ }
6343
6390
  };
6344
6391
  }
6345
6392
  clear() {
@@ -6590,7 +6637,7 @@ function render(s, reqs, logs) {
6590
6637
  // Count lineage types for badges
6591
6638
  const lineageCounts = {};
6592
6639
  for (const r of reqs) { const t = r.lineageType || 'unknown'; lineageCounts[t] = (lineageCounts[t] || 0) + 1; }
6593
- const logCounts = { session: 0, lineage: 0, error: 0 };
6640
+ const logCounts = { session: 0, lineage: 0, error: 0, token: 0 };
6594
6641
  for (const l of logs) { if (logCounts[l.category] !== undefined) logCounts[l.category]++; }
6595
6642
 
6596
6643
  // Tabs
@@ -6615,6 +6662,20 @@ function render(s, reqs, logs) {
6615
6662
  + card('Queue Wait', ms(s.queueWait.p50), 'p95: ' + ms(s.queueWait.p95))
6616
6663
  + '</div>';
6617
6664
 
6665
+ // Token usage cards
6666
+ if (s.tokenUsage) {
6667
+ const t = s.tokenUsage;
6668
+ const fmtTok = n => n > 1000000 ? (n/1000000).toFixed(1) + 'M' : n > 1000 ? Math.round(n/1000) + 'k' : String(n);
6669
+ html += '<div class="section"><div class="section-title">Token Usage</div></div>';
6670
+ html += '<div class="cards">'
6671
+ + card('Input Tokens', fmtTok(t.totalInputTokens), '')
6672
+ + card('Output Tokens', fmtTok(t.totalOutputTokens), '')
6673
+ + card('Cache Read', fmtTok(t.totalCacheReadTokens), '')
6674
+ + card('Cache Write', fmtTok(t.totalCacheCreationTokens), '')
6675
+ + card('Avg Cache Hit', (t.avgCacheHitRate * 100).toFixed(0) + '%', t.cacheMissOnResumeCount > 0 ? t.cacheMissOnResumeCount + ' cache miss on resume' : '')
6676
+ + '</div>';
6677
+ }
6678
+
6618
6679
  // Model breakdown
6619
6680
  const models = Object.entries(s.byModel);
6620
6681
  if (models.length > 0) {
@@ -6658,7 +6719,7 @@ function render(s, reqs, logs) {
6658
6719
  + '<span><span class="legend-dot" style="background:var(--upstream)"></span>Response</span>'
6659
6720
  + '</div>'
6660
6721
  + '<table><thead><tr><th>Time</th><th>Adapter</th><th>Model</th><th>Mode</th><th>Session</th><th>Status</th>'
6661
- + '<th>Queue</th><th>Proxy</th><th>TTFB</th><th>Total</th><th>Waterfall</th></tr></thead><tbody>';
6722
+ + '<th>Queue</th><th>Proxy</th><th>TTFB</th><th>Total</th><th>Tokens</th><th>Cache</th><th>Waterfall</th></tr></thead><tbody>';
6662
6723
 
6663
6724
  const maxTotal = Math.max(...reqs.map(r => r.totalDurationMs), 1);
6664
6725
 
@@ -6686,6 +6747,8 @@ function render(s, reqs, logs) {
6686
6747
  + '<td class="mono">' + ms(r.proxyOverheadMs) + '</td>'
6687
6748
  + '<td class="mono">' + ms(r.ttfbMs) + '</td>'
6688
6749
  + '<td class="mono">' + ms(r.totalDurationMs) + '</td>'
6750
+ + '<td class="mono">' + (r.inputTokens != null ? (r.inputTokens > 1000 ? Math.round(r.inputTokens/1000) + 'k' : r.inputTokens) + ' in<br>' + (r.outputTokens > 1000 ? Math.round(r.outputTokens/1000) + 'k' : r.outputTokens || 0) + ' out' : '—') + '</td>'
6751
+ + '<td class="mono">' + (r.cacheHitRate != null ? '<span style="color:' + (r.cacheHitRate > 0.5 ? 'var(--green)' : r.cacheHitRate > 0 ? 'var(--yellow)' : 'var(--red)') + '">' + Math.round(r.cacheHitRate * 100) + '%</span>' : '—') + '</td>'
6689
6752
  + '<td><div class="waterfall">'
6690
6753
  + '<div class="waterfall-seg queue" style="width:' + qW + 'px"></div>'
6691
6754
  + '<div class="waterfall-seg overhead" style="width:' + ohW + 'px"></div>'
@@ -6706,6 +6769,7 @@ function render(s, reqs, logs) {
6706
6769
  + '<span class="log-filter' + (activeLogFilter === 'session' ? ' active' : '') + '" data-filter="session" onclick="setLogFilter(&apos;session&apos;)" style="--accent:var(--blue)">Session<span class="tab-badge">' + logCounts.session + '</span></span>'
6707
6770
  + '<span class="log-filter' + (activeLogFilter === 'lineage' ? ' active' : '') + '" data-filter="lineage" onclick="setLogFilter(&apos;lineage&apos;)" style="--accent:var(--purple)">Lineage<span class="tab-badge">' + logCounts.lineage + '</span></span>'
6708
6771
  + '<span class="log-filter' + (activeLogFilter === 'error' ? ' active' : '') + '" data-filter="error" onclick="setLogFilter(&apos;error&apos;)" style="--accent:var(--red)">Error<span class="tab-badge">' + logCounts.error + '</span></span>'
6772
+ + '<span class="log-filter' + (activeLogFilter === 'token' ? ' active' : '') + '" data-filter="token" onclick="setLogFilter(&apos;token&apos;)" style="--accent:var(--yellow)">Token<span class="tab-badge">' + logCounts.token + '</span></span>'
6709
6773
  + '</div>';
6710
6774
 
6711
6775
  if (logs.length === 0) {
@@ -6717,7 +6781,7 @@ function render(s, reqs, logs) {
6717
6781
 
6718
6782
  for (const log of logs) {
6719
6783
  const levelColor = {info:'var(--green)',warn:'var(--yellow)',error:'var(--red)'}[log.level] || 'var(--muted)';
6720
- const catColor = {session:'var(--blue)',lineage:'var(--purple)',error:'var(--red)',lifecycle:'var(--muted)'}[log.category] || 'var(--muted)';
6784
+ const catColor = {session:'var(--blue)',lineage:'var(--purple)',error:'var(--red)',lifecycle:'var(--muted)',token:'var(--yellow)'}[log.category] || 'var(--muted)';
6721
6785
  const display = (activeLogFilter === 'all' || log.category === activeLogFilter) ? '' : 'display:none';
6722
6786
  html += '<tr class="log-row" data-category="' + log.category + '" style="' + display + '">'
6723
6787
  + '<td class="mono">' + ago(log.timestamp) + '</td>'
@@ -7702,6 +7766,16 @@ var openCodeAdapter = {
7702
7766
  getAllowedMcpTools() {
7703
7767
  return ALLOWED_MCP_TOOLS;
7704
7768
  },
7769
+ usesPassthrough() {
7770
+ const envVal = process.env.MERIDIAN_PASSTHROUGH ?? process.env.CLAUDE_PROXY_PASSTHROUGH;
7771
+ if (envVal === "0" || envVal === "false" || envVal === "no") {
7772
+ return false;
7773
+ }
7774
+ return true;
7775
+ },
7776
+ supportsThinking() {
7777
+ return true;
7778
+ },
7705
7779
  buildSdkAgents(body, mcpToolNames) {
7706
7780
  if (!Array.isArray(body.tools))
7707
7781
  return {};
@@ -13833,6 +13907,94 @@ function buildQueryOptions(ctx) {
13833
13907
  };
13834
13908
  }
13835
13909
 
13910
+ // src/proxy/betas.ts
13911
+ var BILLABLE_BETA_PREFIXES_ON_MAX = [
13912
+ "extended-cache-ttl-"
13913
+ ];
13914
+ var DEFAULT_BETA_POLICY = "allow-safe";
13915
+ function getBetaPolicyFromEnv() {
13916
+ const raw2 = process.env.MERIDIAN_BETA_POLICY;
13917
+ if (raw2 === "allow-safe" || raw2 === "strip-all" || raw2 === "allow-all") {
13918
+ return raw2;
13919
+ }
13920
+ return DEFAULT_BETA_POLICY;
13921
+ }
13922
+ function filterBetasForProfile(rawBetaHeader, profileType, policy = DEFAULT_BETA_POLICY) {
13923
+ if (!rawBetaHeader) {
13924
+ return { forwarded: undefined, stripped: [] };
13925
+ }
13926
+ const parsed = rawBetaHeader.split(",").map((b) => b.trim()).filter(Boolean);
13927
+ if (parsed.length === 0) {
13928
+ return { forwarded: undefined, stripped: [] };
13929
+ }
13930
+ if (profileType === "api") {
13931
+ return { forwarded: parsed, stripped: [] };
13932
+ }
13933
+ if (policy === "allow-all") {
13934
+ return { forwarded: parsed, stripped: [] };
13935
+ }
13936
+ if (policy === "strip-all") {
13937
+ return { forwarded: undefined, stripped: parsed };
13938
+ }
13939
+ const forwarded = [];
13940
+ const stripped = [];
13941
+ for (const beta of parsed) {
13942
+ if (BILLABLE_BETA_PREFIXES_ON_MAX.some((prefix) => beta.startsWith(prefix))) {
13943
+ stripped.push(beta);
13944
+ } else {
13945
+ forwarded.push(beta);
13946
+ }
13947
+ }
13948
+ return {
13949
+ forwarded: forwarded.length > 0 ? forwarded : undefined,
13950
+ stripped
13951
+ };
13952
+ }
13953
+
13954
+ // src/proxy/tokenHealth.ts
13955
+ var CONTEXT_SPIKE_THRESHOLD = 0.6;
13956
+ var CACHE_MISS_THRESHOLD = 0.05;
13957
+ var OUTPUT_EXPLOSION_RATIO = 2;
13958
+ var fmt = (n) => n > 1000 ? `${Math.round(n / 1000)}k` : String(n);
13959
+ function detectTokenAnomalies(current, previous) {
13960
+ const anomalies = [];
13961
+ if (previous && previous.inputTokens > 0) {
13962
+ const growth = (current.inputTokens - previous.inputTokens) / previous.inputTokens;
13963
+ if (growth > CONTEXT_SPIKE_THRESHOLD) {
13964
+ const pct = Math.round(growth * 100);
13965
+ anomalies.push({
13966
+ type: "context_spike",
13967
+ severity: growth > 2 ? "critical" : "warn",
13968
+ detail: `Input tokens grew ${pct}% in one turn (${fmt(previous.inputTokens)} -> ${fmt(current.inputTokens)}). Possible context leak or full replay.`
13969
+ });
13970
+ }
13971
+ }
13972
+ if (current.isResume && current.cacheHitRate <= CACHE_MISS_THRESHOLD && current.inputTokens > 0) {
13973
+ anomalies.push({
13974
+ type: "cache_miss",
13975
+ severity: "critical",
13976
+ detail: `Cache hit rate ${Math.round(current.cacheHitRate * 100)}% on resume (expected >50%). Prompt caching likely invalidated — check tool ordering or system prompt changes.`
13977
+ });
13978
+ }
13979
+ if (previous && previous.outputTokens > 0 && current.outputTokens > 0) {
13980
+ const ratio = current.outputTokens / previous.outputTokens;
13981
+ if (ratio > OUTPUT_EXPLOSION_RATIO && current.outputTokens > 2000) {
13982
+ anomalies.push({
13983
+ type: "output_explosion",
13984
+ severity: "warn",
13985
+ detail: `Output tokens ${fmt(current.outputTokens)} are ${ratio.toFixed(1)}x the previous turn (${fmt(previous.outputTokens)}).`
13986
+ });
13987
+ }
13988
+ }
13989
+ return anomalies;
13990
+ }
13991
+ function formatAnomalyAlerts(requestId, anomalies) {
13992
+ return anomalies.map((a) => {
13993
+ const icon = a.severity === "critical" ? "TOKEN ALERT" : "TOKEN WARN";
13994
+ return `[PROXY] ${requestId} ${icon}: ${a.detail}`;
13995
+ });
13996
+ }
13997
+
13836
13998
  // src/proxy/session/lineage.ts
13837
13999
  import { createHash as createHash2 } from "crypto";
13838
14000
  var MIN_SUFFIX_FOR_COMPACTION = 2;
@@ -14511,14 +14673,72 @@ function buildFreshPrompt(messages, stripCacheControl) {
14511
14673
  `) || "";
14512
14674
  }
14513
14675
  function logUsage(requestId, usage) {
14514
- const fmt = (n) => n > 1000 ? `${Math.round(n / 1000)}k` : String(n);
14676
+ const fmt2 = (n) => n > 1000 ? `${Math.round(n / 1000)}k` : String(n);
14677
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
14678
+ const totalInput = usage.input_tokens ?? 0;
14679
+ const cacheRate = totalInput > 0 ? Math.round(cacheRead / totalInput * 100) : 0;
14680
+ const cacheTag = totalInput > 0 ? ` cache=${cacheRate}%` : "";
14515
14681
  const parts = [
14516
- `input=${fmt(usage.input_tokens ?? 0)}`,
14517
- `output=${fmt(usage.output_tokens ?? 0)}`,
14518
- ...usage.cache_read_input_tokens ? [`cache_read=${fmt(usage.cache_read_input_tokens)}`] : [],
14519
- ...usage.cache_creation_input_tokens ? [`cache_write=${fmt(usage.cache_creation_input_tokens)}`] : []
14682
+ `input=${fmt2(usage.input_tokens ?? 0)}`,
14683
+ `output=${fmt2(usage.output_tokens ?? 0)}`,
14684
+ ...usage.cache_read_input_tokens ? [`cache_read=${fmt2(usage.cache_read_input_tokens)}`] : [],
14685
+ ...usage.cache_creation_input_tokens ? [`cache_write=${fmt2(usage.cache_creation_input_tokens)}`] : []
14520
14686
  ];
14521
- console.error(`[PROXY] ${requestId} usage: ${parts.join(" ")}`);
14687
+ console.error(`[PROXY] ${requestId} usage: ${parts.join(" ")}${cacheTag}`);
14688
+ }
14689
+ function computeCacheHitRate(usage) {
14690
+ if (!usage)
14691
+ return;
14692
+ const read = usage.cache_read_input_tokens ?? 0;
14693
+ const creation = usage.cache_creation_input_tokens ?? 0;
14694
+ const uncached = usage.input_tokens ?? 0;
14695
+ const total = uncached + read + creation;
14696
+ if (total === 0)
14697
+ return;
14698
+ return read / total;
14699
+ }
14700
+ function checkTokenHealth(requestId, sdkSessionId, usage, turnNumber, isResume, isPassthrough) {
14701
+ if (!usage || !sdkSessionId)
14702
+ return;
14703
+ const cacheHitRate = computeCacheHitRate(usage) ?? 0;
14704
+ const current = {
14705
+ requestId,
14706
+ turnNumber,
14707
+ inputTokens: usage.input_tokens ?? 0,
14708
+ outputTokens: usage.output_tokens ?? 0,
14709
+ cacheReadInputTokens: usage.cache_read_input_tokens ?? 0,
14710
+ cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
14711
+ cacheHitRate,
14712
+ isResume,
14713
+ isPassthrough
14714
+ };
14715
+ const prevMetric = telemetryStore.getLastForSession(sdkSessionId);
14716
+ const previous = prevMetric ? {
14717
+ requestId: prevMetric.requestId,
14718
+ turnNumber: turnNumber - 1,
14719
+ inputTokens: prevMetric.inputTokens ?? 0,
14720
+ outputTokens: prevMetric.outputTokens ?? 0,
14721
+ cacheReadInputTokens: prevMetric.cacheReadInputTokens ?? 0,
14722
+ cacheCreationInputTokens: prevMetric.cacheCreationInputTokens ?? 0,
14723
+ cacheHitRate: prevMetric.cacheHitRate ?? 0,
14724
+ isResume: prevMetric.isResume,
14725
+ isPassthrough: prevMetric.isPassthrough
14726
+ } : undefined;
14727
+ const anomalies = detectTokenAnomalies(current, previous);
14728
+ if (anomalies.length > 0) {
14729
+ const alerts = formatAnomalyAlerts(requestId, anomalies);
14730
+ for (const line of alerts) {
14731
+ console.error(line);
14732
+ }
14733
+ for (const a of anomalies) {
14734
+ diagnosticLog.log({
14735
+ level: a.severity === "critical" ? "error" : "warn",
14736
+ category: "token",
14737
+ message: `${requestId} ${a.type}: ${a.detail}`,
14738
+ requestId
14739
+ });
14740
+ }
14741
+ }
14522
14742
  }
14523
14743
  function createProxyServer(config = {}) {
14524
14744
  const finalConfig = { ...DEFAULT_PROXY_CONFIG, ...config };
@@ -14613,7 +14833,11 @@ function createProxyServer(config = {}) {
14613
14833
  const effortHeader = c.req.header("x-opencode-effort");
14614
14834
  const thinkingHeader = c.req.header("x-opencode-thinking");
14615
14835
  const taskBudgetHeader = c.req.header("x-opencode-task-budget");
14616
- const betaHeader = profile.type === "api" ? c.req.header("anthropic-beta") : undefined;
14836
+ const rawBetaHeader = c.req.header("anthropic-beta");
14837
+ const betaFilter = filterBetasForProfile(rawBetaHeader, profile.type, getBetaPolicyFromEnv());
14838
+ if (betaFilter.stripped.length > 0) {
14839
+ console.error(`[PROXY] ${requestMeta.requestId} stripped anthropic-beta(s) for Max profile: ${betaFilter.stripped.join(", ")}`);
14840
+ }
14617
14841
  const effort = effortHeader || body.effort || undefined;
14618
14842
  let thinking = body.thinking || undefined;
14619
14843
  if (thinkingHeader !== undefined) {
@@ -14625,10 +14849,7 @@ function createProxyServer(config = {}) {
14625
14849
  }
14626
14850
  const parsedBudget = taskBudgetHeader ? Number.parseInt(taskBudgetHeader, 10) : NaN;
14627
14851
  const taskBudget = Number.isFinite(parsedBudget) ? { total: parsedBudget } : body.task_budget ? { total: body.task_budget.total ?? body.task_budget } : undefined;
14628
- const betas = betaHeader ? betaHeader.split(",").map((b) => b.trim()).filter(Boolean) : undefined;
14629
- if (!betaHeader && c.req.header("anthropic-beta")) {
14630
- console.error(`[PROXY] ${requestMeta.requestId} stripped anthropic-beta header (Max subscription — betas trigger extra usage billing)`);
14631
- }
14852
+ const betas = betaFilter.forwarded;
14632
14853
  const agentSessionId = adapter.getSessionId(c);
14633
14854
  const profileSessionId = profile.id !== "default" && agentSessionId ? `${profile.id}:${agentSessionId}` : agentSessionId;
14634
14855
  const profileScopedCwd = profile.id !== "default" ? `${workingDirectory}::profile=${profile.id}` : workingDirectory;
@@ -14970,7 +15191,7 @@ function createProxyServer(config = {}) {
14970
15191
  } else {
14971
15192
  for (const block of message.message.content) {
14972
15193
  const b = block;
14973
- if (passthrough && (b.type === "thinking" || b.type === "redacted_thinking")) {
15194
+ if (passthrough && !adapter.supportsThinking?.() && (b.type === "thinking" || b.type === "redacted_thinking")) {
14974
15195
  claudeLog("passthrough.thinking_stripped", { mode: "non_stream", type: b.type });
14975
15196
  continue;
14976
15197
  }
@@ -15055,6 +15276,7 @@ Subprocess stderr: ${stderrOutput}`;
15055
15276
  hasToolUse
15056
15277
  });
15057
15278
  const nonStreamQueueWaitMs = requestMeta.queueStartedAt - requestMeta.queueEnteredAt;
15279
+ checkTokenHealth(requestMeta.requestId, currentSessionId || resumeSessionId, lastUsage, allMessages.length, isResume, passthrough);
15058
15280
  telemetryStore.record({
15059
15281
  requestId: requestMeta.requestId,
15060
15282
  timestamp: Date.now(),
@@ -15075,7 +15297,12 @@ Subprocess stderr: ${stderrOutput}`;
15075
15297
  totalDurationMs,
15076
15298
  contentBlocks: contentBlocks.length,
15077
15299
  textEvents: 0,
15078
- error: null
15300
+ error: null,
15301
+ inputTokens: lastUsage?.input_tokens,
15302
+ outputTokens: lastUsage?.output_tokens,
15303
+ cacheReadInputTokens: lastUsage?.cache_read_input_tokens,
15304
+ cacheCreationInputTokens: lastUsage?.cache_creation_input_tokens,
15305
+ cacheHitRate: computeCacheHitRate(lastUsage)
15079
15306
  });
15080
15307
  if (currentSessionId) {
15081
15308
  storeSession(profileSessionId, body.messages || [], currentSessionId, profileScopedCwd, sdkUuidMap, lastUsage);
@@ -15343,7 +15570,7 @@ data: ${JSON.stringify({ type: "message_stop" })}
15343
15570
  }
15344
15571
  if (eventType === "content_block_start") {
15345
15572
  const block = event.content_block;
15346
- if (passthrough && (block?.type === "thinking" || block?.type === "redacted_thinking")) {
15573
+ if (passthrough && !adapter.supportsThinking?.() && (block?.type === "thinking" || block?.type === "redacted_thinking")) {
15347
15574
  if (eventIndex !== undefined)
15348
15575
  skipBlockIndices.add(eventIndex);
15349
15576
  claudeLog("passthrough.thinking_stripped", { mode: "stream", type: block.type, index: eventIndex });
@@ -15525,6 +15752,7 @@ data: {"type":"message_stop"}
15525
15752
  textEventsForwarded
15526
15753
  });
15527
15754
  const streamQueueWaitMs = requestMeta.queueStartedAt - requestMeta.queueEnteredAt;
15755
+ checkTokenHealth(requestMeta.requestId, currentSessionId || resumeSessionId, lastUsage, allMessages.length, isResume, passthrough);
15528
15756
  telemetryStore.record({
15529
15757
  requestId: requestMeta.requestId,
15530
15758
  timestamp: Date.now(),
@@ -15545,7 +15773,12 @@ data: {"type":"message_stop"}
15545
15773
  totalDurationMs: streamTotalDurationMs,
15546
15774
  contentBlocks: eventsForwarded,
15547
15775
  textEvents: textEventsForwarded,
15548
- error: null
15776
+ error: null,
15777
+ inputTokens: lastUsage?.input_tokens,
15778
+ outputTokens: lastUsage?.output_tokens,
15779
+ cacheReadInputTokens: lastUsage?.cache_read_input_tokens,
15780
+ cacheCreationInputTokens: lastUsage?.cache_creation_input_tokens,
15781
+ cacheHitRate: computeCacheHitRate(lastUsage)
15549
15782
  });
15550
15783
  if (textEventsForwarded === 0) {
15551
15784
  claudeLog("response.empty_stream", {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-msyx6dnk.js";
4
+ } from "./cli-b2n69dg7.js";
5
5
  import"./cli-g9ypdz51.js";
6
6
  import"./cli-rtab0qa6.js";
7
7
  import"./cli-m9pfb7h9.js";
@@ -49,7 +49,7 @@ See https://github.com/rynfar/meridian for full documentation.`);
49
49
  process.exit(0);
50
50
  }
51
51
  if (args[0] === "profile") {
52
- const { profileAdd, profileList, profileRemove, profileSwitch, profileLogin, profileHelp } = await import("./profileCli-pdqrpw0m.js");
52
+ const { profileAdd, profileList, profileRemove, profileSwitch, profileLogin, profileHelp } = await import("./profileCli-5e3p99k0.js");
53
53
  const subcommand = args[1];
54
54
  const profileId = args[2];
55
55
  if (subcommand === "add" && profileId)
@@ -199,8 +199,8 @@ function profileLogin(id) {
199
199
  }
200
200
  }
201
201
  function promptYesNo(question) {
202
+ process.stderr.write(`${question} [Y/n] `);
202
203
  const result = spawnSync("node", ["-e", [
203
- `process.stdout.write(${JSON.stringify(`${question} [Y/n] `)});`,
204
204
  `const rl = require("readline").createInterface({ input: process.stdin });`,
205
205
  `rl.once("line", (a) => { process.stdout.write(a); rl.close(); });`,
206
206
  `rl.once("close", () => process.exit(0));`
@@ -82,6 +82,15 @@ export interface AgentAdapter {
82
82
  * When defined, takes precedence over the env var for this agent.
83
83
  */
84
84
  usesPassthrough?(): boolean;
85
+ /**
86
+ * Whether this agent's client can render thinking blocks.
87
+ *
88
+ * When true, thinking/redacted_thinking blocks are forwarded in
89
+ * passthrough mode instead of being stripped.
90
+ * When false or undefined, they are stripped (safe default for
91
+ * clients that may choke on the encrypted signature field).
92
+ */
93
+ supportsThinking?(): boolean;
85
94
  /**
86
95
  * Map a client-side tool_use block to file changes (passthrough mode).
87
96
  *
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/proxy/adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IAE5C;;;OAGG;IACH,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAEtD;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAAA;IAEtC;;;OAGG;IACH,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE3C;;;;OAIG;IACH,yBAAyB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE9C;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAA;IAE1B;;OAEG;IACH,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAAA;IAEvC;;;;OAIG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEhF;;;OAGG;IACH,aAAa,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAA;IAE9D;;;OAGG;IACH,0BAA0B,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAA;IAE9E;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAA;IAErC;;;;;;;;OAQG;IACH,eAAe,CAAC,IAAI,OAAO,CAAA;IAE3B;;;;;;;;;;;;;;;OAeG;IACH,6BAA6B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,eAAe,EAAE,UAAU,EAAE,CAAA;CAC3G"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/proxy/adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IAE5C;;;OAGG;IACH,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAEtD;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAAA;IAEtC;;;OAGG;IACH,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE3C;;;;OAIG;IACH,yBAAyB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE9C;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAA;IAE1B;;OAEG;IACH,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAAA;IAEvC;;;;OAIG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEhF;;;OAGG;IACH,aAAa,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAA;IAE9D;;;OAGG;IACH,0BAA0B,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAA;IAE9E;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAA;IAErC;;;;;;;;OAQG;IACH,eAAe,CAAC,IAAI,OAAO,CAAA;IAE3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,OAAO,CAAA;IAE5B;;;;;;;;;;;;;;;OAeG;IACH,6BAA6B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,eAAe,EAAE,UAAU,EAAE,CAAA;CAC3G"}
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/opencode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAQ9C,eAAO,MAAM,eAAe,EAAE,YAqG7B,CAAA"}
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/opencode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAQ9C,eAAO,MAAM,eAAe,EAAE,YAiH7B,CAAA"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * anthropic-beta header filtering for Max vs API profiles.
3
+ *
4
+ * Some betas (e.g. `extended-cache-ttl-*`) trigger Extra-Usage billing on
5
+ * Claude Max subscriptions. The default `allow-safe` policy strips only those
6
+ * for claude-max profiles while forwarding everything else so that prompt
7
+ * caching, 1M context, fine-grained tool streaming, and interleaved thinking
8
+ * continue to work as the SDK expects.
9
+ *
10
+ * Unconditional stripping (the previous behaviour) caused cache misses on
11
+ * every turn, which tripled TTFB and inflated token consumption roughly 3x on
12
+ * long conversations. See issue #278 for the original context.
13
+ *
14
+ * An operator can override the policy at runtime via the `MERIDIAN_BETA_POLICY`
15
+ * env var to force `strip-all` (safest — old behaviour) or `allow-all`
16
+ * (most permissive — matches api-profile behaviour) without a rebuild.
17
+ *
18
+ * This module is pure — no I/O, no imports from server.ts or session/.
19
+ */
20
+ import type { ProfileType } from "./profiles";
21
+ /**
22
+ * Beta prefixes that are known to trigger Extra-Usage billing on Max accounts.
23
+ *
24
+ * A beta is considered billable if its name starts with any of these strings.
25
+ * Keep this list conservative — prefer allowing unknown betas through over
26
+ * silently stripping something the SDK needs for normal operation.
27
+ */
28
+ export declare const BILLABLE_BETA_PREFIXES_ON_MAX: readonly string[];
29
+ /**
30
+ * Runtime policy for `anthropic-beta` header handling on claude-max profiles.
31
+ *
32
+ * - `allow-safe` (default): forward all betas except those matching
33
+ * {@link BILLABLE_BETA_PREFIXES_ON_MAX}. Restores prompt caching + 1M
34
+ * context while keeping the original billing-safety intent.
35
+ * - `strip-all`: the pre-fix (1.28.0 – 1.29.x) behaviour. Drops every beta
36
+ * for claude-max profiles. Use this as a kill switch if the allow-safe
37
+ * policy ever causes quota surprises.
38
+ * - `allow-all`: forward every beta unconditionally, same as api profiles.
39
+ * Use only if you've verified your Max tier treats all betas as free.
40
+ */
41
+ export type BetaPolicy = "allow-safe" | "strip-all" | "allow-all";
42
+ export declare const DEFAULT_BETA_POLICY: BetaPolicy;
43
+ export interface BetaFilterResult {
44
+ /** Betas to forward upstream. `undefined` means no header should be sent. */
45
+ forwarded: string[] | undefined;
46
+ /** Betas that were removed. Empty for api-type profiles. */
47
+ stripped: string[];
48
+ }
49
+ /**
50
+ * Read the beta policy from the `MERIDIAN_BETA_POLICY` env var.
51
+ *
52
+ * Falls back to {@link DEFAULT_BETA_POLICY} for missing or invalid values.
53
+ * Invalid values are silently ignored rather than crashing the proxy.
54
+ */
55
+ export declare function getBetaPolicyFromEnv(): BetaPolicy;
56
+ /**
57
+ * Filter an `anthropic-beta` header value for the given profile type.
58
+ *
59
+ * - For `api` profiles, all betas pass through unchanged regardless of policy.
60
+ * - For `claude-max` profiles, behaviour depends on `policy`:
61
+ * - `allow-safe` (default): strip only billable betas
62
+ * (see {@link BILLABLE_BETA_PREFIXES_ON_MAX}).
63
+ * - `strip-all`: strip every beta.
64
+ * - `allow-all`: forward every beta unchanged.
65
+ * - Whitespace and empty entries are trimmed.
66
+ * - Returns `forwarded: undefined` when the result would be an empty list so
67
+ * callers can omit the header entirely.
68
+ */
69
+ export declare function filterBetasForProfile(rawBetaHeader: string | undefined, profileType: ProfileType, policy?: BetaPolicy): BetaFilterResult;
70
+ //# sourceMappingURL=betas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"betas.d.ts","sourceRoot":"","sources":["../../src/proxy/betas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAE1D,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,CAAA;AAEjE,eAAO,MAAM,mBAAmB,EAAE,UAAyB,CAAA;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAC/B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAMjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,UAAgC,GACvC,gBAAgB,CA0ClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"passthroughTools.d.ts","sourceRoot":"","sources":["../../src/proxy/passthroughTools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,eAAO,MAAM,sBAAsB,cAAmC,CAAA;AAsCtE;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC;;;EAsCzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKvD"}
1
+ {"version":3,"file":"passthroughTools.d.ts","sourceRoot":"","sources":["../../src/proxy/passthroughTools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,eAAO,MAAM,sBAAsB,cAAmC,CAAA;AAsCtE;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC;;;EA2CzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKvD"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAqBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAoG7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA0pDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAuBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAyK7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAyrDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Token health anomaly detection.
3
+ *
4
+ * Pure functions that compare consecutive turn token snapshots
5
+ * and classify abnormal patterns. No I/O, no imports from server.ts.
6
+ */
7
+ export interface TokenSnapshot {
8
+ requestId: string;
9
+ turnNumber: number;
10
+ inputTokens: number;
11
+ outputTokens: number;
12
+ cacheReadInputTokens: number;
13
+ cacheCreationInputTokens: number;
14
+ cacheHitRate: number;
15
+ isResume: boolean;
16
+ isPassthrough: boolean;
17
+ }
18
+ export interface TokenAnomaly {
19
+ type: "context_spike" | "cache_miss" | "output_explosion";
20
+ severity: "warn" | "critical";
21
+ detail: string;
22
+ }
23
+ /**
24
+ * Compare a current turn's token snapshot against the previous turn.
25
+ * Returns an array of detected anomalies (empty if everything looks normal).
26
+ */
27
+ export declare function detectTokenAnomalies(current: TokenSnapshot, previous: TokenSnapshot | undefined): TokenAnomaly[];
28
+ /**
29
+ * Format anomalies as stderr log lines.
30
+ */
31
+ export declare function formatAnomalyAlerts(requestId: string, anomalies: TokenAnomaly[]): string[];
32
+ //# sourceMappingURL=tokenHealth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenHealth.d.ts","sourceRoot":"","sources":["../../src/proxy/tokenHealth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,wBAAwB,EAAE,MAAM,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,eAAe,GAAG,YAAY,GAAG,kBAAkB,CAAA;IACzD,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;IAC7B,MAAM,EAAE,MAAM,CAAA;CACf;AAaD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,YAAY,EAAE,CAsChB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAK1F"}
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getMaxSessionsLimit,
7
7
  hashMessage,
8
8
  startProxyServer
9
- } from "./cli-msyx6dnk.js";
9
+ } from "./cli-b2n69dg7.js";
10
10
  import"./cli-g9ypdz51.js";
11
11
  import"./cli-rtab0qa6.js";
12
12
  import"./cli-m9pfb7h9.js";
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QA4UlB,CAAA"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QA6VlB,CAAA"}
@@ -11,7 +11,7 @@ export interface DiagnosticLog {
11
11
  /** Log level */
12
12
  level: "info" | "warn" | "error";
13
13
  /** Log category for filtering */
14
- category: "session" | "lineage" | "error" | "lifecycle";
14
+ category: "session" | "lineage" | "error" | "lifecycle" | "token";
15
15
  /** Request ID (if associated with a request) */
16
16
  requestId?: string;
17
17
  /** Human-readable message */
@@ -1 +1 @@
1
- {"version":3,"file":"logStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/logStore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB;IAChB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IAChC,iCAAiC;IACjC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAA;IACvD,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;CAChB;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,0BAA0B;IAC1B,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,GAAG,IAAI;IAMlD,wCAAwC;IACxC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,qEAAqE;IACrE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,iCAAiC;IACjC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIhD;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAgB/F,6BAA6B;IAC7B,KAAK,IAAI,IAAI;CAKd;AAED,4CAA4C;AAC5C,eAAO,MAAM,aAAa,oBAA2B,CAAA"}
1
+ {"version":3,"file":"logStore.d.ts","sourceRoot":"","sources":["../../src/telemetry/logStore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB;IAChB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IAChC,iCAAiC;IACjC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CAAA;IACjE,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;CAChB;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,0BAA0B;IAC1B,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,GAAG,IAAI;IAMlD,wCAAwC;IACxC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,qEAAqE;IACrE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlD,iCAAiC;IACjC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIhD;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAgB/F,6BAA6B;IAC7B,KAAK,IAAI,IAAI;CAKd;AAED,4CAA4C;AAC5C,eAAO,MAAM,aAAa,oBAA2B,CAAA"}
@@ -26,6 +26,9 @@ export declare class TelemetryStore {
26
26
  since?: number;
27
27
  model?: string;
28
28
  }): RequestMetric[];
29
+ /** Find the most recent successful metric for a given SDK session ID.
30
+ * Used by anomaly detection to compare consecutive turns. */
31
+ getLastForSession(sdkSessionId: string): RequestMetric | undefined;
29
32
  /**
30
33
  * Compute aggregate statistics over a time window.
31
34
  * @param windowMs - Time window in ms (default: 1 hour)
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAe,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAY3E,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,yCAAyC;IACzC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAMnC,8CAA8C;IAC9C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAiB5F;;;OAGG;IACH,SAAS,CAAC,QAAQ,GAAE,MAAuB,GAAG,gBAAgB;IAwE9D,gCAAgC;IAChC,KAAK,IAAI,IAAI;CAKd;AAkBD,kDAAkD;AAClD,eAAO,MAAM,cAAc,gBAAuB,CAAA"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAe,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAY3E,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,QAAQ,CAAC,EAAE,MAAM;IAK7B,yCAAyC;IACzC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAMnC,8CAA8C;IAC9C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,aAAa,EAAE;IAiB5F;kEAC8D;IAC9D,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAWlE;;;OAGG;IACH,SAAS,CAAC,QAAQ,GAAE,MAAuB,GAAG,gBAAgB;IAiH9D,gCAAgC;IAChC,KAAK,IAAI,IAAI;CAKd;AAkBD,kDAAkD;AAClD,eAAO,MAAM,cAAc,gBAAuB,CAAA"}
@@ -57,6 +57,17 @@ export interface RequestMetric {
57
57
  textEvents: number;
58
58
  /** Error type if the request failed, null if successful */
59
59
  error: string | null;
60
+ /** Input tokens consumed (from SDK usage) */
61
+ inputTokens?: number;
62
+ /** Output tokens generated */
63
+ outputTokens?: number;
64
+ /** Input tokens read from prompt cache (lower cost) */
65
+ cacheReadInputTokens?: number;
66
+ /** Input tokens written to prompt cache (one-time cost) */
67
+ cacheCreationInputTokens?: number;
68
+ /** Cache hit ratio: cacheRead / (cacheRead + cacheCreation + uncached).
69
+ * 1.0 = perfect caching, 0.0 = no caching. undefined when no token data. */
70
+ cacheHitRate?: number;
60
71
  }
61
72
  export interface PhaseTiming {
62
73
  p50: number;
@@ -91,5 +102,15 @@ export interface TelemetrySummary {
91
102
  count: number;
92
103
  avgTotalMs: number;
93
104
  }>;
105
+ /** Aggregate token usage across all requests in the window */
106
+ tokenUsage: {
107
+ totalInputTokens: number;
108
+ totalOutputTokens: number;
109
+ totalCacheReadTokens: number;
110
+ totalCacheCreationTokens: number;
111
+ avgCacheHitRate: number;
112
+ /** Requests where cache hit rate was 0 despite being a resume */
113
+ cacheMissOnResumeCount: number;
114
+ };
94
115
  }
95
116
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/telemetry/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IAEjB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAA;IAEjB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IAEb,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,iCAAiC;IACjC,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAA;IAE7B,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,CAAA;IAEjB,0CAA0C;IAC1C,aAAa,EAAE,OAAO,CAAA;IAEtB;;;;;sEAKkE;IAClE,WAAW,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,CAAA;IAEzE,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IAEd,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IAEnB;;8CAE0C;IAC1C,eAAe,EAAE,MAAM,CAAA;IAEvB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAErB,yCAAyC;IACzC,kBAAkB,EAAE,MAAM,CAAA;IAE1B,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAA;IAEvB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAA;IAErB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAElB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IAEzB,iCAAiC;IACjC,SAAS,EAAE,WAAW,CAAA;IACtB,aAAa,EAAE,WAAW,CAAA;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,gBAAgB,EAAE,WAAW,CAAA;IAC7B,aAAa,EAAE,WAAW,CAAA;IAE1B,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9D,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC9D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/telemetry/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IAEjB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAA;IAEjB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IAEb,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,iCAAiC;IACjC,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAA;IAE7B,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,CAAA;IAEjB,0CAA0C;IAC1C,aAAa,EAAE,OAAO,CAAA;IAEtB;;;;;sEAKkE;IAClE,WAAW,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,CAAA;IAEzE,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IAEd,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IAEnB;;8CAE0C;IAC1C,eAAe,EAAE,MAAM,CAAA;IAEvB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAErB,yCAAyC;IACzC,kBAAkB,EAAE,MAAM,CAAA;IAE1B,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAA;IAEvB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAA;IAErB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAElB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAE7B,2DAA2D;IAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAA;IAEjC;iFAC6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IAEzB,iCAAiC;IACjC,SAAS,EAAE,WAAW,CAAA;IACtB,aAAa,EAAE,WAAW,CAAA;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,gBAAgB,EAAE,WAAW,CAAA;IAC7B,aAAa,EAAE,WAAW,CAAA;IAE1B,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9D,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAE7D,8DAA8D;IAC9D,UAAU,EAAE;QACV,gBAAgB,EAAE,MAAM,CAAA;QACxB,iBAAiB,EAAE,MAAM,CAAA;QACzB,oBAAoB,EAAE,MAAM,CAAA;QAC5B,wBAAwB,EAAE,MAAM,CAAA;QAChC,eAAe,EAAE,MAAM,CAAA;QACvB,iEAAiE;QACjE,sBAAsB,EAAE,MAAM,CAAA;KAC/B,CAAA;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.29.1",
3
+ "version": "1.30.0",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",