@rynfar/meridian 1.50.0 → 1.51.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.
Files changed (40) hide show
  1. package/README.md +51 -19
  2. package/assets/banner.svg +28 -21
  3. package/assets/bot-icon-512.png +0 -0
  4. package/assets/bot-icon.svg +49 -43
  5. package/assets/how-it-works.svg +40 -40
  6. package/assets/icon-512.png +0 -0
  7. package/assets/icon.svg +16 -13
  8. package/assets/logo-512.png +0 -0
  9. package/assets/logo.svg +14 -12
  10. package/dist/{cli-yw8cx02m.js → cli-7df5eg5d.js} +828 -164
  11. package/dist/{cli-q6dz9r5p.js → cli-aybvv9nb.js} +7 -1
  12. package/dist/cli-xmweegb1.js +179 -0
  13. package/dist/cli.js +4 -4
  14. package/dist/{pluginPage-85s6t6k8.js → pluginPage-03q5j753.js} +4 -10
  15. package/dist/{profileCli-fbhe9p5z.js → profileCli-j01rr99j.js} +1 -1
  16. package/dist/{profilePage-txxpbc44.js → profilePage-naychnb8.js} +6 -5
  17. package/dist/proxy/oauthUsage.d.ts.map +1 -1
  18. package/dist/proxy/plugins/pluginPage.d.ts.map +1 -1
  19. package/dist/proxy/server.d.ts.map +1 -1
  20. package/dist/server.js +3 -3
  21. package/dist/telemetry/dashboard.d.ts.map +1 -1
  22. package/dist/telemetry/landing.d.ts +6 -2
  23. package/dist/telemetry/landing.d.ts.map +1 -1
  24. package/dist/telemetry/percentiles.d.ts +5 -1
  25. package/dist/telemetry/percentiles.d.ts.map +1 -1
  26. package/dist/telemetry/pricing.d.ts +63 -0
  27. package/dist/telemetry/pricing.d.ts.map +1 -0
  28. package/dist/telemetry/pricingStore.d.ts +32 -0
  29. package/dist/telemetry/pricingStore.d.ts.map +1 -0
  30. package/dist/telemetry/profileBar.d.ts +19 -9
  31. package/dist/telemetry/profileBar.d.ts.map +1 -1
  32. package/dist/telemetry/profileUsage.d.ts.map +1 -1
  33. package/dist/telemetry/settingsPage.d.ts +1 -1
  34. package/dist/telemetry/settingsPage.d.ts.map +1 -1
  35. package/dist/telemetry/sqlite.d.ts.map +1 -1
  36. package/dist/telemetry/store.d.ts.map +1 -1
  37. package/dist/telemetry/types.d.ts +31 -0
  38. package/dist/telemetry/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/cli-4rqtm83g.js +0 -148
@@ -20,7 +20,7 @@ import {
20
20
  profileBarHtml,
21
21
  profileBarJs,
22
22
  themeCss
23
- } from "./cli-4rqtm83g.js";
23
+ } from "./cli-xmweegb1.js";
24
24
  import {
25
25
  env,
26
26
  envBool,
@@ -29,6 +29,7 @@ import {
29
29
  getClaudeAuthStatusAsync,
30
30
  getResolvedClaudeExecutableInfo,
31
31
  hasExtendedContext,
32
+ init_env,
32
33
  isClosedControllerError,
33
34
  mapModelToClaudeModel,
34
35
  recordExtendedContextUnavailable,
@@ -36,7 +37,7 @@ import {
36
37
  resolvePassthrough,
37
38
  resolveSdkModelDefaults,
38
39
  stripExtendedContext
39
- } from "./cli-q6dz9r5p.js";
40
+ } from "./cli-aybvv9nb.js";
40
41
  import {
41
42
  getSetting
42
43
  } from "./cli-340h1chz.js";
@@ -61,6 +62,137 @@ import {
61
62
  __toESM
62
63
  } from "./cli-p9swy5t3.js";
63
64
 
65
+ // src/telemetry/pricing.ts
66
+ var exports_pricing = {};
67
+ __export(exports_pricing, {
68
+ resolveModelPricing: () => resolveModelPricing,
69
+ normalizeModelKey: () => normalizeModelKey,
70
+ estimateRequestCostUsd: () => estimateRequestCostUsd,
71
+ computeCostEstimate: () => computeCostEstimate,
72
+ CACHE_WRITE_MULTIPLIER: () => CACHE_WRITE_MULTIPLIER,
73
+ CACHE_READ_MULTIPLIER: () => CACHE_READ_MULTIPLIER,
74
+ BUILTIN_MODEL_PRICING: () => BUILTIN_MODEL_PRICING
75
+ });
76
+ function normalizeModelKey(model) {
77
+ return model.trim().toLowerCase().replace(/\[1m\]$/, "");
78
+ }
79
+ function rates(inputPerMTok, outputPerMTok) {
80
+ return {
81
+ inputPerMTok,
82
+ outputPerMTok,
83
+ cacheReadPerMTok: inputPerMTok * CACHE_READ_MULTIPLIER,
84
+ cacheWritePerMTok: inputPerMTok * CACHE_WRITE_MULTIPLIER
85
+ };
86
+ }
87
+ function resolveModelPricing(model, overrides) {
88
+ const normalized = normalizeModelKey(model);
89
+ if (overrides) {
90
+ const override = overrides[normalized];
91
+ if (override)
92
+ return override;
93
+ }
94
+ const exact = BUILTIN_MODEL_PRICING[normalized];
95
+ if (exact)
96
+ return exact;
97
+ if (normalized.includes("fable") || normalized.includes("mythos"))
98
+ return FABLE;
99
+ if (normalized.includes("haiku")) {
100
+ if (normalized.includes("3-5") || normalized.includes("3.5"))
101
+ return HAIKU_35;
102
+ if (/haiku-3\b|3-haiku/.test(normalized))
103
+ return HAIKU_3;
104
+ return HAIKU;
105
+ }
106
+ if (normalized.includes("opus")) {
107
+ if (/opus-4-1\b|opus-4-1-|opus-4-0|opus-4-2025|3-opus/.test(normalized))
108
+ return OPUS_LEGACY;
109
+ return OPUS;
110
+ }
111
+ if (normalized.includes("sonnet"))
112
+ return SONNET;
113
+ return null;
114
+ }
115
+ function estimateRequestCostUsd(metric, pricing) {
116
+ return (metric.inputTokens ?? 0) / 1e6 * pricing.inputPerMTok + (metric.outputTokens ?? 0) / 1e6 * pricing.outputPerMTok + (metric.cacheReadInputTokens ?? 0) / 1e6 * pricing.cacheReadPerMTok + (metric.cacheCreationInputTokens ?? 0) / 1e6 * pricing.cacheWritePerMTok;
117
+ }
118
+ function roundUsd(value) {
119
+ return Math.round(value * 1e6) / 1e6;
120
+ }
121
+ function computeCostEstimate(metrics, overrides) {
122
+ const byModel = {};
123
+ const byProfile = {};
124
+ let totalUsd = 0;
125
+ let unpricedRequestCount = 0;
126
+ for (const metric of metrics) {
127
+ const modelKey = metric.requestModel || metric.model;
128
+ const pricing = resolveModelPricing(modelKey, overrides);
129
+ const profileEntry = byProfile[metric.profileId ?? "default"] ??= { requests: 0, estimatedUsd: 0 };
130
+ profileEntry.requests++;
131
+ const entry = byModel[modelKey] ??= {
132
+ requests: 0,
133
+ inputTokens: 0,
134
+ outputTokens: 0,
135
+ cacheReadTokens: 0,
136
+ cacheCreationTokens: 0,
137
+ estimatedUsd: pricing === null ? null : 0
138
+ };
139
+ entry.requests++;
140
+ entry.inputTokens += metric.inputTokens ?? 0;
141
+ entry.outputTokens += metric.outputTokens ?? 0;
142
+ entry.cacheReadTokens += metric.cacheReadInputTokens ?? 0;
143
+ entry.cacheCreationTokens += metric.cacheCreationInputTokens ?? 0;
144
+ if (pricing === null) {
145
+ unpricedRequestCount++;
146
+ continue;
147
+ }
148
+ const cost = estimateRequestCostUsd(metric, pricing);
149
+ entry.estimatedUsd = (entry.estimatedUsd ?? 0) + cost;
150
+ profileEntry.estimatedUsd += cost;
151
+ totalUsd += cost;
152
+ }
153
+ for (const entry of Object.values(byProfile)) {
154
+ entry.estimatedUsd = roundUsd(entry.estimatedUsd);
155
+ }
156
+ for (const entry of Object.values(byModel)) {
157
+ if (entry.estimatedUsd !== null)
158
+ entry.estimatedUsd = roundUsd(entry.estimatedUsd);
159
+ }
160
+ return { totalUsd: roundUsd(totalUsd), byModel, unpricedRequestCount, byProfile };
161
+ }
162
+ var CACHE_READ_MULTIPLIER = 0.1, CACHE_WRITE_MULTIPLIER = 1.25, FABLE, OPUS, OPUS_LEGACY, SONNET, SONNET_5_INTRO, HAIKU, HAIKU_35, HAIKU_3, BUILTIN_MODEL_PRICING;
163
+ var init_pricing = __esm(() => {
164
+ FABLE = rates(10, 50);
165
+ OPUS = rates(5, 25);
166
+ OPUS_LEGACY = rates(15, 75);
167
+ SONNET = rates(3, 15);
168
+ SONNET_5_INTRO = rates(2, 10);
169
+ HAIKU = rates(1, 5);
170
+ HAIKU_35 = rates(0.8, 4);
171
+ HAIKU_3 = rates(0.25, 1.25);
172
+ BUILTIN_MODEL_PRICING = {
173
+ fable: FABLE,
174
+ "claude-fable-5": FABLE,
175
+ "claude-mythos-5": FABLE,
176
+ opus: OPUS,
177
+ "claude-opus-4-8": OPUS,
178
+ "claude-opus-4-7": OPUS,
179
+ "claude-opus-4-6": OPUS,
180
+ "claude-opus-4-5": OPUS,
181
+ "claude-opus-4-1": OPUS_LEGACY,
182
+ "claude-opus-4-0": OPUS_LEGACY,
183
+ "claude-opus-4-20250514": OPUS_LEGACY,
184
+ "claude-3-opus-20240229": OPUS_LEGACY,
185
+ sonnet: SONNET,
186
+ "claude-sonnet-5": SONNET_5_INTRO,
187
+ "claude-sonnet-4-6": SONNET,
188
+ "claude-sonnet-4-5": SONNET,
189
+ haiku: HAIKU,
190
+ "claude-haiku-4-5": HAIKU,
191
+ "claude-3-5-haiku-20241022": HAIKU_35,
192
+ "claude-3-haiku-20240307": HAIKU_3
193
+ };
194
+ });
195
+
64
196
  // src/telemetry/percentiles.ts
65
197
  function computePercentiles(values) {
66
198
  if (values.length === 0)
@@ -76,7 +208,7 @@ function computePercentiles(values) {
76
208
  avg: Math.round(sum / sorted.length)
77
209
  };
78
210
  }
79
- function computeSummary(metrics, windowMs) {
211
+ function computeSummary(metrics, windowMs, pricingOverrides) {
80
212
  if (metrics.length === 0) {
81
213
  const emptyPhase = { p50: 0, p95: 0, p99: 0, min: 0, max: 0, avg: 0 };
82
214
  return {
@@ -99,7 +231,8 @@ function computeSummary(metrics, windowMs) {
99
231
  totalCacheCreationTokens: 0,
100
232
  avgCacheHitRate: 0,
101
233
  cacheMissOnResumeCount: 0
102
- }
234
+ },
235
+ costEstimate: { totalUsd: 0, byModel: {}, unpricedRequestCount: 0, byProfile: {} }
103
236
  };
104
237
  }
105
238
  const errorCount = metrics.filter((m) => m.error !== null).length;
@@ -166,10 +299,132 @@ function computeSummary(metrics, windowMs) {
166
299
  totalCacheCreationTokens,
167
300
  avgCacheHitRate: cacheHitRateCount > 0 ? Math.round(cacheHitRateSum / cacheHitRateCount * 100) / 100 : 0,
168
301
  cacheMissOnResumeCount
302
+ },
303
+ costEstimate: computeCostEstimate(metrics, pricingOverrides)
304
+ };
305
+ }
306
+ var init_percentiles = __esm(() => {
307
+ init_pricing();
308
+ });
309
+
310
+ // src/telemetry/pricingStore.ts
311
+ var exports_pricingStore = {};
312
+ __export(exports_pricingStore, {
313
+ validatePricingUpdate: () => validatePricingUpdate,
314
+ validateModelKey: () => validateModelKey,
315
+ setPricingOverride: () => setPricingOverride,
316
+ resetPricingOverridesCache: () => resetPricingOverridesCache,
317
+ getPricingOverrides: () => getPricingOverrides,
318
+ deletePricingOverride: () => deletePricingOverride
319
+ });
320
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
321
+ import { join, dirname } from "node:path";
322
+ import { homedir } from "node:os";
323
+ function getConfigPath() {
324
+ const explicit = env("PRICING_CONFIG");
325
+ if (explicit)
326
+ return explicit;
327
+ const dir = join(homedir(), ".config", "meridian");
328
+ return join(dir, "model-pricing.json");
329
+ }
330
+ function readOverrides() {
331
+ const path = getConfigPath();
332
+ const now = Date.now();
333
+ if (cachedOverrides && cachedPath === path && now - lastReadTime < CACHE_TTL_MS) {
334
+ return cachedOverrides;
335
+ }
336
+ try {
337
+ if (existsSync2(path)) {
338
+ const raw2 = JSON.parse(readFileSync(path, "utf-8"));
339
+ const result = {};
340
+ for (const [model, value] of Object.entries(raw2)) {
341
+ try {
342
+ result[normalizeModelKey(model)] = validatePricingUpdate(value);
343
+ } catch {}
344
+ }
345
+ cachedOverrides = result;
346
+ } else {
347
+ cachedOverrides = {};
348
+ }
349
+ } catch {
350
+ cachedOverrides = {};
351
+ }
352
+ cachedPath = path;
353
+ lastReadTime = now;
354
+ return cachedOverrides;
355
+ }
356
+ function writeOverrides(overrides) {
357
+ const path = getConfigPath();
358
+ const tmp = `${path}.tmp`;
359
+ try {
360
+ const dir = dirname(path);
361
+ if (!existsSync2(dir))
362
+ mkdirSync(dir, { recursive: true });
363
+ writeFileSync(tmp, JSON.stringify(overrides, null, 2));
364
+ renameSync(tmp, path);
365
+ cachedOverrides = overrides;
366
+ cachedPath = path;
367
+ lastReadTime = Date.now();
368
+ } catch (e) {
369
+ console.error("[pricing] write failed:", e.message);
370
+ }
371
+ }
372
+ function getPricingOverrides() {
373
+ return readOverrides();
374
+ }
375
+ function validatePricingUpdate(raw2) {
376
+ if (raw2 === null || typeof raw2 !== "object" || Array.isArray(raw2)) {
377
+ throw new Error("body must be a JSON object");
378
+ }
379
+ const input = raw2;
380
+ const readRate = (key, required) => {
381
+ const value = input[key];
382
+ if (value === undefined || value === null || value === "") {
383
+ if (required)
384
+ throw new Error(`${key} is required`);
385
+ return;
169
386
  }
387
+ if (typeof value !== "number" || !isFinite(value) || value < 0) {
388
+ throw new Error(`${key} must be a non-negative finite number`);
389
+ }
390
+ return value;
170
391
  };
392
+ const inputPerMTok = readRate("inputPerMTok", true);
393
+ const outputPerMTok = readRate("outputPerMTok", true);
394
+ const cacheReadPerMTok = readRate("cacheReadPerMTok", false) ?? inputPerMTok * CACHE_READ_MULTIPLIER;
395
+ const cacheWritePerMTok = readRate("cacheWritePerMTok", false) ?? inputPerMTok * CACHE_WRITE_MULTIPLIER;
396
+ return { inputPerMTok, outputPerMTok, cacheReadPerMTok, cacheWritePerMTok };
397
+ }
398
+ function validateModelKey(model) {
399
+ const normalized = normalizeModelKey(model);
400
+ if (normalized.length === 0)
401
+ throw new Error("model must be a non-empty string");
402
+ if (normalized.length > MAX_MODEL_KEY_LENGTH) {
403
+ throw new Error(`model must be at most ${MAX_MODEL_KEY_LENGTH} characters`);
404
+ }
405
+ return normalized;
171
406
  }
172
- var init_percentiles = () => {};
407
+ function setPricingOverride(model, pricing) {
408
+ const key = validateModelKey(model);
409
+ const overrides = { ...readOverrides(), [key]: pricing };
410
+ writeOverrides(overrides);
411
+ }
412
+ function deletePricingOverride(model) {
413
+ const key = validateModelKey(model);
414
+ const overrides = { ...readOverrides() };
415
+ delete overrides[key];
416
+ writeOverrides(overrides);
417
+ }
418
+ function resetPricingOverridesCache() {
419
+ cachedOverrides = null;
420
+ cachedPath = null;
421
+ lastReadTime = 0;
422
+ }
423
+ var MAX_MODEL_KEY_LENGTH = 200, cachedOverrides = null, cachedPath = null, lastReadTime = 0, CACHE_TTL_MS = 5000;
424
+ var init_pricingStore = __esm(() => {
425
+ init_env();
426
+ init_pricing();
427
+ });
173
428
 
174
429
  // node_modules/@neon-rs/load/dist/index.js
175
430
  var require_dist = __commonJS((exports) => {
@@ -272,8 +527,8 @@ var require_dist = __commonJS((exports) => {
272
527
  const header = report.header;
273
528
  return typeof header === "object" && !!header && "glibcVersionRuntime" in header;
274
529
  }
275
- function load(dirname2) {
276
- const m = path.join(dirname2, "index.node");
530
+ function load(dirname3) {
531
+ const m = path.join(dirname3, "index.node");
277
532
  return fs.existsSync(m) ? __require(m) : null;
278
533
  }
279
534
  exports.load = load;
@@ -296,7 +551,7 @@ var require_process = __commonJS((exports, module) => {
296
551
  var require_filesystem = __commonJS((exports, module) => {
297
552
  var fs = __require("fs");
298
553
  var LDD_PATH = "/usr/bin/ldd";
299
- var readFileSync2 = (path) => fs.readFileSync(path, "utf-8");
554
+ var readFileSync3 = (path) => fs.readFileSync(path, "utf-8");
300
555
  var readFile = (path) => new Promise((resolve2, reject) => {
301
556
  fs.readFile(path, "utf-8", (err, data) => {
302
557
  if (err) {
@@ -308,7 +563,7 @@ var require_filesystem = __commonJS((exports, module) => {
308
563
  });
309
564
  module.exports = {
310
565
  LDD_PATH,
311
- readFileSync: readFileSync2,
566
+ readFileSync: readFileSync3,
312
567
  readFile
313
568
  };
314
569
  });
@@ -317,7 +572,7 @@ var require_filesystem = __commonJS((exports, module) => {
317
572
  var require_detect_libc = __commonJS((exports, module) => {
318
573
  var childProcess = __require("child_process");
319
574
  var { isLinux, getReport } = require_process();
320
- var { LDD_PATH, readFile, readFileSync: readFileSync2 } = require_filesystem();
575
+ var { LDD_PATH, readFile, readFileSync: readFileSync3 } = require_filesystem();
321
576
  var cachedFamilyFilesystem;
322
577
  var cachedVersionFilesystem;
323
578
  var command = "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true";
@@ -397,7 +652,7 @@ var require_detect_libc = __commonJS((exports, module) => {
397
652
  }
398
653
  cachedFamilyFilesystem = null;
399
654
  try {
400
- const lddContent = readFileSync2(LDD_PATH);
655
+ const lddContent = readFileSync3(LDD_PATH);
401
656
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
402
657
  } catch (e) {}
403
658
  return cachedFamilyFilesystem;
@@ -452,7 +707,7 @@ var require_detect_libc = __commonJS((exports, module) => {
452
707
  }
453
708
  cachedVersionFilesystem = null;
454
709
  try {
455
- const lddContent = readFileSync2(LDD_PATH);
710
+ const lddContent = readFileSync3(LDD_PATH);
456
711
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
457
712
  if (versionMatch) {
458
713
  cachedVersionFilesystem = versionMatch[1];
@@ -910,7 +1165,7 @@ class SqliteTelemetryStore {
910
1165
  status, queue_wait_ms, proxy_overhead_ms, ttfb_ms,
911
1166
  upstream_duration_ms, total_duration_ms, content_blocks, text_events, error,
912
1167
  input_tokens, output_tokens, cache_read_input_tokens,
913
- cache_creation_input_tokens, cache_hit_rate
1168
+ cache_creation_input_tokens, cache_hit_rate, profile_id, envelope_violations
914
1169
  ) VALUES (
915
1170
  @requestId, @timestamp, @adapter, @requestSource, @model, @requestModel, @mode,
916
1171
  @isResume, @isPassthrough, @lineageType,
@@ -919,7 +1174,7 @@ class SqliteTelemetryStore {
919
1174
  @status, @queueWaitMs, @proxyOverheadMs, @ttfbMs,
920
1175
  @upstreamDurationMs, @totalDurationMs, @contentBlocks, @textEvents, @error,
921
1176
  @inputTokens, @outputTokens, @cacheReadInputTokens,
922
- @cacheCreationInputTokens, @cacheHitRate
1177
+ @cacheCreationInputTokens, @cacheHitRate, @profileId, @envelopeViolations
923
1178
  )
924
1179
  `);
925
1180
  this.countStmt = db.prepare("SELECT COUNT(*) as cnt FROM metrics");
@@ -957,7 +1212,9 @@ class SqliteTelemetryStore {
957
1212
  outputTokens: metric.outputTokens ?? null,
958
1213
  cacheReadInputTokens: metric.cacheReadInputTokens ?? null,
959
1214
  cacheCreationInputTokens: metric.cacheCreationInputTokens ?? null,
960
- cacheHitRate: metric.cacheHitRate ?? null
1215
+ cacheHitRate: metric.cacheHitRate ?? null,
1216
+ profileId: metric.profileId ?? null,
1217
+ envelopeViolations: metric.envelopeViolations && metric.envelopeViolations.length > 0 ? JSON.stringify(metric.envelopeViolations) : null
961
1218
  });
962
1219
  } catch (err) {
963
1220
  console.error("[telemetry] SQLite write failed, skipping:", err);
@@ -1006,7 +1263,7 @@ class SqliteTelemetryStore {
1006
1263
  summarize(windowMs = 60 * 60 * 1000) {
1007
1264
  const since = Date.now() - windowMs;
1008
1265
  const metrics = this.getRecent({ limit: 1e5, since });
1009
- return computeSummary(metrics, windowMs);
1266
+ return computeSummary(metrics, windowMs, getPricingOverrides());
1010
1267
  }
1011
1268
  clear() {
1012
1269
  try {
@@ -1090,6 +1347,16 @@ class SqliteDiagnosticLogStore {
1090
1347
  } catch {}
1091
1348
  }
1092
1349
  }
1350
+ function parseEnvelopeViolations(raw2) {
1351
+ if (typeof raw2 !== "string" || raw2.length === 0)
1352
+ return;
1353
+ try {
1354
+ const parsed = JSON.parse(raw2);
1355
+ return Array.isArray(parsed) ? parsed.filter((v) => typeof v === "string") : undefined;
1356
+ } catch {
1357
+ return;
1358
+ }
1359
+ }
1093
1360
  function rowToMetric(r) {
1094
1361
  return {
1095
1362
  requestId: r.request_id,
@@ -1122,7 +1389,9 @@ function rowToMetric(r) {
1122
1389
  outputTokens: r.output_tokens ?? undefined,
1123
1390
  cacheReadInputTokens: r.cache_read_input_tokens ?? undefined,
1124
1391
  cacheCreationInputTokens: r.cache_creation_input_tokens ?? undefined,
1125
- cacheHitRate: r.cache_hit_rate ?? undefined
1392
+ cacheHitRate: r.cache_hit_rate ?? undefined,
1393
+ profileId: r.profile_id ?? undefined,
1394
+ envelopeViolations: parseEnvelopeViolations(r.envelope_violations)
1126
1395
  };
1127
1396
  }
1128
1397
  function createSqliteStores(dbPath, retentionDays) {
@@ -1170,7 +1439,9 @@ CREATE TABLE IF NOT EXISTS metrics (
1170
1439
  output_tokens INTEGER,
1171
1440
  cache_read_input_tokens INTEGER,
1172
1441
  cache_creation_input_tokens INTEGER,
1173
- cache_hit_rate REAL
1442
+ cache_hit_rate REAL,
1443
+ profile_id TEXT,
1444
+ envelope_violations TEXT
1174
1445
  );
1175
1446
  CREATE INDEX IF NOT EXISTS idx_metrics_ts ON metrics(timestamp);
1176
1447
  CREATE INDEX IF NOT EXISTS idx_metrics_model ON metrics(model);
@@ -1189,9 +1460,12 @@ CREATE INDEX IF NOT EXISTS idx_logs_cat ON diagnostic_logs(category);
1189
1460
  `, CLEANUP_INTERVAL = 1000;
1190
1461
  var init_sqlite = __esm(() => {
1191
1462
  init_percentiles();
1463
+ init_pricingStore();
1192
1464
  import_libsql = __toESM(require_libsql(), 1);
1193
1465
  METRICS_MIGRATIONS = [
1194
- "ALTER TABLE metrics ADD COLUMN request_source TEXT"
1466
+ "ALTER TABLE metrics ADD COLUMN request_source TEXT",
1467
+ "ALTER TABLE metrics ADD COLUMN profile_id TEXT",
1468
+ "ALTER TABLE metrics ADD COLUMN envelope_violations TEXT"
1195
1469
  ];
1196
1470
  });
1197
1471
 
@@ -1205,40 +1479,40 @@ __export(exports_sdkFeatures, {
1205
1479
  getExplicitThinking: () => getExplicitThinking,
1206
1480
  getAllFeatureConfigs: () => getAllFeatureConfigs
1207
1481
  });
1208
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "node:fs";
1209
- import { join as join5 } from "node:path";
1210
- import { homedir as homedir4 } from "node:os";
1211
- function getConfigPath() {
1212
- const dir = join5(homedir4(), ".config", "meridian");
1213
- if (!existsSync6(dir))
1214
- mkdirSync2(dir, { recursive: true });
1215
- return join5(dir, "sdk-features.json");
1482
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "node:fs";
1483
+ import { join as join6 } from "node:path";
1484
+ import { homedir as homedir5 } from "node:os";
1485
+ function getConfigPath2() {
1486
+ const dir = join6(homedir5(), ".config", "meridian");
1487
+ if (!existsSync7(dir))
1488
+ mkdirSync3(dir, { recursive: true });
1489
+ return join6(dir, "sdk-features.json");
1216
1490
  }
1217
1491
  function readConfig() {
1218
1492
  const now = Date.now();
1219
- if (cachedConfig && now - lastReadTime < CACHE_TTL_MS)
1493
+ if (cachedConfig && now - lastReadTime2 < CACHE_TTL_MS2)
1220
1494
  return cachedConfig;
1221
- const path3 = getConfigPath();
1495
+ const path3 = getConfigPath2();
1222
1496
  try {
1223
- if (existsSync6(path3)) {
1224
- cachedConfig = JSON.parse(readFileSync5(path3, "utf-8"));
1497
+ if (existsSync7(path3)) {
1498
+ cachedConfig = JSON.parse(readFileSync6(path3, "utf-8"));
1225
1499
  } else {
1226
1500
  cachedConfig = {};
1227
1501
  }
1228
1502
  } catch {
1229
1503
  cachedConfig = {};
1230
1504
  }
1231
- lastReadTime = now;
1505
+ lastReadTime2 = now;
1232
1506
  return cachedConfig;
1233
1507
  }
1234
1508
  function writeConfig(config) {
1235
- const path3 = getConfigPath();
1509
+ const path3 = getConfigPath2();
1236
1510
  const tmp = `${path3}.tmp`;
1237
1511
  try {
1238
- writeFileSync2(tmp, JSON.stringify(config, null, 2));
1239
- renameSync2(tmp, path3);
1512
+ writeFileSync3(tmp, JSON.stringify(config, null, 2));
1513
+ renameSync3(tmp, path3);
1240
1514
  cachedConfig = config;
1241
- lastReadTime = Date.now();
1515
+ lastReadTime2 = Date.now();
1242
1516
  } catch (e) {
1243
1517
  console.error("[sdk-features] write failed:", e.message);
1244
1518
  }
@@ -1311,7 +1585,7 @@ function resetAdapterFeatures(adapterName) {
1311
1585
  delete config[adapterName];
1312
1586
  writeConfig(config);
1313
1587
  }
1314
- var DEFAULT_FEATURES, ADAPTER_DEFAULTS, cachedConfig = null, lastReadTime = 0, CACHE_TTL_MS = 5000, VALID_CLAUDE_MD_VALUES, VALID_THINKING_VALUES;
1588
+ var DEFAULT_FEATURES, ADAPTER_DEFAULTS, cachedConfig = null, lastReadTime2 = 0, CACHE_TTL_MS2 = 5000, VALID_CLAUDE_MD_VALUES, VALID_THINKING_VALUES;
1315
1589
  var init_sdkFeatures = __esm(() => {
1316
1590
  DEFAULT_FEATURES = {
1317
1591
  codeSystemPrompt: true,
@@ -1361,7 +1635,7 @@ var init_settingsPage = __esm(() => {
1361
1635
  ${themeCss}
1362
1636
  * { box-sizing: border-box; margin: 0; padding: 0; }
1363
1637
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1364
- background: var(--bg); color: var(--text); padding: 0; line-height: 1.5; }
1638
+ color: var(--text); padding: 0; line-height: 1.5; }
1365
1639
  ${profileBarCss}
1366
1640
  .content { max-width: 900px; margin: 0 auto; padding: 24px; }
1367
1641
  h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
@@ -1435,6 +1709,28 @@ var init_settingsPage = __esm(() => {
1435
1709
  border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;
1436
1710
  }
1437
1711
  .reset-btn:hover { border-color: var(--red); color: var(--red); }
1712
+
1713
+ /* Model pricing */
1714
+ .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }
1715
+ .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;
1716
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
1717
+ .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
1718
+ .pricing-table tr:last-child td { border-bottom: none; }
1719
+ .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }
1720
+ .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);
1721
+ border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;
1722
+ font-variant-numeric: tabular-nums; }
1723
+ .pricing-input:focus { border-color: var(--accent); outline: none; }
1724
+ .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;
1725
+ text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
1726
+ .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }
1727
+ .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }
1728
+ .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;
1729
+ padding-top: 14px; border-top: 1px solid var(--border); }
1730
+ .pricing-add input[type="text"] { width: 240px; text-align: left; }
1731
+ .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;
1732
+ padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }
1733
+ .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }
1438
1734
  </style>
1439
1735
  </head>
1440
1736
  <body>
@@ -1451,6 +1747,32 @@ ${profileBarHtml}
1451
1747
  </p>
1452
1748
 
1453
1749
  <div id="adapters"></div>
1750
+
1751
+ <h1 style="margin-top:40px">Model Pricing</h1>
1752
+ <p class="subtitle" style="max-width:720px;line-height:1.6">
1753
+ Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the
1754
+ built-in rate, or add models the built-in table doesn't know about (they show as "no pricing" on the
1755
+ dashboard until defined here). Changes apply on the next dashboard refresh.
1756
+ </p>
1757
+ <div class="adapter-card">
1758
+ <table class="pricing-table">
1759
+ <thead><tr><th>Model</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Source</th><th></th></tr></thead>
1760
+ <tbody id="pricingRows"></tbody>
1761
+ </table>
1762
+ <div class="pricing-add">
1763
+ <input type="text" class="pricing-input" id="newModelName" placeholder="model id (e.g. claude-opus-9)">
1764
+ <input type="number" class="pricing-input" id="newModelInput" placeholder="input" min="0" step="0.01">
1765
+ <input type="number" class="pricing-input" id="newModelOutput" placeholder="output" min="0" step="0.01">
1766
+ <input type="number" class="pricing-input" id="newModelCacheRead" placeholder="cache read" min="0" step="0.01">
1767
+ <input type="number" class="pricing-input" id="newModelCacheWrite" placeholder="cache write" min="0" step="0.01">
1768
+ <button class="add-btn" onclick="addPricingModel()">Add Model</button>
1769
+ </div>
1770
+ <div class="pricing-note">
1771
+ Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the
1772
+ input rate (the 5-minute cache TTL multipliers). Verify current list prices at
1773
+ <a href="https://claude.com/pricing" target="_blank" rel="noreferrer" style="color:var(--accent)">claude.com/pricing</a>.
1774
+ </div>
1775
+ </div>
1454
1776
  </div>
1455
1777
 
1456
1778
  <div class="save-indicator" id="saveIndicator">Saved</div>
@@ -1585,7 +1907,127 @@ function render() {
1585
1907
  }
1586
1908
  }
1587
1909
 
1910
+ // ---- Model pricing (telemetry cost estimate) ----
1911
+ let pricingData = { builtin: {}, overrides: {} };
1912
+
1913
+ function fmtRate(v) { return String(Math.round(v * 10000) / 10000); }
1914
+
1915
+ async function loadPricing() {
1916
+ const res = await fetch('/settings/api/pricing');
1917
+ pricingData = await res.json();
1918
+ renderPricing();
1919
+ }
1920
+
1921
+ async function putPricing(model, rates) {
1922
+ const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {
1923
+ method: 'PUT',
1924
+ headers: { 'Content-Type': 'application/json' },
1925
+ body: JSON.stringify(rates),
1926
+ });
1927
+ if (!res.ok) {
1928
+ const err = await res.json().catch(function () { return {}; });
1929
+ alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));
1930
+ return false;
1931
+ }
1932
+ showSaved();
1933
+ return true;
1934
+ }
1935
+
1936
+ async function removePricing(model) {
1937
+ await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });
1938
+ showSaved();
1939
+ await loadPricing();
1940
+ }
1941
+
1942
+ function rateCell(value, onCommit) {
1943
+ const td = document.createElement('td');
1944
+ const input = document.createElement('input');
1945
+ input.type = 'number';
1946
+ input.min = '0';
1947
+ input.step = '0.01';
1948
+ input.className = 'pricing-input';
1949
+ input.value = fmtRate(value);
1950
+ input.addEventListener('change', onCommit);
1951
+ td.appendChild(input);
1952
+ return { td: td, input: input };
1953
+ }
1954
+
1955
+ function renderPricing() {
1956
+ const tbody = document.getElementById('pricingRows');
1957
+ tbody.innerHTML = '';
1958
+ const models = Array.from(new Set(
1959
+ Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))
1960
+ )).sort();
1961
+
1962
+ for (const model of models) {
1963
+ const override = pricingData.overrides[model];
1964
+ const effective = override || pricingData.builtin[model];
1965
+ const tr = document.createElement('tr');
1966
+
1967
+ const nameTd = document.createElement('td');
1968
+ nameTd.className = 'pricing-model';
1969
+ nameTd.textContent = model;
1970
+ tr.appendChild(nameTd);
1971
+
1972
+ const cells = [];
1973
+ const commit = async function () {
1974
+ const rates = {
1975
+ inputPerMTok: parseFloat(cells[0].input.value),
1976
+ outputPerMTok: parseFloat(cells[1].input.value),
1977
+ cacheReadPerMTok: parseFloat(cells[2].input.value),
1978
+ cacheWritePerMTok: parseFloat(cells[3].input.value),
1979
+ };
1980
+ for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }
1981
+ if (await putPricing(model, rates)) await loadPricing();
1982
+ };
1983
+ ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {
1984
+ const cell = rateCell(effective[key], commit);
1985
+ cells.push(cell);
1986
+ tr.appendChild(cell.td);
1987
+ });
1988
+
1989
+ const badgeTd = document.createElement('td');
1990
+ const badge = document.createElement('span');
1991
+ badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');
1992
+ badge.textContent = override ? 'Override' : 'Built-in';
1993
+ badgeTd.appendChild(badge);
1994
+ tr.appendChild(badgeTd);
1995
+
1996
+ const actionTd = document.createElement('td');
1997
+ if (override) {
1998
+ const btn = document.createElement('button');
1999
+ btn.className = 'reset-btn';
2000
+ btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';
2001
+ btn.addEventListener('click', function () { removePricing(model); });
2002
+ actionTd.appendChild(btn);
2003
+ }
2004
+ tr.appendChild(actionTd);
2005
+
2006
+ tbody.appendChild(tr);
2007
+ }
2008
+ }
2009
+
2010
+ async function addPricingModel() {
2011
+ const name = document.getElementById('newModelName').value.trim();
2012
+ const inputRate = parseFloat(document.getElementById('newModelInput').value);
2013
+ const outputRate = parseFloat(document.getElementById('newModelOutput').value);
2014
+ if (!name) { alert('Enter a model id'); return; }
2015
+ if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }
2016
+ const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };
2017
+ const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);
2018
+ const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);
2019
+ if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;
2020
+ if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;
2021
+ if (await putPricing(name, rates)) {
2022
+ ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {
2023
+ document.getElementById(id).value = '';
2024
+ });
2025
+ await loadPricing();
2026
+ }
2027
+ }
2028
+
1588
2029
  loadConfig();
2030
+ loadPricing();
1589
2031
  ${profileBarJs}
1590
2032
  </script>
1591
2033
  </body>
@@ -3745,8 +4187,8 @@ var serve = (options, listeningListener) => {
3745
4187
  };
3746
4188
 
3747
4189
  // src/proxy/server.ts
3748
- import { homedir as homedir5 } from "node:os";
3749
- import { join as join6 } from "node:path";
4190
+ import { homedir as homedir6 } from "node:os";
4191
+ import { join as join7 } from "node:path";
3750
4192
  import { query } from "@anthropic-ai/claude-agent-sdk";
3751
4193
 
3752
4194
  // src/proxy/rateLimitStore.ts
@@ -3883,6 +4325,16 @@ function normalizeUtilization(raw2) {
3883
4325
  return null;
3884
4326
  return Math.max(0, raw2 / 100);
3885
4327
  }
4328
+ function modelScopedWindowType(limit) {
4329
+ if (limit.kind !== "weekly_scoped")
4330
+ return null;
4331
+ const model = limit.scope?.model;
4332
+ const name = model?.display_name?.trim() || model?.id?.trim();
4333
+ if (!name)
4334
+ return null;
4335
+ const slug = name.toLowerCase().replace(/^claude[\s_-]+/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
4336
+ return slug ? `seven_day_${slug}` : null;
4337
+ }
3886
4338
  function buildSnapshot(raw2) {
3887
4339
  const windows = [];
3888
4340
  for (const key of WINDOW_TYPES) {
@@ -3895,6 +4347,18 @@ function buildSnapshot(raw2) {
3895
4347
  continue;
3896
4348
  windows.push({ type: key, utilization, resetsAt });
3897
4349
  }
4350
+ const windowTypes = new Set(windows.map((window) => window.type));
4351
+ for (const limit of raw2.limits ?? []) {
4352
+ const type = modelScopedWindowType(limit);
4353
+ if (!type || windowTypes.has(type))
4354
+ continue;
4355
+ const utilization = normalizeUtilization(limit.percent);
4356
+ const resetsAt = parseIsoToMs(limit.resets_at);
4357
+ if (utilization === null && resetsAt === null)
4358
+ continue;
4359
+ windows.push({ type, utilization, resetsAt });
4360
+ windowTypes.add(type);
4361
+ }
3898
4362
  const extra = raw2.extra_usage;
3899
4363
  const extraUsage = extra ? {
3900
4364
  isEnabled: !!extra.is_enabled,
@@ -4001,6 +4465,9 @@ var DEFAULT_PROXY_CONFIG = {
4001
4465
  version: undefined
4002
4466
  };
4003
4467
 
4468
+ // src/proxy/server.ts
4469
+ init_env();
4470
+
4004
4471
  // src/proxy/transform.ts
4005
4472
  function runTransformHook(transforms, hook, ctx, adapterName) {
4006
4473
  return transforms.reduce((acc, transform) => {
@@ -8602,11 +9069,13 @@ class LRUMap {
8602
9069
  }
8603
9070
 
8604
9071
  // src/telemetry/index.ts
8605
- import { join } from "node:path";
8606
- import { homedir } from "node:os";
9072
+ init_env();
9073
+ import { join as join2 } from "node:path";
9074
+ import { homedir as homedir2 } from "node:os";
8607
9075
 
8608
9076
  // src/telemetry/store.ts
8609
9077
  init_percentiles();
9078
+ init_pricingStore();
8610
9079
  var DEFAULT_CAPACITY = 1000;
8611
9080
  function getCapacity() {
8612
9081
  const raw2 = process.env.MERIDIAN_TELEMETRY_SIZE ?? process.env.CLAUDE_PROXY_TELEMETRY_SIZE;
@@ -8665,7 +9134,7 @@ class MemoryTelemetryStore {
8665
9134
  summarize(windowMs = 60 * 60 * 1000) {
8666
9135
  const since = Date.now() - windowMs;
8667
9136
  const metrics = this.getRecent({ limit: this.capacity, since });
8668
- return computeSummary(metrics, windowMs);
9137
+ return computeSummary(metrics, windowMs, getPricingOverrides());
8669
9138
  }
8670
9139
  clear() {
8671
9140
  this.buffer = new Array(this.capacity).fill(null);
@@ -8726,8 +9195,8 @@ class MemoryDiagnosticLogStore {
8726
9195
  }
8727
9196
  var diagnosticLog = new MemoryDiagnosticLogStore;
8728
9197
  // src/telemetry/routes.ts
8729
- import { existsSync as existsSync2, readFileSync } from "node:fs";
8730
- import { resolve, dirname } from "node:path";
9198
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
9199
+ import { resolve, dirname as dirname2 } from "node:path";
8731
9200
  import { fileURLToPath } from "node:url";
8732
9201
 
8733
9202
  // src/telemetry/dashboard.ts
@@ -8744,7 +9213,7 @@ var dashboardHtml = `<!DOCTYPE html>
8744
9213
  :root { --total: var(--accent); }
8745
9214
  * { box-sizing: border-box; margin: 0; padding: 0; }
8746
9215
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
8747
- background: var(--bg); color: var(--text); padding: 0; line-height: 1.5; }
9216
+ color: var(--text); padding: 0; line-height: 1.5; }
8748
9217
  h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
8749
9218
  .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }
8750
9219
  .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
@@ -8830,8 +9299,8 @@ var dashboardHtml = `<!DOCTYPE html>
8830
9299
  <body>
8831
9300
  ` + profileBarHtml + `
8832
9301
  <div style="padding:24px">
8833
- <h1>Meridian</h1>
8834
- <div class="subtitle">Request Performance Telemetry</div>
9302
+ <h1>Telemetry</h1>
9303
+ <div class="subtitle">Request performance, cost, and wire-contract integrity</div>
8835
9304
 
8836
9305
  <div class="refresh-bar">
8837
9306
  <select id="window">
@@ -8867,6 +9336,26 @@ function ago(ts) {
8867
9336
  return Math.floor(s/3600) + 'h ago';
8868
9337
  }
8869
9338
 
9339
+ function fmtTok(n) {
9340
+ return n > 1000000 ? (n/1000000).toFixed(1) + 'M' : n > 1000 ? Math.round(n/1000) + 'k' : String(n);
9341
+ }
9342
+
9343
+ // Model names come from client-supplied request bodies (requestModel) — escape
9344
+ // before concatenating into innerHTML so a quirky/malicious client can't
9345
+ // script the dashboard.
9346
+ function esc(s) {
9347
+ return String(s).replace(/[&<>"']/g, function (ch) {
9348
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch];
9349
+ });
9350
+ }
9351
+
9352
+ function usd(v) {
9353
+ if (v == null) return '—';
9354
+ if (v > 0 && v < 0.01) return '$' + v.toFixed(4);
9355
+ if (v < 100) return '$' + v.toFixed(2);
9356
+ return '$' + Math.round(v).toLocaleString();
9357
+ }
9358
+
8870
9359
  function pctRow(label, color, phase) {
8871
9360
  return '<tr>'
8872
9361
  + '<td><span class="phase-dot" style="background:' + color + '"></span>' + label + '</td>'
@@ -8949,7 +9438,6 @@ function render(s, reqs, logs, quota) {
8949
9438
  // Token usage cards
8950
9439
  if (s.tokenUsage) {
8951
9440
  const t = s.tokenUsage;
8952
- const fmtTok = n => n > 1000000 ? (n/1000000).toFixed(1) + 'M' : n > 1000 ? Math.round(n/1000) + 'k' : String(n);
8953
9441
  html += '<div class="section"><div class="section-title">Token Usage</div></div>';
8954
9442
  html += '<div class="cards">'
8955
9443
  + card('Input Tokens', fmtTok(t.totalInputTokens), '')
@@ -8960,12 +9448,51 @@ function render(s, reqs, logs, quota) {
8960
9448
  + '</div>';
8961
9449
  }
8962
9450
 
9451
+ // Estimated cost: static API list pricing applied to the window's token usage
9452
+ if (s.costEstimate && Object.keys(s.costEstimate.byModel).length > 0) {
9453
+ const ce = s.costEstimate;
9454
+ const costRows = Object.entries(ce.byModel)
9455
+ .sort((a, b) => (b[1].estimatedUsd || 0) - (a[1].estimatedUsd || 0));
9456
+
9457
+ html += '<div class="section"><div class="section-title">Estimated Cost</div></div>';
9458
+ html += '<div class="cards">'
9459
+ + card('Est. API Cost', usd(ce.totalUsd), 'window total at API list prices');
9460
+ for (const [model, m] of costRows) {
9461
+ html += card(esc(model), usd(m.estimatedUsd), m.requests + ' req' + (m.requests === 1 ? '' : 's'));
9462
+ }
9463
+ html += '</div>';
9464
+
9465
+ html += '<div class="section">'
9466
+ + '<table><thead><tr><th>Model</th><th>Requests</th><th>Input</th><th>Output</th>'
9467
+ + '<th>Cache Read</th><th>Cache Write</th><th>Est. Cost</th></tr></thead><tbody>';
9468
+ for (const [model, m] of costRows) {
9469
+ html += '<tr>'
9470
+ + '<td>' + esc(model) + (m.estimatedUsd == null ? ' <span style="font-size:10px;color:var(--yellow)">no pricing</span>' : '') + '</td>'
9471
+ + '<td class="mono">' + m.requests + '</td>'
9472
+ + '<td class="mono">' + fmtTok(m.inputTokens) + '</td>'
9473
+ + '<td class="mono">' + fmtTok(m.outputTokens) + '</td>'
9474
+ + '<td class="mono">' + fmtTok(m.cacheReadTokens) + '</td>'
9475
+ + '<td class="mono">' + fmtTok(m.cacheCreationTokens) + '</td>'
9476
+ + '<td class="mono">' + usd(m.estimatedUsd) + '</td>'
9477
+ + '</tr>';
9478
+ }
9479
+ html += '</tbody></table>'
9480
+ + '<div class="usage-note" style="margin-top:8px">Estimated at static Anthropic API list prices'
9481
+ + ' (cache writes at the 5-minute TTL rate). Claude Max usage is covered by your subscription'
9482
+ + ' (equivalent API cost, not a charge).'
9483
+ + (ce.unpricedRequestCount > 0
9484
+ ? ' ' + ce.unpricedRequestCount + ' request' + (ce.unpricedRequestCount === 1 ? '' : 's') + ' from unrecognized models excluded.'
9485
+ : '')
9486
+ + ' Rates are editable in <a href="/settings" style="color:var(--accent)">Settings</a>.'
9487
+ + '</div></div>';
9488
+ }
9489
+
8963
9490
  // Model breakdown
8964
9491
  const models = Object.entries(s.byModel);
8965
9492
  if (models.length > 0) {
8966
9493
  html += '<div class="cards">';
8967
9494
  for (const [name, data] of models) {
8968
- html += card(name, data.count + ' reqs', 'avg ' + ms(data.avgTotalMs));
9495
+ html += card(esc(name), data.count + ' reqs', 'avg ' + ms(data.avgTotalMs));
8969
9496
  }
8970
9497
  html += '</div>';
8971
9498
  }
@@ -9205,8 +9732,8 @@ timer = setInterval(refresh, 5000);
9205
9732
  </html>`;
9206
9733
 
9207
9734
  // src/telemetry/routes.ts
9208
- var _iconPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "icon.svg");
9209
- var _iconSvg = existsSync2(_iconPath) ? readFileSync(_iconPath, "utf-8") : null;
9735
+ var _iconPath = resolve(dirname2(fileURLToPath(import.meta.url)), "..", "..", "assets", "icon.svg");
9736
+ var _iconSvg = existsSync3(_iconPath) ? readFileSync2(_iconPath, "utf-8") : null;
9210
9737
  function createTelemetryRoutes() {
9211
9738
  const routes = new Hono2;
9212
9739
  routes.get("/", (c) => {
@@ -9261,108 +9788,202 @@ var landingHtml = `<!DOCTYPE html>
9261
9788
  ${themeCss}
9262
9789
  * { box-sizing: border-box; margin: 0; padding: 0; }
9263
9790
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
9264
- background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
9265
- .container { max-width: 960px; margin: 0 auto; padding: 32px 24px; }
9266
-
9267
- .header { display: flex; align-items: center; gap: 16px; margin-bottom: 6px; }
9268
- .header h1 { font-size: 28px; font-weight: 700; letter-spacing: 3px; }
9269
- .tagline { color: var(--muted); font-size: 14px; margin-bottom: 32px; letter-spacing: 0.5px; }
9270
-
9271
- .status-banner { display: flex; align-items: center; gap: 12px; padding: 16px 20px;
9272
- background: var(--surface); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 24px; }
9273
- .status-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
9274
- .status-dot.healthy { background: var(--green); box-shadow: 0 0 8px rgba(63,185,80,0.4); }
9275
- .status-dot.degraded { background: var(--yellow); }
9276
- .status-dot.unhealthy { background: var(--red); }
9277
- .status-text { font-size: 14px; font-weight: 500; }
9278
- .status-detail { font-size: 12px; color: var(--muted); margin-left: auto; }
9279
-
9280
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
9281
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
9282
- .card-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; font-weight: 500; }
9283
- .card-value { font-size: 32px; font-weight: 700; margin-top: 4px; font-variant-numeric: tabular-nums; }
9284
- .card-value.green { color: var(--green); }
9285
- .card-value.violet { color: var(--violet); }
9286
- .card-detail { font-size: 12px; color: var(--muted); margin-top: 4px; }
9791
+ color: var(--text); line-height: 1.6; min-height: 100vh; }
9792
+ .container { max-width: 960px; margin: 0 auto; padding: 28px 24px; }
9793
+
9794
+ /* Intro friendly one-paragraph overview of how Meridian works */
9795
+ .intro { margin-bottom: 28px; }
9796
+ .intro h2 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
9797
+ .intro p { font-size: 13px; color: var(--muted); max-width: 640px; }
9798
+ .intro code { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px;
9799
+ background: var(--surface); border: 1px solid var(--border); border-radius: 5px;
9800
+ padding: 1px 6px; color: var(--accent2); white-space: nowrap; }
9801
+ .intro a { color: var(--accent); text-decoration: none; }
9802
+ .intro a:hover { text-decoration: underline; }
9803
+ .intro-meta { font-size: 12px; color: var(--muted); margin-top: 8px; }
9804
+
9805
+ /* Profile cards the centerpiece: usage + cost per account, click to switch */
9806
+ .profile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 24px; }
9807
+ .profile-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
9808
+ padding: 18px 20px; position: relative; transition: border-color 0.15s; }
9809
+ .profile-card.switchable { cursor: pointer; }
9810
+ .profile-card.switchable:hover { border-color: var(--accent); }
9811
+ .profile-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
9812
+ .profile-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 4px; }
9813
+ .profile-name { font-size: 13px; font-weight: 600; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; }
9814
+ .profile-name .prof-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); }
9815
+ .profile-card.active .prof-dot { background: var(--accent); box-shadow: 0 0 6px rgba(88,166,255,0.5); }
9816
+ .active-pill { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
9817
+ color: var(--accent); background: rgba(88,166,255,0.12); border: 1px solid rgba(88,166,255,0.35);
9818
+ border-radius: 10px; padding: 1px 8px; }
9819
+ .switch-hint { font-size: 9px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;
9820
+ color: var(--muted); opacity: 0; transition: opacity 0.15s; }
9821
+ .profile-card.switchable:hover .switch-hint { opacity: 1; }
9822
+ .profile-cost { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
9823
+ .profile-sub { font-size: 11px; color: var(--muted); text-align: right; margin-bottom: 12px; }
9824
+ .usage-row { display: flex; align-items: center; gap: 10px; font-size: 12px; padding: 4px 0; }
9825
+ .usage-row .w-label { color: var(--muted); width: 64px; flex-shrink: 0; }
9826
+ .usage-row .w-bar { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
9827
+ .usage-row .w-fill { height: 100%; border-radius: 3px; }
9828
+ .usage-row .w-pct { width: 38px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
9829
+ .usage-row .w-reset { color: var(--muted); font-size: 11px; width: 76px; text-align: right; }
9830
+ .no-usage { font-size: 12px; color: var(--muted); padding: 4px 0; }
9831
+
9832
+ /* Traffic strip — one compact surface */
9833
+ .strip { display: flex; flex-wrap: wrap; background: var(--surface); border: 1px solid var(--border);
9834
+ border-radius: 12px; padding: 14px 4px; margin-bottom: 24px; }
9835
+ .strip-item { flex: 1; min-width: 120px; padding: 2px 18px; border-right: 1px solid var(--border); }
9836
+ .strip-item:last-child { border-right: none; }
9837
+ .strip-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
9838
+ .strip-value { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 2px; }
9839
+ .strip-value.green { color: var(--green); }
9840
+ .strip-value.red { color: var(--red); }
9841
+ .strip-detail { font-size: 11px; color: var(--muted); }
9287
9842
 
9288
9843
  .section { margin-bottom: 24px; }
9289
9844
  .section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase;
9290
9845
  letter-spacing: 1px; margin-bottom: 12px; }
9291
- .info-grid { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; font-size: 13px;
9292
- background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; }
9293
- .info-label { color: var(--muted); }
9294
- .info-value { color: var(--text); font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; }
9295
-
9296
- .snippet { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
9297
- padding: 16px 20px; margin-top: 12px; }
9298
- .snippet code { display: block; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;
9299
- font-size: 12px; color: var(--lavender); line-height: 1.8; white-space: pre-wrap; word-break: break-all; }
9300
- .snippet-tabs { display: flex; gap: 0; margin-bottom: 12px; }
9301
- .snippet-tab { padding: 6px 14px; font-size: 11px; font-weight: 500; cursor: pointer;
9302
- color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-bottom: none; }
9303
- .snippet-tab:first-child { border-radius: 8px 0 0 0; }
9304
- .snippet-tab:last-child { border-radius: 0 8px 0 0; }
9305
- .snippet-tab.active { color: var(--violet); background: var(--surface2); border-color: var(--accent); }
9306
-
9307
- .links { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; }
9308
- .link { padding: 10px 20px; background: var(--surface2); border: 1px solid var(--border);
9309
- border-radius: 8px; color: var(--violet); text-decoration: none; font-size: 13px; font-weight: 500;
9310
- transition: border-color 0.2s; }
9311
- .link:hover { border-color: var(--accent); }
9312
9846
 
9313
9847
  .footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border);
9314
9848
  font-size: 11px; color: var(--muted); text-align: center; }
9315
- .footer a { color: var(--violet); text-decoration: none; }
9849
+ .footer a { color: var(--accent); text-decoration: none; }
9316
9850
  ` + profileBarCss + `
9317
9851
  </style>
9318
9852
  </head>
9319
9853
  <body>
9320
9854
  ` + profileBarHtml + `
9321
9855
  <div class="container">
9322
- <div class="header">
9323
- <svg width="40" height="40" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
9324
- <rect width="64" height="64" rx="14" fill="#1C1830"/>
9325
- <line x1="32" y1="10" x2="32" y2="54" stroke="#8B7CF6" stroke-width="2.5" stroke-linecap="round"/>
9326
- <path d="M16 20 A18 18 0 0 1 48 20" fill="none" stroke="#C4B5FD" stroke-width="1.2" opacity="0.4"/>
9327
- <path d="M16 44 A18 18 0 0 0 48 44" fill="none" stroke="#C4B5FD" stroke-width="1.2" opacity="0.4"/>
9328
- <path d="M20 30 A14 14 0 0 1 44 30" fill="none" stroke="#C4B5FD" stroke-width="0.8" opacity="0.2"/>
9329
- <path d="M20 34 A14 14 0 0 0 44 34" fill="none" stroke="#C4B5FD" stroke-width="0.8" opacity="0.2"/>
9330
- <circle cx="32" cy="10" r="3.5" fill="#C4B5FD"/><circle cx="32" cy="54" r="3.5" fill="#C4B5FD"/>
9331
- <circle cx="32" cy="32" r="3" fill="#8B7CF6"/>
9332
- </svg>
9333
- <h1>MERIDIAN</h1>
9334
- </div>
9335
- <div class="tagline">Harness Claude, your way.</div>
9336
9856
  <div id="content"><div style="color:var(--muted);padding:40px;text-align:center">Loading…</div></div>
9337
9857
  </div>
9338
9858
  <script>
9339
9859
  function ms(v){if(v==null||v===0)return '—';return v<1000?v+'ms':(v/1000).toFixed(1)+'s'}
9340
- function card(l,v,d,c){return '<div class="card"><div class="card-label">'+l+'</div><div class="card-value '+(c||'')+'">'+v+'</div>'+(d?'<div class="card-detail">'+d+'</div>':'')+'</div>'}
9860
+ function esc(s){return String(s).replace(/[&<>"']/g,function(ch){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]})}
9861
+ function usd(v){if(v==null)return '—';if(v>0&&v<0.01)return '$'+v.toFixed(4);if(v<100)return '$'+v.toFixed(2);return '$'+Math.round(v).toLocaleString()}
9862
+
9863
+ var WIN_LABELS={five_hour:'5h',seven_day:'7d',seven_day_opus:'7d Opus',seven_day_sonnet:'7d Sonnet',seven_day_fable:'7d Fable',seven_day_oauth_apps:'7d Apps',seven_day_cowork:'7d Cowork',seven_day_omelette:'7d Omelette'};
9864
+ function winLabel(t){if(WIN_LABELS[t])return WIN_LABELS[t];return t.replace(/^seven_day_/,'7d ').replace(/_/g,' ').replace(/\bw/g,function(c){return c.toUpperCase()})}
9865
+ function utilColor(u){return u>=0.85?'var(--red)':u>=0.6?'var(--yellow)':'var(--green)'}
9866
+ function resetIn(ts){if(ts==null)return '';var d=ts-Date.now();if(d<=0)return 'resetting…';var m=Math.ceil(d/60000);if(m<60)return 'in '+m+'m';var h=Math.floor(m/60);if(h<24)return 'in '+h+'h'+(m%60?' '+(m%60)+'m':'');var days=Math.floor(h/24);return 'in '+days+'d'+(h%24?' '+(h%24)+'h':'')}
9867
+
9868
+ function introSection(h){
9869
+ var meta=[];
9870
+ if(h.auth&&h.auth.loggedIn)meta.push(esc(h.auth.email||'')+(h.auth.subscriptionType?' ('+esc(h.auth.subscriptionType)+')':''));
9871
+ meta.push(h.mode||'internal');
9872
+ meta.push('port '+location.port);
9873
+ return '<div class="intro">'
9874
+ +'<h2>Harness Claude, your way.</h2>'
9875
+ +'<p>Meridian bridges any Anthropic-API agent to your Claude subscription — point the agent’s <code>ANTHROPIC_BASE_URL</code> at <code>http://'+esc(location.host)+'</code> and every request routes through the active account below. Setup guides for each agent live in the <a href="https://github.com/rynfar/meridian#readme">README</a>.</p>'
9876
+ +'<div class="intro-meta">'+meta.join(' · ')+'</div>'
9877
+ +'</div>';
9878
+ }
9879
+
9880
+ function profileSection(q,s,pl,h){
9881
+ var byProfile=(s&&s.costEstimate&&s.costEstimate.byProfile)||{};
9882
+ var quotaByProfile={};
9883
+ if(q&&Array.isArray(q.profiles))for(var i=0;i<q.profiles.length;i++){var qid=q.profiles[i].id||q.profiles[i].profile||'default';quotaByProfile[qid]=q.profiles[i].windows||[]}
9884
+ var profs=[];var seen={};
9885
+ var configured=(pl&&Array.isArray(pl.profiles))?pl.profiles:[];
9886
+ var multi=configured.length>1;
9887
+ if(configured.length>0){
9888
+ // Real profiles exist: show exactly those. Traffic that predates
9889
+ // per-profile attribution (the synthetic "default" bucket) still
9890
+ // counts in the totals strip but doesn't render as a fake account.
9891
+ for(var i=0;i<configured.length;i++){var p=configured[i];profs.push({id:p.id,label:p.id,type:p.type,isActive:!!p.isActive,configured:true});seen[p.id]=1}
9892
+ }else{
9893
+ // Single-account setup: one card, labeled with the logged-in email.
9894
+ var email=(h&&h.auth&&h.auth.loggedIn&&h.auth.email)||'';
9895
+ for(var k in quotaByProfile){profs.push({id:k,label:k==='default'?(email||'account'):k,configured:false});seen[k]=1}
9896
+ for(var k in byProfile){if(!seen[k])profs.push({id:k,label:k==='default'?(email||'account'):k,configured:false});seen[k]=1}
9897
+ }
9898
+ if(profs.length===0)return '';
9899
+ var cards='';
9900
+ for(var i=0;i<profs.length;i++){
9901
+ var p=profs[i];var cost=byProfile[p.id];
9902
+ var wins=(quotaByProfile[p.id]||[]).filter(function(w){return w.utilization!=null});
9903
+ if(!p.configured&&wins.length===0&&!cost)continue;
9904
+ var rows='';
9905
+ for(var j=0;j<wins.length;j++){
9906
+ var w=wins[j];var pct=Math.round(w.utilization*100);
9907
+ rows+='<div class="usage-row"><span class="w-label">'+esc(winLabel(w.type))+'</span>'
9908
+ +'<div class="w-bar"><div class="w-fill" style="width:'+Math.min(pct,100)+'%;background:'+utilColor(w.utilization)+'"></div></div>'
9909
+ +'<span class="w-pct" style="color:'+utilColor(w.utilization)+'">'+pct+'%</span>'
9910
+ +'<span class="w-reset">'+resetIn(w.resetsAt)+'</span></div>';
9911
+ }
9912
+ if(!rows)rows='<div class="no-usage">no usage data yet</div>';
9913
+ var switchable=multi&&p.configured&&!p.isActive;
9914
+ var badge=p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
9915
+ cards+='<div class="profile-card'+(p.isActive?' active':'')+(switchable?' switchable':'')+'"'+(switchable?' data-profile="'+esc(p.id)+'" role="button" tabindex="0"':'')+'>'
9916
+ +'<div class="profile-head"><span class="profile-name"><span class="prof-dot"></span>'+esc(p.label||p.id)+' '+badge+'</span>'
9917
+ +'<span class="profile-cost">'+usd(cost?cost.estimatedUsd:0)+'</span></div>'
9918
+ +'<div class="profile-sub">'+(cost?cost.requests+' request'+(cost.requests===1?'':'s')+' · est. API value · 24h':'no traffic · 24h')+'</div>'
9919
+ +rows+'</div>';
9920
+ }
9921
+ if(!cards)return '';
9922
+ return '<div class="section"><div class="section-title">'+(profs.length===1?'Account':'Accounts')+'</div><div class="profile-grid">'+cards+'</div></div>';
9923
+ }
9924
+
9925
+ function strip(items){
9926
+ var o='<div class="strip">';
9927
+ for(var i=0;i<items.length;i++){var it=items[i];
9928
+ o+='<div class="strip-item"><div class="strip-label">'+it[0]+'</div><div class="strip-value '+(it[2]||'')+'">'+it[1]+'</div>'+(it[3]?'<div class="strip-detail">'+it[3]+'</div>':'')+'</div>';
9929
+ }
9930
+ return o+'</div>';
9931
+ }
9341
9932
 
9342
9933
  async function refresh(){
9343
9934
  try{
9344
- const [health,stats]=await Promise.all([fetch('/health').then(r=>r.json()),fetch('/telemetry/summary?window=86400000').then(r=>r.json())]);
9345
- render(health,stats);
9935
+ const [health,stats,quota,profiles]=await Promise.all([
9936
+ fetch('/health').then(r=>r.json()),
9937
+ fetch('/telemetry/summary?window=86400000').then(r=>r.json()),
9938
+ fetch('/v1/usage/quota/all').then(r=>r.json()).catch(function(){return null}),
9939
+ fetch('/profiles/list').then(r=>r.json()).catch(function(){return null})
9940
+ ]);
9941
+ render(health,stats,quota,profiles);
9346
9942
  }catch(e){document.getElementById('content').innerHTML='<div style="color:var(--red);padding:40px;text-align:center">Could not connect</div>'}
9347
9943
  }
9348
9944
 
9349
- function render(h,s){
9350
- const st=h.status||'unknown',dot=st==='healthy'?'healthy':st==='degraded'?'degraded':'unhealthy';
9945
+ function tokens(v){if(v==null)return '—';if(v>=1e6)return (v/1e6).toFixed(1)+'M';if(v>=1e3)return (v/1e3).toFixed(1)+'k';return String(v)}
9946
+
9947
+ function render(h,s,q,pl){
9351
9948
  let o='';
9352
- o+='<div class="status-banner"><div class="status-dot '+dot+'"></div><span class="status-text">'+(st==='healthy'?'Operational':st==='degraded'?'Degraded':'Offline')+'</span><span class="status-detail">Port '+location.port+' · '+(h.mode||'internal')+' mode</span></div>';
9353
- const er=s.totalRequests>0?((s.errorCount/s.totalRequests)*100).toFixed(1):'0';
9354
- o+='<div class="grid">'+card('Requests (24h)',s.totalRequests,'','violet')+card('Median Response',ms(s.totalDuration?.p50),'p95: '+ms(s.totalDuration?.p95),'')+card('Median TTFB',ms(s.ttfb?.p50),'p95: '+ms(s.ttfb?.p95),'')+card('Error Rate',er+'%',s.errorCount+' errors',parseFloat(er)>5?'':'green')+'</div>';
9355
- o+='<div class="section"><div class="section-title">Account</div>';
9356
- if(h.auth?.loggedIn){o+='<div class="info-grid"><span class="info-label">Email</span><span class="info-value">'+(h.auth.email||'—')+'</span><span class="info-label">Subscription</span><span class="info-value">'+(h.auth.subscriptionType||'—')+'</span><span class="info-label">Mode</span><span class="info-value">'+(h.mode||'internal')+'</span><span class="info-label">Endpoint</span><span class="info-value">http://'+location.host+'</span></div>'}
9357
- else{o+='<div class="info-grid"><span class="info-label">Status</span><span class="info-value" style="color:var(--yellow)">'+(h.error||'Not authenticated')+'</span></div>'}
9358
- o+='</div>';
9359
- if(s.byModel&&Object.keys(s.byModel).length>0){o+='<div class="section"><div class="section-title">Models (24h)</div><div class="grid">';for(const[n,d]of Object.entries(s.byModel))o+=card(n,d.count,'avg '+ms(d.avgTotalMs),'');o+='</div></div>'}
9360
- o+='<div class="section"><div class="section-title">Connect an Agent</div><div class="snippet"><div class="snippet-tabs"><div class="snippet-tab active" onclick="showTab(this,&apos;opencode&apos;)">OpenCode</div><div class="snippet-tab" onclick="showTab(this,&apos;crush&apos;)">Crush</div><div class="snippet-tab" onclick="showTab(this,&apos;generic&apos;)">Any Tool</div></div><div id="tab-opencode"><code>ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://'+location.host+' opencode</code></div><div id="tab-crush" style="display:none"><code>'+JSON.stringify({providers:{meridian:{type:"anthropic",base_url:"http://"+location.host,api_key:"x",models:[{id:"claude-sonnet-4-5-20250514",name:"Sonnet 4.5"}]}}},null,2)+'</code></div><div id="tab-generic" style="display:none"><code>export ANTHROPIC_API_KEY=x\\nexport ANTHROPIC_BASE_URL=http://'+location.host+'</code></div></div></div>';
9361
- o+='<div class="links"><a href="/telemetry" class="link">\uD83D\uDCCA Telemetry</a><a href="/settings" class="link">\uD83D\uDD27 Settings</a><a href="/profiles" class="link">\uD83D\uDC64 Profiles</a><a href="/health" class="link">\uD83E\uDE7A Health</a><a href="/telemetry/summary" class="link">\uD83D\uDCC8 Stats API</a><a href="https://github.com/rynfar/meridian" class="link">⚙️ GitHub</a></div>';
9362
- o+='<div class="footer">Meridian · Built on the <a href="https://github.com/anthropics/claude-code-sdk-js">Claude Code SDK</a></div>';
9949
+ o+=introSection(h);
9950
+
9951
+ // Accounts per-profile usage + est cost; click a card to switch
9952
+ o+=profileSection(q,s,pl,h);
9953
+
9954
+ // Last 24 hours — meaningful signals only. Errors and envelope
9955
+ // violations appear only when there is something to report.
9956
+ var tu=s.tokenUsage||{};
9957
+ var cache=tu.avgCacheHitRate!=null?Math.round(tu.avgCacheHitRate*100)+'%':'';
9958
+ var items=[
9959
+ ['Requests',String(s.totalRequests),s.errorCount>0?'red':'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors'],
9960
+ ['Tokens Out',tokens(tu.totalOutputTokens),'',tokens(tu.totalInputTokens)+' in'],
9961
+ ['Cache Hit',cache,tu.avgCacheHitRate>=0.5?'green':'','prompt cache'],
9962
+ ['Est. API Value',usd(s.costEstimate?.totalUsd),'','list prices'],
9963
+ ['Median Response',ms(s.totalDuration?.p50),'','p95 '+ms(s.totalDuration?.p95)]
9964
+ ];
9965
+ if(s.envelopeViolationCount>0)items.push(['Envelope',String(s.envelopeViolationCount),'red','wire-contract violations']);
9966
+ o+='<div class="section"><div class="section-title">Last 24 Hours</div>'+strip(items)+'</div>';
9967
+
9968
+ o+='<div class="footer">Meridian · <a href="https://github.com/rynfar/meridian">GitHub</a> · Built on the <a href="https://github.com/anthropics/claude-agent-sdk-typescript">Claude Agent SDK</a></div>';
9363
9969
  document.getElementById('content').innerHTML=o;
9364
9970
  }
9365
- function showTab(el,id){document.querySelectorAll('.snippet-tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');document.querySelectorAll('[id^="tab-"]').forEach(t=>t.style.display='none');document.getElementById('tab-'+id).style.display='block'}
9971
+
9972
+ function switchProfile(id){
9973
+ fetch('/profiles/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({profile:id})})
9974
+ .then(function(r){return r.json()})
9975
+ .then(function(data){if(data.success){refresh();if(window.meridianHeaderRefresh)window.meridianHeaderRefresh()}})
9976
+ .catch(function(){});
9977
+ }
9978
+ document.getElementById('content').addEventListener('click',function(e){
9979
+ var card=e.target.closest('.profile-card.switchable');
9980
+ if(card&&card.dataset.profile)switchProfile(card.dataset.profile);
9981
+ });
9982
+ document.getElementById('content').addEventListener('keydown',function(e){
9983
+ if(e.key!=='Enter'&&e.key!==' ')return;
9984
+ var card=e.target.closest('.profile-card.switchable');
9985
+ if(card&&card.dataset.profile){e.preventDefault();switchProfile(card.dataset.profile)}
9986
+ });
9366
9987
  refresh();setInterval(refresh,10000);
9367
9988
  ` + profileBarJs + `
9368
9989
  </script>
@@ -9429,7 +10050,7 @@ function renderPrometheusMetrics(store) {
9429
10050
  // src/telemetry/index.ts
9430
10051
  init_sqlite();
9431
10052
  function getDefaultDbPath() {
9432
- return join(homedir(), ".config", "meridian", "telemetry.db");
10053
+ return join2(homedir2(), ".config", "meridian", "telemetry.db");
9433
10054
  }
9434
10055
  function createStores() {
9435
10056
  if (!envBool("TELEMETRY_PERSIST")) {
@@ -10470,7 +11091,11 @@ ${text.slice(0, 2000)}` : text.slice(0, 2000);
10470
11091
  return createHash("sha256").update(seed).digest("hex").slice(0, 16);
10471
11092
  }
10472
11093
 
11094
+ // src/proxy/adapters/opencode.ts
11095
+ init_env();
11096
+
10473
11097
  // src/proxy/transforms/opencode.ts
11098
+ init_env();
10474
11099
  var openCodeTransforms = [
10475
11100
  {
10476
11101
  name: "opencode-core",
@@ -10636,7 +11261,11 @@ IMPORTANT: When using the task/Task tool, the subagent_type parameter must be on
10636
11261
  }
10637
11262
  };
10638
11263
 
11264
+ // src/proxy/adapters/droid.ts
11265
+ init_env();
11266
+
10639
11267
  // src/proxy/transforms/droid.ts
11268
+ init_env();
10640
11269
  var DROID_MCP_SERVER_NAME = "droid";
10641
11270
  var DROID_ALLOWED_MCP_TOOLS = [
10642
11271
  `mcp__${DROID_MCP_SERVER_NAME}__read`,
@@ -10942,7 +11571,11 @@ var passthroughAdapter = {
10942
11571
  }
10943
11572
  };
10944
11573
 
11574
+ // src/proxy/adapters/pi.ts
11575
+ init_env();
11576
+
10945
11577
  // src/proxy/transforms/pi.ts
11578
+ init_env();
10946
11579
  var PI_MCP_SERVER_NAME = "pi";
10947
11580
  var PI_ALLOWED_MCP_TOOLS = [
10948
11581
  `mcp__${PI_MCP_SERVER_NAME}__read`,
@@ -11175,6 +11808,7 @@ var forgeCodeAdapter = {
11175
11808
  };
11176
11809
 
11177
11810
  // src/proxy/adapters/claudecode.ts
11811
+ init_env();
11178
11812
  function extractClaudeCodeClientCwd(body) {
11179
11813
  let systemText = "";
11180
11814
  if (typeof body.system === "string") {
@@ -11298,10 +11932,10 @@ var cherryAdapter = {
11298
11932
  };
11299
11933
 
11300
11934
  // src/proxy/adapterInstances.ts
11301
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
11302
- import { join as join2 } from "node:path";
11303
- import { homedir as homedir2 } from "node:os";
11304
- var CONFIG_FILE = join2(homedir2(), ".config", "meridian", "adapter-instances.json");
11935
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
11936
+ import { join as join3 } from "node:path";
11937
+ import { homedir as homedir3 } from "node:os";
11938
+ var CONFIG_FILE = join3(homedir3(), ".config", "meridian", "adapter-instances.json");
11305
11939
  function parseAdapterInstances(raw2) {
11306
11940
  if (!raw2)
11307
11941
  return {};
@@ -11348,7 +11982,7 @@ function loadAdapterInstances() {
11348
11982
  if (diskCacheAt > 0 && Date.now() - diskCacheAt < DISK_CACHE_TTL_MS)
11349
11983
  return diskCache;
11350
11984
  try {
11351
- diskCache = existsSync3(CONFIG_FILE) ? parseAdapterInstances(readFileSync2(CONFIG_FILE, "utf-8")) : {};
11985
+ diskCache = existsSync4(CONFIG_FILE) ? parseAdapterInstances(readFileSync3(CONFIG_FILE, "utf-8")) : {};
11352
11986
  } catch (err) {
11353
11987
  console.warn(`[meridian] Failed to read ${CONFIG_FILE}: ${err instanceof Error ? err.message : err}`);
11354
11988
  diskCache = {};
@@ -17156,6 +17790,7 @@ function createOpencodeMcpServer() {
17156
17790
  }
17157
17791
 
17158
17792
  // src/proxy/query.ts
17793
+ init_env();
17159
17794
  function stripConfigDir(env2) {
17160
17795
  if (!("CLAUDE_CONFIG_DIR" in env2))
17161
17796
  return env2;
@@ -17379,8 +18014,8 @@ function getAdapterTransforms(adapterName) {
17379
18014
  }
17380
18015
 
17381
18016
  // src/proxy/plugins/loader.ts
17382
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
17383
- import { join as join3, isAbsolute as isAbsolute2, extname } from "path";
18017
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
18018
+ import { join as join4, isAbsolute as isAbsolute2, extname } from "path";
17384
18019
  import { pathToFileURL } from "url";
17385
18020
 
17386
18021
  // src/proxy/plugins/validation.ts
@@ -17421,10 +18056,10 @@ function validateTransform(exported) {
17421
18056
  // src/proxy/plugins/loader.ts
17422
18057
  var loadCounter = 0;
17423
18058
  function parsePluginConfig(configPath) {
17424
- if (!existsSync4(configPath))
18059
+ if (!existsSync5(configPath))
17425
18060
  return [];
17426
18061
  try {
17427
- const raw2 = readFileSync3(configPath, "utf-8");
18062
+ const raw2 = readFileSync4(configPath, "utf-8");
17428
18063
  const parsed = JSON.parse(raw2);
17429
18064
  return Array.isArray(parsed.plugins) ? parsed.plugins : [];
17430
18065
  } catch {
@@ -17434,7 +18069,7 @@ function parsePluginConfig(configPath) {
17434
18069
  async function loadPlugins(pluginDir, configPath) {
17435
18070
  resetAllPluginStats();
17436
18071
  const config = configPath ? parsePluginConfig(configPath) : [];
17437
- const pluginDirExists = existsSync4(pluginDir);
18072
+ const pluginDirExists = existsSync5(pluginDir);
17438
18073
  let filenames = [];
17439
18074
  if (pluginDirExists) {
17440
18075
  try {
@@ -17465,7 +18100,7 @@ async function loadPlugins(pluginDir, configPath) {
17465
18100
  const loaded = [];
17466
18101
  const seenNames = new Set;
17467
18102
  for (const { filename, entry } of ordered) {
17468
- const filePath = isAbsolute2(filename) ? filename : join3(pluginDir, filename);
18103
+ const filePath = isAbsolute2(filename) ? filename : join4(pluginDir, filename);
17469
18104
  if (entry && !entry.enabled) {
17470
18105
  loaded.push({
17471
18106
  name: filename,
@@ -17838,17 +18473,17 @@ function verifyLineage(cached, messages, cacheKey2, cache) {
17838
18473
  // src/proxy/sessionStore.ts
17839
18474
  import {
17840
18475
  closeSync,
17841
- existsSync as existsSync5,
17842
- mkdirSync,
18476
+ existsSync as existsSync6,
18477
+ mkdirSync as mkdirSync2,
17843
18478
  openSync,
17844
- readFileSync as readFileSync4,
17845
- renameSync,
18479
+ readFileSync as readFileSync5,
18480
+ renameSync as renameSync2,
17846
18481
  statSync,
17847
18482
  unlinkSync,
17848
- writeFileSync
18483
+ writeFileSync as writeFileSync2
17849
18484
  } from "node:fs";
17850
- import { homedir as homedir3 } from "node:os";
17851
- import { join as join4 } from "node:path";
18485
+ import { homedir as homedir4 } from "node:os";
18486
+ import { join as join5 } from "node:path";
17852
18487
  var DEFAULT_MAX_STORED_SESSIONS = 1e4;
17853
18488
  var STALE_LOCK_THRESHOLD_MS = 30000;
17854
18489
  function getMaxStoredSessions() {
@@ -17896,17 +18531,17 @@ var sessionDirOverride = null;
17896
18531
  var skipLocking = false;
17897
18532
  function getStorePath() {
17898
18533
  const dir = sessionDirOverride || process.env.MERIDIAN_SESSION_DIR || process.env.CLAUDE_PROXY_SESSION_DIR || getDefaultCacheDir();
17899
- if (!existsSync5(dir)) {
17900
- mkdirSync(dir, { recursive: true });
18534
+ if (!existsSync6(dir)) {
18535
+ mkdirSync2(dir, { recursive: true });
17901
18536
  }
17902
- return join4(dir, "sessions.json");
18537
+ return join5(dir, "sessions.json");
17903
18538
  }
17904
18539
  function getDefaultCacheDir() {
17905
- const newDir = join4(homedir3(), ".cache", "meridian");
17906
- const oldDir = join4(homedir3(), ".cache", "opencode-claude-max-proxy");
17907
- if (existsSync5(newDir))
18540
+ const newDir = join5(homedir4(), ".cache", "meridian");
18541
+ const oldDir = join5(homedir4(), ".cache", "opencode-claude-max-proxy");
18542
+ if (existsSync6(newDir))
17908
18543
  return newDir;
17909
- if (existsSync5(oldDir)) {
18544
+ if (existsSync6(oldDir)) {
17910
18545
  try {
17911
18546
  const { symlinkSync } = __require("fs");
17912
18547
  symlinkSync(oldDir, newDir);
@@ -17919,10 +18554,10 @@ function getDefaultCacheDir() {
17919
18554
  }
17920
18555
  function readStore() {
17921
18556
  const path3 = getStorePath();
17922
- if (!existsSync5(path3))
18557
+ if (!existsSync6(path3))
17923
18558
  return {};
17924
18559
  try {
17925
- const data = readFileSync4(path3, "utf-8");
18560
+ const data = readFileSync5(path3, "utf-8");
17926
18561
  return JSON.parse(data);
17927
18562
  } catch (e) {
17928
18563
  console.error("[sessionStore] read failed:", e.message);
@@ -17933,12 +18568,12 @@ function writeStore(store) {
17933
18568
  const path3 = getStorePath();
17934
18569
  const tmp = `${path3}.tmp`;
17935
18570
  try {
17936
- writeFileSync(tmp, JSON.stringify(store, null, 2));
17937
- renameSync(tmp, path3);
18571
+ writeFileSync2(tmp, JSON.stringify(store, null, 2));
18572
+ renameSync2(tmp, path3);
17938
18573
  } catch (e) {
17939
18574
  console.error("[sessionStore] write failed:", e.message);
17940
18575
  try {
17941
- writeFileSync(path3, JSON.stringify(store, null, 2));
18576
+ writeFileSync2(path3, JSON.stringify(store, null, 2));
17942
18577
  } catch (directWriteError) {
17943
18578
  console.error("[sessionStore] write failed:", directWriteError.message);
17944
18579
  }
@@ -18044,7 +18679,7 @@ function listStoredSessions() {
18044
18679
  function clearSharedSessions() {
18045
18680
  const path3 = getStorePath();
18046
18681
  try {
18047
- writeFileSync(path3, "{}");
18682
+ writeFileSync2(path3, "{}");
18048
18683
  } catch (e) {
18049
18684
  console.error("[sessionStore] clear failed:", e.message);
18050
18685
  }
@@ -18473,8 +19108,8 @@ function createProxyServer(config = {}) {
18473
19108
  pendingSessionStores.delete(key);
18474
19109
  };
18475
19110
  };
18476
- const pluginDir = finalConfig.pluginDir ?? join6(homedir5(), ".config", "meridian", "plugins");
18477
- const pluginConfigPath = finalConfig.pluginConfigPath ?? join6(homedir5(), ".config", "meridian", "plugins.json");
19111
+ const pluginDir = finalConfig.pluginDir ?? join7(homedir6(), ".config", "meridian", "plugins");
19112
+ const pluginConfigPath = finalConfig.pluginConfigPath ?? join7(homedir6(), ".config", "meridian", "plugins.json");
18478
19113
  let loadedPlugins = [];
18479
19114
  let pluginTransforms = [];
18480
19115
  const app = new Hono2;
@@ -19391,6 +20026,7 @@ Subprocess stderr: ${stderrOutput}`;
19391
20026
  requestId: requestMeta.requestId,
19392
20027
  timestamp: Date.now(),
19393
20028
  adapter: adapter.name,
20029
+ profileId: profile.id,
19394
20030
  requestSource,
19395
20031
  model,
19396
20032
  requestModel: body.model || undefined,
@@ -20202,6 +20838,7 @@ data: {"type":"message_stop"}
20202
20838
  requestId: requestMeta.requestId,
20203
20839
  timestamp: Date.now(),
20204
20840
  adapter: adapter.name,
20841
+ profileId: profile.id,
20205
20842
  requestSource,
20206
20843
  model,
20207
20844
  requestModel: body.model || undefined,
@@ -20335,6 +20972,7 @@ data: {"type":"message_stop"}
20335
20972
  requestId: requestMeta.requestId,
20336
20973
  timestamp: Date.now(),
20337
20974
  adapter: adapter.name,
20975
+ profileId: profile.id,
20338
20976
  requestSource,
20339
20977
  model,
20340
20978
  requestModel: body.model || undefined,
@@ -20379,6 +21017,7 @@ data: {"type":"message_stop"}
20379
21017
  requestId: requestMeta.requestId,
20380
21018
  timestamp: Date.now(),
20381
21019
  adapter: adapter.name,
21020
+ profileId: profile.id,
20382
21021
  requestSource,
20383
21022
  model,
20384
21023
  requestModel: body.model || undefined,
@@ -20534,6 +21173,31 @@ data: ${JSON.stringify({
20534
21173
  resetAdapterFeatures2(adapter);
20535
21174
  return c.json({ ok: true });
20536
21175
  });
21176
+ app.get("/settings/api/pricing", (c) => {
21177
+ const { BUILTIN_MODEL_PRICING: BUILTIN_MODEL_PRICING2 } = (init_pricing(), __toCommonJS(exports_pricing));
21178
+ const { getPricingOverrides: getPricingOverrides2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
21179
+ return c.json({ builtin: BUILTIN_MODEL_PRICING2, overrides: getPricingOverrides2() });
21180
+ });
21181
+ app.put("/settings/api/pricing/:model", async (c) => {
21182
+ const { validatePricingUpdate: validatePricingUpdate2, setPricingOverride: setPricingOverride2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
21183
+ const model = c.req.param("model");
21184
+ try {
21185
+ const body = await c.req.json();
21186
+ setPricingOverride2(model, validatePricingUpdate2(body));
21187
+ } catch (e) {
21188
+ return c.json({ error: e.message }, 400);
21189
+ }
21190
+ return c.json({ ok: true });
21191
+ });
21192
+ app.delete("/settings/api/pricing/:model", (c) => {
21193
+ const { deletePricingOverride: deletePricingOverride2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
21194
+ try {
21195
+ deletePricingOverride2(c.req.param("model"));
21196
+ } catch (e) {
21197
+ return c.json({ error: e.message }, 400);
21198
+ }
21199
+ return c.json({ ok: true });
21200
+ });
20537
21201
  app.get("/metrics", (c) => {
20538
21202
  const body = renderPrometheusMetrics(telemetryStore2);
20539
21203
  return c.body(body, 200, {
@@ -20606,7 +21270,7 @@ data: ${JSON.stringify({
20606
21270
  });
20607
21271
  });
20608
21272
  app.get("/profiles", async (c) => {
20609
- const { profilePageHtml } = await import("./profilePage-txxpbc44.js");
21273
+ const { profilePageHtml } = await import("./profilePage-naychnb8.js");
20610
21274
  return c.html(profilePageHtml);
20611
21275
  });
20612
21276
  app.post("/profiles/active", async (c) => {
@@ -20668,7 +21332,7 @@ data: ${JSON.stringify({
20668
21332
  }
20669
21333
  });
20670
21334
  app.get("/plugins", async (c) => {
20671
- const { pluginPageHtml } = await import("./pluginPage-85s6t6k8.js");
21335
+ const { pluginPageHtml } = await import("./pluginPage-03q5j753.js");
20672
21336
  return c.html(pluginPageHtml);
20673
21337
  });
20674
21338
  app.post("/auth/refresh", async (c) => {