@rynfar/meridian 1.55.1 → 1.56.1

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.
@@ -1,12 +1,15 @@
1
1
  import {
2
+ ProfileExhaustion,
3
+ choosePriorityProfile,
2
4
  getActiveProfileId,
3
5
  getEffectiveProfiles,
4
6
  getRoutingMode,
5
7
  listProfiles,
8
+ resolvePriorityOrder,
6
9
  resolveProfile,
7
10
  restoreActiveProfile,
8
11
  setActiveProfile
9
- } from "./cli-f0yqy2d2.js";
12
+ } from "./cli-ngtexmne.js";
10
13
  import {
11
14
  isTrackedPlugin,
12
15
  recordError,
@@ -46,7 +49,8 @@ import {
46
49
  stripExtendedContext
47
50
  } from "./cli-jhm27q0x.js";
48
51
  import {
49
- getSetting
52
+ getSetting,
53
+ setSetting
50
54
  } from "./cli-340h1chz.js";
51
55
  import {
52
56
  checkPluginConfigured
@@ -1758,6 +1762,19 @@ ${profileBarHtml}
1758
1762
 
1759
1763
  <div id="adapters"></div>
1760
1764
 
1765
+ <h1 style="margin-top:40px">Routing</h1>
1766
+ <p class="subtitle" style="max-width:720px;line-height:1.6">
1767
+ How unpinned requests choose an account. <strong style="color:var(--text)">Active</strong> uses the manually
1768
+ selected profile. <strong style="color:var(--text)">Sticky</strong> distributes sessions across profiles evenly
1769
+ (cache-affine). <strong style="color:var(--text)">Priority</strong> drains the pool in order — highest first —
1770
+ and fails over per request when an account runs out; conversations keep their account, and new sessions
1771
+ return to the preferred account after its window resets. An explicit <code>x-meridian-profile</code> header
1772
+ always overrides. Changes apply to the next request — no restart needed.
1773
+ </p>
1774
+ <div class="adapter-card" id="routing-card">
1775
+ <div id="routing-body">Loading…</div>
1776
+ </div>
1777
+
1761
1778
  <h1 style="margin-top:40px">Model Pricing</h1>
1762
1779
  <p class="subtitle" style="max-width:720px;line-height:1.6">
1763
1780
  Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the
@@ -2036,8 +2053,45 @@ async function addPricingModel() {
2036
2053
  }
2037
2054
  }
2038
2055
 
2056
+ async function loadRouting() {
2057
+ const res = await fetch('/settings/api/routing');
2058
+ const cfg = await res.json();
2059
+ const el = document.getElementById('routing-body');
2060
+ const envNote = (on) => on ? ' <span style="font-size:11px;color:var(--yellow)">(env override active — setting saved but env wins)</span>' : '';
2061
+ let h = '<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">'
2062
+ + '<label style="color:var(--muted);font-size:13px;width:90px">Mode</label>'
2063
+ + '<select id="routing-mode" style="background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px">'
2064
+ + ['active','sticky','priority'].map(m => '<option value="'+m+'"'+(cfg.routing===m?' selected':'')+'>'+m+'</option>').join('')
2065
+ + '</select>' + envNote(cfg.envOverride.routing) + '</div>';
2066
+ h += '<div id="routing-order-wrap" style="'+(cfg.routing==='priority'?'':'display:none')+'">'
2067
+ + '<div style="color:var(--muted);font-size:13px;margin-bottom:8px">Pool order — highest priority first. Drained top to bottom.'
2068
+ + envNote(cfg.envOverride.profileOrder) + '</div>'
2069
+ + '<ol id="routing-order" style="margin:0;padding-left:22px">'
2070
+ + cfg.profileOrder.map((id, i) =>
2071
+ '<li style="padding:4px 0;color:var(--text)">'
2072
+ + '<span style="font-family:var(--mono, monospace)">'+id+'</span>'
2073
+ + ' <button data-move="up" data-i="'+i+'" style="margin-left:10px;background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer"'+(i===0?' disabled':'')+'>&uarr;</button>'
2074
+ + ' <button data-move="down" data-i="'+i+'" style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer"'+(i===cfg.profileOrder.length-1?' disabled':'')+'>&darr;</button>'
2075
+ + '</li>').join('')
2076
+ + '</ol></div>';
2077
+ el.innerHTML = h;
2078
+ document.getElementById('routing-mode').addEventListener('change', async (e) => {
2079
+ await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ routing: e.target.value }) });
2080
+ await loadRouting();
2081
+ });
2082
+ el.querySelectorAll('button[data-move]').forEach(btn => btn.addEventListener('click', async () => {
2083
+ const i = Number(btn.dataset.i);
2084
+ const j = btn.dataset.move === 'up' ? i - 1 : i + 1;
2085
+ const order = cfg.profileOrder.slice();
2086
+ const tmp = order[i]; order[i] = order[j]; order[j] = tmp;
2087
+ await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileOrder: order }) });
2088
+ await loadRouting();
2089
+ }));
2090
+ }
2091
+
2039
2092
  loadConfig();
2040
2093
  loadPricing();
2094
+ loadRouting();
2041
2095
  ${profileBarJs}
2042
2096
  </script>
2043
2097
  </body>
@@ -4418,6 +4472,7 @@ function linkRequestAbort(signal) {
4418
4472
  var OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
4419
4473
  var OAUTH_BETA_HEADER = "oauth-2025-04-20";
4420
4474
  var CACHE_TTL_MS_DEFAULT = 30000;
4475
+ var STALE_MAX_MS_DEFAULT = 15 * 60000;
4421
4476
  var cacheByProfile = new Map;
4422
4477
  var inflightByProfile = new Map;
4423
4478
  var DEFAULT_KEY = "__default__";
@@ -4522,34 +4577,45 @@ async function fetchOAuthUsageImpl(opts) {
4522
4577
  if (existing)
4523
4578
  return existing;
4524
4579
  const store = opts?.store ?? createPlatformCredentialStore({ claudeConfigDir: opts?.claudeConfigDir });
4580
+ const staleMaxMs = opts?.staleMaxMs ?? STALE_MAX_MS_DEFAULT;
4581
+ const staleOr = (reason) => {
4582
+ const last = cacheByProfile.get(cacheKey2);
4583
+ if (last && Date.now() - last.fetchedAt < staleMaxMs) {
4584
+ claudeLog("oauth_usage.serving_stale", { profile: cacheKey2, reason, ageMs: Date.now() - last.fetchedAt });
4585
+ return { ...last, stale: true };
4586
+ }
4587
+ return null;
4588
+ };
4525
4589
  const promise = (async () => {
4526
4590
  try {
4527
4591
  const token = await readAccessToken(store);
4528
- if (!token)
4529
- return null;
4592
+ if (!token) {
4593
+ claudeLog("oauth_usage.no_token", { profile: cacheKey2 });
4594
+ return staleOr("no_token");
4595
+ }
4530
4596
  let result = await callAnthropic(token, fetchImpl);
4531
4597
  if ("__status" in result && result.__status === 401) {
4532
4598
  claudeLog("oauth_usage.token_refresh_attempt", { profile: cacheKey2 });
4533
4599
  const refreshed = await refreshOAuthToken(store);
4534
4600
  if (!refreshed) {
4535
4601
  claudeLog("oauth_usage.refresh_failed", { profile: cacheKey2 });
4536
- return null;
4602
+ return staleOr("refresh_failed");
4537
4603
  }
4538
4604
  const newToken = await readAccessToken(store);
4539
4605
  if (!newToken)
4540
- return null;
4606
+ return staleOr("no_token_after_refresh");
4541
4607
  result = await callAnthropic(newToken, fetchImpl);
4542
4608
  }
4543
4609
  if ("__status" in result) {
4544
4610
  claudeLog("oauth_usage.upstream_error", { profile: cacheKey2, status: result.__status });
4545
- return null;
4611
+ return staleOr(`upstream_${result.__status}`);
4546
4612
  }
4547
4613
  const snapshot = buildSnapshot(result);
4548
4614
  cacheByProfile.set(cacheKey2, snapshot);
4549
4615
  return snapshot;
4550
4616
  } catch (err) {
4551
4617
  claudeLog("oauth_usage.fetch_failed", { profile: cacheKey2, error: err instanceof Error ? err.message : String(err) });
4552
- return null;
4618
+ return staleOr("exception");
4553
4619
  } finally {
4554
4620
  inflightByProfile.delete(cacheKey2);
4555
4621
  }
@@ -9394,27 +9460,8 @@ var dashboardHtml = `<!DOCTYPE html>
9394
9460
  transition: all 0.15s; }
9395
9461
  .log-filter:hover { border-color: var(--accent); color: var(--text); }
9396
9462
  .log-filter.active { background: rgba(88,166,255,0.1); border-color: var(--accent); color: var(--accent); }
9397
-
9398
- /* Usage tab */
9399
- .usage-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
9400
- .ucard { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
9401
- .ucard-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
9402
- .ucard-title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
9403
- .ucard-reset { font-size: 11px; color: var(--muted); white-space: nowrap; }
9404
- .ucard-pct { font-size: 32px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 8px; color: var(--green); }
9405
- .ucard.warn .ucard-pct { color: var(--yellow); }
9406
- .ucard.high .ucard-pct { color: var(--red); }
9407
- .ucard-sub { font-size: 12px; color: var(--muted); margin-top: 8px; min-height: 16px; }
9408
- .ubar { position: relative; height: 8px; border-radius: 4px; background: var(--border); overflow: visible; margin-top: 12px; }
9409
- .ubar-fill { height: 100%; border-radius: 4px; background: var(--green); transition: width 0.4s ease; max-width: 100%; }
9410
- .ucard.warn .ubar-fill { background: var(--yellow); }
9411
- .ucard.high .ubar-fill { background: var(--red); }
9412
- .ubar-marker { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); opacity: 0.55; border-radius: 1px; }
9413
- .pace-pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
9414
- .pace-pill.on, .pace-pill.under { background: rgba(63,185,80,0.15); color: var(--green); }
9415
- .pace-pill.ahead { background: rgba(210,153,34,0.18); color: var(--yellow); }
9416
- .pace-pill.over { background: rgba(248,81,73,0.15); color: var(--red); }
9417
9463
  .usage-note { font-size: 11px; color: var(--muted); }
9464
+
9418
9465
  ` + profileBarCss + `
9419
9466
  </style>
9420
9467
  </head>
@@ -9507,22 +9554,20 @@ function setLogFilter(filter) {
9507
9554
  async function refresh() {
9508
9555
  const w = $('#window').value;
9509
9556
  try {
9510
- const [summary, reqs, logs, quota] = await Promise.all([
9557
+ const [summary, reqs, logs] = await Promise.all([
9511
9558
  fetch('/telemetry/summary?window=' + w).then(r => r.json()),
9512
9559
  fetch('/telemetry/requests?limit=50&since=' + (Date.now() - Number(w))).then(r => r.json()),
9513
9560
  fetch('/telemetry/logs?limit=200&since=' + (Date.now() - Number(w))).then(r => r.json()),
9514
- fetch('/v1/usage/quota').then(r => r.json()).catch(() => null),
9515
9561
  ]);
9516
- render(summary, reqs, logs, quota);
9562
+ render(summary, reqs, logs);
9517
9563
  $('#lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString();
9518
9564
  } catch (e) {
9519
9565
  $('#content').innerHTML = '<div class="empty">Failed to load telemetry</div>';
9520
9566
  }
9521
9567
  }
9522
9568
 
9523
- function render(s, reqs, logs, quota) {
9524
- const hasUsage = quota && quota.buckets && quota.buckets.some(b => b.utilization != null);
9525
- if (s.totalRequests === 0 && (!logs || logs.length === 0) && !hasUsage) {
9569
+ function render(s, reqs, logs) {
9570
+ if (s.totalRequests === 0 && (!logs || logs.length === 0)) {
9526
9571
  $('#content').innerHTML = '<div class="empty">No requests recorded yet. Send a request through the proxy to see telemetry.</div>';
9527
9572
  return;
9528
9573
  }
@@ -9540,7 +9585,6 @@ function render(s, reqs, logs, quota) {
9540
9585
  + 'Requests<span class="tab-badge">' + reqs.length + '</span></div>'
9541
9586
  + '<div class="tab' + (activeTab === 'logs' ? ' active' : '') + '" data-tab="logs" onclick="switchTab(&apos;logs&apos;)">'
9542
9587
  + 'Logs<span class="tab-badge">' + logs.length + '</span></div>'
9543
- + '<div class="tab' + (activeTab === 'usage' ? ' active' : '') + '" data-tab="usage" onclick="switchTab(&apos;usage&apos;)">Usage</div>'
9544
9588
  + '</div>';
9545
9589
 
9546
9590
  // ==================== Overview tab ====================
@@ -9730,11 +9774,6 @@ function render(s, reqs, logs, quota) {
9730
9774
  }
9731
9775
  html += '</div>'; // end logs panel
9732
9776
 
9733
- // ==================== Usage tab ====================
9734
- html += '<div id="panel-usage" class="tab-panel' + (activeTab === 'usage' ? ' active' : '') + '">';
9735
- html += renderUsage(quota);
9736
- html += '</div>'; // end usage panel
9737
-
9738
9777
  $('#content').innerHTML = html;
9739
9778
  }
9740
9779
 
@@ -9745,101 +9784,6 @@ function card(label, value, detail) {
9745
9784
  + '</div>';
9746
9785
  }
9747
9786
 
9748
- // ---- Usage tab helpers (mirror src/telemetry/profileUsage.ts; unit-tested there) ----
9749
- function classifyUtil(u) {
9750
- if (u == null || !isFinite(u)) return '';
9751
- if (u >= 0.85) return 'high';
9752
- if (u >= 0.6) return 'warn';
9753
- return '';
9754
- }
9755
- function resetIn(resetsAt) {
9756
- if (resetsAt == null || !isFinite(resetsAt)) return '';
9757
- var ms = resetsAt - Date.now();
9758
- if (ms <= 0) return 'resetting…';
9759
- var m = Math.floor(ms / 60000);
9760
- if (m < 60) return 'resets in ' + Math.max(1, m) + 'm';
9761
- var h = Math.floor(m / 60), rm = m % 60;
9762
- if (h < 24) return 'resets in ' + h + 'h' + (rm ? ' ' + rm + 'm' : '');
9763
- var d = Math.floor(h / 24), rh = h % 24;
9764
- return 'resets in ' + d + 'd' + (rh ? ' ' + rh + 'h' : '');
9765
- }
9766
- function pct(u) { return Math.round(Math.max(0, u) * 100); }
9767
-
9768
- function usageCard(title, bucket) {
9769
- if (!bucket || bucket.utilization == null) {
9770
- return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">' + title + '</span></div>'
9771
- + '<div class="ucard-pct" style="color:var(--muted)">—</div>'
9772
- + '<div class="ucard-sub">No data yet</div></div>';
9773
- }
9774
- var u = bucket.utilization;
9775
- var cls = classifyUtil(u);
9776
- var fill = Math.min(100, pct(u));
9777
- return '<div class="ucard ' + cls + '">'
9778
- + '<div class="ucard-head"><span class="ucard-title">' + title + '</span>'
9779
- + '<span class="ucard-reset">' + resetIn(bucket.resetsAt) + '</span></div>'
9780
- + '<div class="ucard-pct">' + pct(u) + '<span style="font-size:16px;font-weight:500;color:var(--muted)">%</span></div>'
9781
- + '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%"></div></div>'
9782
- + '<div class="ucard-sub">of your ' + title.split('·')[1].trim() + ' allowance used</div>'
9783
- + '</div>';
9784
- }
9785
-
9786
- // Weekly pace: actual vs. expected (even) consumption at this point in the 7-day window.
9787
- function paceCard(weekly) {
9788
- if (!weekly || weekly.utilization == null || weekly.resetsAt == null) {
9789
- return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">Weekly Pace</span></div>'
9790
- + '<div class="ucard-pct" style="color:var(--muted)">—</div>'
9791
- + '<div class="ucard-sub">Needs weekly usage data</div></div>';
9792
- }
9793
- var WEEK = 7 * 86400000;
9794
- var start = weekly.resetsAt - WEEK;
9795
- var elapsed = Math.max(0, Math.min(1, (Date.now() - start) / WEEK));
9796
- var actual = pct(weekly.utilization);
9797
- var expected = Math.round(elapsed * 100);
9798
- var delta = actual - expected;
9799
- var projected = elapsed >= 0.1 ? Math.round((Math.max(0, weekly.utilization) / elapsed) * 100) : null;
9800
-
9801
- var pill, label;
9802
- if (delta > 7) { pill = 'ahead'; label = '+' + delta + '% ahead of pace'; }
9803
- else if (delta < -7) { pill = 'under'; label = Math.abs(delta) + '% under pace'; }
9804
- else { pill = 'on'; label = 'On pace'; }
9805
- if (projected != null && projected >= 100) { pill = 'over'; label = 'On track to run out'; }
9806
-
9807
- var fill = Math.min(100, actual);
9808
- var mark = Math.min(100, expected);
9809
- var proj = projected == null ? '—' : projected + '%';
9810
- return '<div class="ucard">'
9811
- + '<div class="ucard-head"><span class="ucard-title">Weekly Pace</span>'
9812
- + '<span class="ucard-reset">' + Math.round(elapsed * 100) + '% through week</span></div>'
9813
- + '<div style="margin-top:8px"><span class="pace-pill ' + pill + '">' + label + '</span></div>'
9814
- + '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%;background:' + (pill === 'over' ? 'var(--red)' : pill === 'ahead' ? 'var(--yellow)' : 'var(--green)') + '"></div>'
9815
- + '<div class="ubar-marker" style="left:' + mark + '%" title="Expected at even pace"></div></div>'
9816
- + '<div class="ucard-sub">' + actual + '% used vs ' + expected + '% expected · at this rate ~' + proj + ' by reset</div>'
9817
- + '</div>';
9818
- }
9819
-
9820
- function renderUsage(quota) {
9821
- if (!quota || !quota.buckets) {
9822
- return '<div class="empty">Usage data unavailable.</div>';
9823
- }
9824
- var by = {};
9825
- quota.buckets.forEach(function (b) { by[b.type] = b; });
9826
- var session = by['five_hour'], weekly = by['seven_day'];
9827
- if ((!session || session.utilization == null) && (!weekly || weekly.utilization == null)) {
9828
- return '<div class="empty">No usage data yet — Anthropic reports it after your first request through Meridian.</div>';
9829
- }
9830
- var h = '<div class="usage-cards">'
9831
- + usageCard('Session · 5h', session)
9832
- + usageCard('Weekly · 7d', weekly)
9833
- + paceCard(weekly)
9834
- + '</div>';
9835
- var asOf = quota.asOf ? new Date(quota.asOf).toLocaleTimeString() : '';
9836
- h += '<div class="usage-note">'
9837
- + (quota.profile ? 'Profile: ' + quota.profile + ' · ' : '')
9838
- + 'Reported by Anthropic' + (asOf ? ' · as of ' + asOf : '')
9839
- + '</div>';
9840
- return h;
9841
- }
9842
-
9843
9787
  $('#autoRefresh').addEventListener('change', function() {
9844
9788
  clearInterval(timer);
9845
9789
  if (this.checked) timer = setInterval(refresh, 5000);
@@ -9947,6 +9891,12 @@ var landingHtml = `<!DOCTYPE html>
9947
9891
  .usage-row .w-label { color: var(--muted); width: 64px; flex-shrink: 0; }
9948
9892
  .usage-row .w-bar { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
9949
9893
  .usage-row .w-fill { height: 100%; border-radius: 3px; }
9894
+ .pace-row { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 8px; }
9895
+ .pace-row .w-bar { overflow: visible; position: relative; }
9896
+ .pace-marker { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); opacity: 0.55; border-radius: 1px; }
9897
+ .pace-row .w-pct { font-weight: 600; }
9898
+ .pool-chip { font-size: 10px; padding: 2px 8px; border-radius: 10px; background: var(--surface2); color: var(--muted); margin-left: 6px; vertical-align: middle; }
9899
+ .pool-chip.exhausted { color: var(--red); background: rgba(248,81,73,0.12); }
9950
9900
  .usage-row .w-pct { width: 38px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
9951
9901
  .usage-row .w-reset { color: var(--muted); font-size: 11px; width: 76px; text-align: right; }
9952
9902
  .no-usage { font-size: 12px; color: var(--muted); padding: 4px 0; }
@@ -9961,6 +9911,7 @@ var landingHtml = `<!DOCTYPE html>
9961
9911
  .strip-value.green { color: var(--green); }
9962
9912
  .strip-value.red { color: var(--red); }
9963
9913
  .strip-detail { font-size: 11px; color: var(--muted); }
9914
+ .strip-detail.red { color: var(--red); }
9964
9915
 
9965
9916
  .section { margin-bottom: 24px; }
9966
9917
  .section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase;
@@ -9985,6 +9936,29 @@ function usd(v){if(v==null)return '—';if(v>0&&v<0.01)return '$'+v.toFixed(4);i
9985
9936
  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'};
9986
9937
  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()})}
9987
9938
  function utilColor(u){return u>=0.85?'var(--red)':u>=0.6?'var(--yellow)':'var(--green)'}
9939
+ // Mirrors computeWeeklyPace in src/telemetry/profileUsage.ts (unit-tested
9940
+ // there): actual vs expected (even) consumption at this point in the 7-day
9941
+ // window, with the dashboard's over-promotion when the projection hits 100%.
9942
+ function weeklyPace(u,resetsAt){
9943
+ var WEEK=7*86400000;
9944
+ if(u==null||resetsAt==null)return null;
9945
+ var el=Math.max(0,Math.min(1,(Date.now()-(resetsAt-WEEK))/WEEK));
9946
+ var actual=Math.round(Math.max(0,u)*100);
9947
+ var expected=Math.round(el*100);
9948
+ var delta=actual-expected;
9949
+ var proj=el>=0.1?Math.round((Math.max(0,u)/el)*100):null;
9950
+ var st=delta>7?'ahead':delta<-7?'under':'on';
9951
+ if(proj!=null&&proj>=100)st='over';
9952
+ return {actual:actual,expected:expected,delta:delta,proj:proj,status:st};
9953
+ }
9954
+ function paceText(pc){
9955
+ if(pc.status==='over')return 'on track to run out';
9956
+ if(pc.status==='ahead')return '+'+pc.delta+'% ahead of pace';
9957
+ if(pc.status==='under')return Math.abs(pc.delta)+'% under pace';
9958
+ return 'on pace';
9959
+ }
9960
+ function paceColor(pc){return pc.status==='over'?'var(--red)':pc.status==='ahead'?'var(--yellow)':'var(--green)'}
9961
+
9988
9962
  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':'')}
9989
9963
 
9990
9964
  function introSection(h){
@@ -10031,9 +10005,30 @@ function profileSection(q,s,pl,h){
10031
10005
  +'<span class="w-pct" style="color:'+utilColor(w.utilization)+'">'+pct+'%</span>'
10032
10006
  +'<span class="w-reset">'+resetIn(w.resetsAt)+'</span></div>';
10033
10007
  }
10008
+ var weekly=null;
10009
+ for(var j=0;j<wins.length;j++){if(wins[j].type==='seven_day')weekly=wins[j]}
10010
+ var pc=weekly?weeklyPace(weekly.utilization,weekly.resetsAt):null;
10011
+ if(pc){
10012
+ // Visual actual-vs-expected: fill = actual usage (status-colored),
10013
+ // tick marker = where even pace would be. The gap IS the pace.
10014
+ var paceTip=paceText(pc)+' · '+pc.actual+'% used vs '+pc.expected+'% expected'+(pc.proj!=null?' · ~'+pc.proj+'% by reset':'');
10015
+ var deltaLabel=pc.status==='over'?(pc.proj!=null?pc.proj+'%':'100%'):(pc.delta>=0?'+':'−')+Math.abs(pc.delta)+'%';
10016
+ rows+='<div class="usage-row pace-row" title="'+paceTip+'"><span class="w-label">pace</span>'
10017
+ +'<div class="w-bar"><div class="w-fill" style="width:'+Math.min(pc.actual,100)+'%;background:'+paceColor(pc)+'"></div>'
10018
+ +'<div class="pace-marker" style="left:'+Math.min(pc.expected,100)+'%" title="expected at even pace ('+pc.expected+'%)"></div></div>'
10019
+ +'<span class="w-pct" style="color:'+paceColor(pc)+'">'+deltaLabel+'</span>'
10020
+ +'<span class="w-reset">'+(pc.status==='over'?'runs out before reset':pc.proj!=null?'~'+pc.proj+'% by reset':'')+'</span></div>';
10021
+ }
10034
10022
  if(!rows)rows='<div class="no-usage">no usage data yet</div>';
10035
- var switchable=multi&&p.configured&&!p.isActive;
10036
- var badge=p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
10023
+ var isPriority=pl&&pl.routing==='priority';
10024
+ var switchable=multi&&p.configured&&!p.isActive&&!isPriority;
10025
+ var badge=isPriority?'':p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
10026
+ if(isPriority){
10027
+ var orderIdx=(pl.profileOrder||[]).indexOf(p.id);
10028
+ if(orderIdx>=0)badge+='<span class="pool-chip">#'+(orderIdx+1)+' in pool</span>';
10029
+ var exh=(pl.exhausted||[]).filter(function(e){return e.id===p.id})[0];
10030
+ if(exh)badge+=' <span class="pool-chip exhausted">exhausted · resets '+resetIn(exh.until)+'</span>';
10031
+ }
10037
10032
  cards+='<div class="profile-card'+(p.isActive?' active':'')+(switchable?' switchable':'')+'"'+(switchable?' data-profile="'+esc(p.id)+'" role="button" tabindex="0"':'')+'>'
10038
10033
  +'<div class="profile-head"><span class="profile-name"><span class="prof-dot"></span>'+esc(p.label||p.id)+' '+badge+'</span>'
10039
10034
  +'<span class="profile-cost">'+usd(cost?cost.estimatedUsd:0)+'</span></div>'
@@ -10047,7 +10042,7 @@ function profileSection(q,s,pl,h){
10047
10042
  function strip(items){
10048
10043
  var o='<div class="strip">';
10049
10044
  for(var i=0;i<items.length;i++){var it=items[i];
10050
- 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>';
10045
+ 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[4]||'')+'">'+it[3]+'</div>':'')+'</div>';
10051
10046
  }
10052
10047
  return o+'</div>';
10053
10048
  }
@@ -10078,7 +10073,9 @@ function render(h,s,q,pl){
10078
10073
  var tu=s.tokenUsage||{};
10079
10074
  var cache=tu.avgCacheHitRate!=null?Math.round(tu.avgCacheHitRate*100)+'%':'—';
10080
10075
  var items=[
10081
- ['Requests',String(s.totalRequests),s.errorCount>0?'red':'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors'],
10076
+ // The big number is the TOTAL — never error-colored (a red 1714 reads as
10077
+ // 1714 failures). The error signal lives on the detail line only.
10078
+ ['Requests',String(s.totalRequests),'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors',s.errorCount>0?'red':''],
10082
10079
  ['Tokens Out',tokens(tu.totalOutputTokens),'',tokens(tu.totalInputTokens)+' in'],
10083
10080
  ['Cache Hit',cache,tu.avgCacheHitRate>=0.5?'green':'','prompt cache'],
10084
10081
  ['Est. API Value',usd(s.costEstimate?.totalUsd),'','list prices'],
@@ -10217,7 +10214,7 @@ function classifyError(errMsg) {
10217
10214
  message: "Claude authentication expired or invalid. Run 'claude login' in your terminal to re-authenticate, then restart the proxy."
10218
10215
  };
10219
10216
  }
10220
- if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests")) {
10217
+ if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("hit your session limit") || lower.includes("usage limit reached")) {
10221
10218
  const hint = lower.includes("1m") || lower.includes("context") ? " If you're frequently hitting this, set MERIDIAN_SONNET_MODEL=sonnet to use the 200k model instead." : "";
10222
10219
  return {
10223
10220
  status: 429,
@@ -19854,6 +19851,127 @@ function createProxyServer(config = {}) {
19854
19851
  app.use("/settings/*", requireAuth);
19855
19852
  app.use("/settings", requireAuth);
19856
19853
  app.use("/design-login", requireAuth);
19854
+ const priorityExhaustion = new ProfileExhaustion;
19855
+ const priorityAssignments = new Map;
19856
+ const PRIORITY_ASSIGNMENTS_MAX = 5000;
19857
+ const PRIORITY_DEFAULT_COOLDOWN_MS = 10 * 60000;
19858
+ const PRIORITY_COOLDOWN_CAP_MS = 6 * 60 * 60000;
19859
+ function priorityProfileOrderSetting() {
19860
+ const env2 = process.env.MERIDIAN_PROFILE_ORDER;
19861
+ if (env2 && env2.trim())
19862
+ return env2.split(",").map((s) => s.trim()).filter(Boolean);
19863
+ const setting = getSetting("profileOrder");
19864
+ return Array.isArray(setting) && setting.length > 0 ? setting : undefined;
19865
+ }
19866
+ function priorityCooldownUntil(now) {
19867
+ const fiveHour = rateLimitStore.getAll().find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now);
19868
+ const until = fiveHour?.resetsAt ?? now + PRIORITY_DEFAULT_COOLDOWN_MS;
19869
+ return Math.min(until, now + PRIORITY_COOLDOWN_CAP_MS);
19870
+ }
19871
+ async function sniffQuotaFailure(res) {
19872
+ const contentType = res.headers.get("content-type") ?? "";
19873
+ if (!contentType.includes("text/event-stream")) {
19874
+ if (res.status === 429) {
19875
+ const body = await res.clone().json().catch(() => null);
19876
+ if (body?.error?.type === "rate_limit_error")
19877
+ return { failed: true, errorPayload: body, response: res };
19878
+ }
19879
+ return { failed: false, errorPayload: null, response: res };
19880
+ }
19881
+ const reader = res.body?.getReader();
19882
+ if (!reader)
19883
+ return { failed: false, errorPayload: null, response: res };
19884
+ const decoder = new TextDecoder;
19885
+ const consumed = [];
19886
+ let text = "";
19887
+ let failedPayload = null;
19888
+ while (true) {
19889
+ const { done, value } = await reader.read();
19890
+ if (done)
19891
+ break;
19892
+ consumed.push(value);
19893
+ text += decoder.decode(value, { stream: true });
19894
+ const frameEnd = text.indexOf(`
19895
+
19896
+ `);
19897
+ if (frameEnd === -1)
19898
+ continue;
19899
+ const frame = text.slice(0, frameEnd);
19900
+ if (/^event: error$/m.test(frame)) {
19901
+ const dataLine = frame.split(`
19902
+ `).find((l) => l.startsWith("data: "));
19903
+ try {
19904
+ const parsed = dataLine ? JSON.parse(dataLine.slice(6)) : null;
19905
+ if (parsed?.error?.type === "rate_limit_error") {
19906
+ failedPayload = parsed;
19907
+ }
19908
+ } catch {}
19909
+ }
19910
+ break;
19911
+ }
19912
+ if (failedPayload) {
19913
+ await reader.cancel().catch(() => {});
19914
+ return { failed: true, errorPayload: failedPayload, response: res };
19915
+ }
19916
+ const rest = new ReadableStream({
19917
+ start(ctrl) {
19918
+ for (const chunk of consumed)
19919
+ ctrl.enqueue(chunk);
19920
+ },
19921
+ async pull(ctrl) {
19922
+ const { done, value } = await reader.read();
19923
+ if (done)
19924
+ ctrl.close();
19925
+ else
19926
+ ctrl.enqueue(value);
19927
+ },
19928
+ cancel(reason) {
19929
+ reader.cancel(reason).catch(() => {});
19930
+ }
19931
+ });
19932
+ return { failed: false, errorPayload: null, response: new Response(rest, { status: res.status, headers: res.headers }) };
19933
+ }
19934
+ async function dispatchPriority(c, orderedCandidateIds, sessionKey, wantsStream) {
19935
+ const bodyBuf = await c.req.arrayBuffer();
19936
+ let lastError = null;
19937
+ let previous = null;
19938
+ for (const candidate of orderedCandidateIds) {
19939
+ const headers = new Headers(c.req.raw.headers);
19940
+ headers.set("x-meridian-profile", candidate);
19941
+ headers.set("x-meridian-priority-dispatch", "1");
19942
+ const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
19943
+ const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
19944
+ if (!failed) {
19945
+ if (sessionKey) {
19946
+ priorityAssignments.set(sessionKey, candidate);
19947
+ if (priorityAssignments.size > PRIORITY_ASSIGNMENTS_MAX) {
19948
+ const oldest = priorityAssignments.keys().next().value;
19949
+ if (oldest !== undefined)
19950
+ priorityAssignments.delete(oldest);
19951
+ }
19952
+ }
19953
+ if (previous) {
19954
+ claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
19955
+ plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
19956
+ }
19957
+ return response;
19958
+ }
19959
+ priorityExhaustion.mark(candidate, priorityCooldownUntil(Date.now()), "rate_limit_error");
19960
+ claudeLog("priority.exhausted", { profile: candidate, until: priorityCooldownUntil(Date.now()) });
19961
+ lastError = errorPayload;
19962
+ previous = candidate;
19963
+ }
19964
+ if (wantsStream) {
19965
+ return new Response(`event: error
19966
+ data: ${JSON.stringify(lastError)}
19967
+
19968
+ `, {
19969
+ status: 200,
19970
+ headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" }
19971
+ });
19972
+ }
19973
+ return new Response(JSON.stringify(lastError), { status: 429, headers: { "content-type": "application/json" } });
19974
+ }
19857
19975
  app.use("/auth/*", requireAuth);
19858
19976
  app.get("/", (c) => {
19859
19977
  const accept = c.req.header("accept") || "";
@@ -19921,6 +20039,25 @@ function createProxyServer(config = {}) {
19921
20039
  }
19922
20040
  const outputFormat = parsedOutputFormat.value;
19923
20041
  const routingMode = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
20042
+ if (routingMode === "priority" && !c.req.header("x-meridian-profile")) {
20043
+ const effectivePool = getEffectiveProfiles(finalConfig.profiles);
20044
+ if (effectivePool.length > 1) {
20045
+ const { order, unknown } = resolvePriorityOrder(effectivePool.map((p) => p.id), priorityProfileOrderSetting());
20046
+ if (unknown.length > 0)
20047
+ claudeLog("priority.unknown_order_ids", { unknown });
20048
+ const sessionKey = adapter.getSessionId(c, body) || null;
20049
+ const assigned = sessionKey ? priorityAssignments.get(sessionKey) : undefined;
20050
+ let first;
20051
+ if (assigned && order.includes(assigned) && !priorityExhaustion.isExhausted(assigned)) {
20052
+ first = assigned;
20053
+ } else {
20054
+ const pick = choosePriorityProfile(order, (id) => priorityExhaustion.isExhausted(id));
20055
+ first = pick?.id ?? order[0];
20056
+ }
20057
+ const candidates = [first, ...order.filter((id) => id !== first && !priorityExhaustion.isExhausted(id))];
20058
+ return dispatchPriority(c, candidates, sessionKey, body.stream === true);
20059
+ }
20060
+ }
19924
20061
  const profile = resolveProfile(finalConfig.profiles, finalConfig.defaultProfile, c.req.header("x-meridian-profile") || undefined, routingMode === "sticky" ? { routingMode, stickySessionKey: adapter.getSessionId(c, body) } : undefined);
19925
20062
  const authStatus = await getClaudeAuthStatusAsync(profile.id !== "default" ? profile.id : undefined, Object.keys(profile.env).length > 0 ? profile.env : undefined);
19926
20063
  const agentMode = c.req.header("x-opencode-agent-mode") ?? null;
@@ -20050,7 +20187,7 @@ function createProxyServer(config = {}) {
20050
20187
  const lineageType = lineageResult.type === "diverged" && !cachedSession ? "new" : lineageResult.type;
20051
20188
  const msgCount = Array.isArray(body.messages) ? body.messages.length : 0;
20052
20189
  const toolCount = body.tools?.length ?? 0;
20053
- const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""}${profile.id !== "default" ? ` profile=${profile.id}${routingMode === "sticky" ? "(sticky)" : ""}` : ""} model=${model} stream=${stream3} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
20190
+ const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""}${profile.id !== "default" ? ` profile=${profile.id}${routingMode === "sticky" ? "(sticky)" : c.req.header("x-meridian-priority-dispatch") ? "(priority)" : ""}` : ""} model=${model} stream=${stream3} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
20054
20191
  plog(`[PROXY] ${requestLogLine} msgs=${msgSummary}`);
20055
20192
  diagnosticLog2.session(`${requestLogLine}`, requestMeta.requestId);
20056
20193
  if (lineageResult.type === "diverged" && profileSessionId && !isIndependentSession) {
@@ -21948,6 +22085,44 @@ data: ${JSON.stringify({
21948
22085
  resetAdapterFeatures2(adapter);
21949
22086
  return c.json({ ok: true });
21950
22087
  });
22088
+ app.get("/settings/api/routing", (c) => {
22089
+ const profiles = listProfiles(finalConfig.profiles, finalConfig.defaultProfile);
22090
+ return c.json({
22091
+ routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing")),
22092
+ profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
22093
+ profiles: profiles.map((p) => p.id),
22094
+ envOverride: {
22095
+ routing: Boolean(process.env.MERIDIAN_ROUTING),
22096
+ profileOrder: Boolean(process.env.MERIDIAN_PROFILE_ORDER)
22097
+ }
22098
+ });
22099
+ });
22100
+ app.put("/settings/api/routing", async (c) => {
22101
+ let body;
22102
+ try {
22103
+ body = await c.req.json();
22104
+ } catch {
22105
+ return c.json({ error: "Invalid JSON" }, 400);
22106
+ }
22107
+ if (body.routing !== undefined) {
22108
+ if (typeof body.routing !== "string" || !["active", "sticky", "priority"].includes(body.routing)) {
22109
+ return c.json({ error: 'routing must be "active", "sticky", or "priority"' }, 400);
22110
+ }
22111
+ setSetting("routing", body.routing);
22112
+ }
22113
+ if (body.profileOrder !== undefined) {
22114
+ if (!Array.isArray(body.profileOrder) || body.profileOrder.some((x) => typeof x !== "string")) {
22115
+ return c.json({ error: "profileOrder must be an array of profile ids" }, 400);
22116
+ }
22117
+ const known = new Set(listProfiles(finalConfig.profiles, finalConfig.defaultProfile).map((p) => p.id));
22118
+ const unknown = body.profileOrder.filter((id) => !known.has(id));
22119
+ if (unknown.length > 0)
22120
+ return c.json({ error: `Unknown profiles: ${unknown.join(", ")}` }, 400);
22121
+ setSetting("profileOrder", body.profileOrder);
22122
+ }
22123
+ plog(`[PROXY] Routing settings updated: routing=${getSetting("routing") ?? "active"} order=${(getSetting("profileOrder") ?? []).join(",") || "(config order)"}`);
22124
+ return c.json({ success: true });
22125
+ });
21951
22126
  app.get("/settings/api/pricing", (c) => {
21952
22127
  const { BUILTIN_MODEL_PRICING: BUILTIN_MODEL_PRICING2 } = (init_pricing(), __toCommonJS(exports_pricing));
21953
22128
  const { getPricingOverrides: getPricingOverrides2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
@@ -22038,10 +22213,16 @@ data: ${JSON.stringify({
22038
22213
  lastSuccessAt: cacheInfo.lastSuccessAt || null
22039
22214
  };
22040
22215
  }));
22216
+ const routingModeNow = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
22217
+ const priorityInfo = routingModeNow === "priority" ? {
22218
+ profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
22219
+ exhausted: priorityExhaustion.snapshot()
22220
+ } : {};
22041
22221
  return c.json({
22042
22222
  profiles: enriched,
22043
22223
  activeProfile: getActiveProfileId() || finalConfig.defaultProfile || profiles[0]?.id || "default",
22044
- routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"))
22224
+ routing: routingModeNow,
22225
+ ...priorityInfo
22045
22226
  });
22046
22227
  });
22047
22228
  app.get("/profiles", async (c) => {
@@ -22065,10 +22246,17 @@ data: ${JSON.stringify({
22065
22246
  if (!effective.find((p) => p.id === body.profile)) {
22066
22247
  return c.json({ error: `Unknown profile: ${body.profile}. Available: ${effective.map((p) => p.id).join(", ")}` }, 400);
22067
22248
  }
22249
+ const previousProfile = getActiveProfileId() ?? null;
22068
22250
  setActiveProfile(body.profile);
22069
22251
  clearSessionCache();
22070
22252
  rateLimitStore.clear();
22071
- plog(`[PROXY] Active profile switched to: ${body.profile} (session + rate-limit caches cleared)`);
22253
+ claudeLog("profile.switched", {
22254
+ from: previousProfile,
22255
+ to: body.profile,
22256
+ userAgent: c.req.header("user-agent")?.slice(0, 120) ?? null,
22257
+ origin: c.req.header("origin") ?? c.req.header("referer")?.slice(0, 120) ?? null
22258
+ });
22259
+ plog(`[PROXY] Active profile switched to: ${body.profile} (from ${previousProfile ?? "unset"}, ua: ${(c.req.header("user-agent") || "unknown").slice(0, 60)}) (session + rate-limit caches cleared)`);
22072
22260
  return c.json({ success: true, activeProfile: body.profile });
22073
22261
  });
22074
22262
  app.get("/plugins/list", async (c) => {
@@ -11,7 +11,12 @@ import { homedir } from "node:os";
11
11
  // src/proxy/routing.ts
12
12
  import { createHash } from "node:crypto";
13
13
  function getRoutingMode(raw) {
14
- return raw?.toLowerCase() === "sticky" ? "sticky" : "active";
14
+ const lower = raw?.toLowerCase();
15
+ if (lower === "sticky")
16
+ return "sticky";
17
+ if (lower === "priority")
18
+ return "priority";
19
+ return "active";
15
20
  }
16
21
  function rendezvousScore(sessionKey, profileId) {
17
22
  const digest = createHash("sha256").update(`${sessionKey}\x00${profileId}`).digest();
@@ -31,6 +36,67 @@ function pickStickyProfile(sessionKey, profileIds) {
31
36
  }
32
37
  return best;
33
38
  }
39
+ function resolvePriorityOrder(configuredIds, orderSetting) {
40
+ const existing = new Set(configuredIds);
41
+ const order = [];
42
+ const unknown = [];
43
+ for (const id of orderSetting ?? []) {
44
+ if (!existing.has(id)) {
45
+ unknown.push(id);
46
+ continue;
47
+ }
48
+ if (!order.includes(id))
49
+ order.push(id);
50
+ }
51
+ for (const id of configuredIds)
52
+ if (!order.includes(id))
53
+ order.push(id);
54
+ return { order, unknown };
55
+ }
56
+ function choosePriorityProfile(order, isExhausted) {
57
+ if (order.length === 0)
58
+ return;
59
+ for (const id of order) {
60
+ if (!isExhausted(id))
61
+ return { id, allExhausted: false };
62
+ }
63
+ return { id: order[0], allExhausted: true };
64
+ }
65
+
66
+ class ProfileExhaustion {
67
+ now;
68
+ marks = new Map;
69
+ constructor(now = Date.now) {
70
+ this.now = now;
71
+ }
72
+ mark(id, until, reason) {
73
+ const existing = this.marks.get(id);
74
+ if (existing && existing.until >= until)
75
+ return;
76
+ this.marks.set(id, { until, reason });
77
+ }
78
+ isExhausted(id) {
79
+ const entry = this.marks.get(id);
80
+ if (!entry)
81
+ return false;
82
+ if (entry.until <= this.now()) {
83
+ this.marks.delete(id);
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+ snapshot() {
89
+ const out = [];
90
+ for (const [id, entry] of this.marks) {
91
+ if (entry.until <= this.now()) {
92
+ this.marks.delete(id);
93
+ continue;
94
+ }
95
+ out.push({ id, until: entry.until, reason: entry.reason });
96
+ }
97
+ return out;
98
+ }
99
+ }
34
100
 
35
101
  // src/proxy/profiles.ts
36
102
  var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
@@ -147,4 +213,4 @@ function listProfiles(profiles, defaultProfile) {
147
213
  }));
148
214
  }
149
215
 
150
- export { getRoutingMode, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
216
+ export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-3p1793vq.js";
5
- import"./cli-f0yqy2d2.js";
4
+ } from "./cli-h3c4qrv6.js";
5
+ import"./cli-ngtexmne.js";
6
6
  import"./cli-sry5aqdj.js";
7
7
  import"./cli-xmweegb1.js";
8
8
  import {
@@ -171,7 +171,7 @@ async function runCli(start = startProxyServer, runAuthCheck = async () => {
171
171
  console.error("\x1B[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1B[0m");
172
172
  }
173
173
  if (!profiles) {
174
- const { enableDiskProfileDiscovery } = await import("./profiles-84hnbd6c.js");
174
+ const { enableDiskProfileDiscovery } = await import("./profiles-sq9t3fh9.js");
175
175
  enableDiskProfileDiscovery();
176
176
  }
177
177
  const proxy = await start({ port, host, idleTimeoutSeconds, pluginDir, pluginConfigPath, profiles, defaultProfile, version, installProcessErrorHandlers: true });
@@ -9,7 +9,7 @@ import {
9
9
  resolveProfile,
10
10
  restoreActiveProfile,
11
11
  setActiveProfile
12
- } from "./cli-f0yqy2d2.js";
12
+ } from "./cli-ngtexmne.js";
13
13
  import"./cli-340h1chz.js";
14
14
  import"./cli-p9swy5t3.js";
15
15
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA+G7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAA;IAC5D,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAuCpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA,MAAM,CAYR"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAoH7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAA;IAC5D,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAuCpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA,MAAM,CAYR"}
@@ -36,12 +36,17 @@ export interface OAuthUsageSnapshot {
36
36
  windows: OAuthUsageWindow[];
37
37
  extraUsage: OAuthExtraUsageInfo | null;
38
38
  fetchedAt: number;
39
+ /** Set when this is a previous snapshot served because a fresh fetch
40
+ * failed transiently (credential-read blip, upstream error). */
41
+ stale?: boolean;
39
42
  }
40
43
  /** Minimal fetch shape used by callAnthropic. Avoids `typeof fetch`'s
41
44
  * `preconnect` property, which makes test casts unwieldy. */
42
45
  type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
43
46
  export interface FetchOAuthUsageOpts {
44
47
  ttlMs?: number;
48
+ /** Max age of a last-good snapshot served on fetch failure (default 15 min). */
49
+ staleMaxMs?: number;
45
50
  force?: boolean;
46
51
  store?: CredentialStore;
47
52
  profileId?: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"oauthUsage.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAoD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AA6CvG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AAyFD;8DAC8D;AAC9D,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAmBzE,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAcD,8CAA8C;AAC9C,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,GAC9E,IAAI,CAEN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpG;AAqDD,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
1
+ {"version":3,"file":"oauthUsage.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAoD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AA6CvG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB;qEACiE;IACjE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AA6FD;8DAC8D;AAC9D,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAmBzE,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAcD,8CAA8C;AAC9C,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,GAC9E,IAAI,CAEN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpG;AAwED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * This is a leaf module — pure functions, no I/O.
21
21
  */
22
- export type RoutingMode = "active" | "sticky";
22
+ export type RoutingMode = "active" | "sticky" | "priority";
23
23
  /**
24
24
  * Parse a routing mode string (from settings or MERIDIAN_ROUTING).
25
25
  * Unknown values fall back to "active" — a typo must never change
@@ -41,4 +41,45 @@ export declare function pickStickyProfile(sessionKey: string, profileIds: readon
41
41
  * to update casually.
42
42
  */
43
43
  export declare const RENDEZVOUS_STABLE_GUARD: ReadonlyArray<readonly [string, readonly string[], string]>;
44
+ /**
45
+ * Resolve the effective pool order: the configured order (settings
46
+ * "profileOrder" / MERIDIAN_PROFILE_ORDER) filtered to profiles that exist,
47
+ * with unlisted profiles appended in config order. Unknown ids are returned
48
+ * for a startup warning — a typo must never silently drop an account.
49
+ */
50
+ export declare function resolvePriorityOrder(configuredIds: readonly string[], orderSetting: readonly string[] | undefined): {
51
+ order: string[];
52
+ unknown: string[];
53
+ };
54
+ /**
55
+ * Pick the highest-priority profile that isn't exhausted. When every pool
56
+ * member is exhausted, return the preferred (first) profile with the flag
57
+ * set — callers still attempt it (marks may be stale), and per the design
58
+ * decision the LAST tried profile's error is what ultimately surfaces.
59
+ */
60
+ export declare function choosePriorityProfile(order: readonly string[], isExhausted: (id: string) => boolean): {
61
+ id: string;
62
+ allExhausted: boolean;
63
+ } | undefined;
64
+ export interface ExhaustionEntry {
65
+ id: string;
66
+ until: number;
67
+ reason: string;
68
+ }
69
+ /**
70
+ * In-memory per-profile exhaustion marks with expiry. Deliberately not
71
+ * persisted: this is routing hygiene, not durable truth — after a restart
72
+ * the first failing request re-marks. A later mark may extend an entry but
73
+ * an earlier one never shortens it (two concurrent failures shouldn't
74
+ * un-learn the longer reset).
75
+ */
76
+ export declare class ProfileExhaustion {
77
+ private readonly now;
78
+ private readonly marks;
79
+ constructor(now?: () => number);
80
+ mark(id: string, until: number, reason: string): void;
81
+ isExhausted(id: string): boolean;
82
+ /** Live entries only — expired marks are dropped on read. */
83
+ snapshot(): ExhaustionEntry[];
84
+ }
44
85
  //# sourceMappingURL=routing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE7C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAEnE;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAYvG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAM/F,CAAA"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAA;AAE1D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAKnE;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAYvG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAM/F,CAAA;AAOD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAC1C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAUxC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACnC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAMnD;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuD;gBAChD,GAAG,GAAE,MAAM,MAAiB;IAEzD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMrD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAUhC,6DAA6D;IAC7D,QAAQ,IAAI,eAAe,EAAE;CAQ9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,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;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAysHhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,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;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAs5HhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
@@ -10,9 +10,12 @@
10
10
  export interface MeridianSettings {
11
11
  /** Last active profile ID — restored on proxy startup */
12
12
  activeProfile?: string;
13
- /** Profile routing mode (#383): "active" (default) or "sticky".
14
- * MERIDIAN_ROUTING env var takes precedence when set. */
13
+ /** Profile routing mode (#383, priority spec): "active" (default),
14
+ * "sticky", or "priority". MERIDIAN_ROUTING env var takes precedence. */
15
15
  routing?: string;
16
+ /** Priority-mode pool order (highest priority first). Falls back to
17
+ * profiles.json order. MERIDIAN_PROFILE_ORDER env var takes precedence. */
18
+ profileOrder?: string[];
16
19
  }
17
20
  /** Read settings from disk. Returns empty object if file doesn't exist or is invalid. */
18
21
  export declare function loadSettings(): MeridianSettings;
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAO/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASrE;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAErG"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8EAC0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;gFAC4E;IAC5E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAO/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASrE;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAErG"}
package/dist/server.js CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  runObserveHook,
12
12
  runTransformHook,
13
13
  startProxyServer
14
- } from "./cli-3p1793vq.js";
15
- import"./cli-f0yqy2d2.js";
14
+ } from "./cli-h3c4qrv6.js";
15
+ import"./cli-ngtexmne.js";
16
16
  import"./cli-sry5aqdj.js";
17
17
  import"./cli-xmweegb1.js";
18
18
  import"./cli-jhm27q0x.js";
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QAkhBlB,CAAA"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/telemetry/dashboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,aAAa,QAyZlB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,WAAW,QAkNhB,CAAA"}
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/telemetry/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,WAAW,QAuQhB,CAAA"}
@@ -2,5 +2,5 @@
2
2
  * SDK Features settings page — per-adapter toggle UI.
3
3
  * Same dark theme as the telemetry dashboard. No framework, no CDN.
4
4
  */
5
- export declare const settingsPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Meridian \u2014 SDK Features</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n \n :root {\n /* Cool-gray neutral palette. High contrast, surface/border separation,\n no color cast muddying the text. Blue is the primary accent; violet\n is the fixed secondary \u2014 used together in the brand gradient and\n individually for hover states and a handful of telemetry badges. */\n --bg: #0d1117;\n --surface: #161b22;\n --surface2: #1c2128;\n --border: #30363d;\n /* Text */\n --text: #e6edf3;\n --muted: #8b949e;\n /* Brand \u2014 blue primary, violet secondary */\n --accent: #58a6ff;\n --accent2: #bc8cff;\n --violet: #bc8cff;\n --lavender: #d2a8ff;\n /* Semantic */\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n /* Telemetry-specific aliases (waterfall + lineage badges) */\n --blue: #58a6ff;\n --purple: #bc8cff;\n --queue: #d29922;\n --ttfb: #58a6ff;\n --upstream: #3fb950;\n }\n /* Banner backsplash \u2014 the brand look: a gentle diagonal wash with soft\n blue (top-left) and violet (bottom-right) glows. Pages must not set\n their own body background so this shows through everywhere. */\n body {\n background:\n radial-gradient(1200px 800px at 12% -8%, rgba(88,166,255,0.07), transparent 60%),\n radial-gradient(1100px 800px at 92% 108%, rgba(188,140,255,0.06), transparent 60%),\n linear-gradient(135deg, #0d1117 0%, #10151d 55%, #161b22 100%);\n background-attachment: fixed;\n background-color: var(--bg);\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-header {\n position: sticky; top: 0; z-index: 100;\n display: flex; align-items: center; gap: 20px;\n padding: 10px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n }\n .meridian-header .mh-brand {\n display: flex; align-items: center; gap: 10px;\n text-decoration: none; color: var(--text, #e6edf3);\n }\n .meridian-header .mh-logo { display: block; }\n .meridian-header .mh-name {\n font-size: 15px; font-weight: 700; letter-spacing: 2px;\n text-transform: uppercase;\n }\n .meridian-header .mh-nav { display: flex; align-items: center; gap: 2px; }\n .meridian-header .mh-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 12px;\n font-weight: 500; padding: 5px 10px; border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n }\n .meridian-header .mh-nav a:hover { color: var(--text, #e6edf3); background: var(--surface, #161b22); }\n .meridian-header .mh-nav a.active { color: var(--accent, #58a6ff); background: var(--surface, #161b22); }\n .meridian-header .mh-right {\n margin-left: auto; display: flex; align-items: center; gap: 10px;\n }\n .meridian-header .mh-profile {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; color: var(--text, #e6edf3);\n padding: 3px 10px; border-radius: 20px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n text-decoration: none; transition: border-color 0.15s;\n }\n .meridian-header .mh-profile:hover { border-color: var(--accent, #58a6ff); }\n .meridian-header .mh-profile.visible { display: inline-flex; }\n .meridian-header .mh-profile .mh-profile-type {\n color: var(--muted, #8b949e); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n\n .content { max-width: 900px; margin: 0 auto; padding: 24px; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .nav { display: flex; gap: 16px; margin-bottom: 24px; font-size: 13px; }\n .nav a { color: var(--muted); text-decoration: none; }\n .nav a:hover { color: var(--accent); }\n .nav a.active { color: var(--accent); }\n\n .adapter-card {\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 20px; margin-bottom: 16px;\n }\n .adapter-header {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 16px;\n }\n .adapter-name { font-size: 16px; font-weight: 600; }\n .adapter-badge {\n font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .badge-active { background: rgba(63, 185, 80, 0.15); color: var(--green); }\n .badge-inactive { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n\n .feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n @media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }\n\n .feature-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: 6px;\n background: var(--bg); border: 1px solid var(--border);\n }\n .feature-info { display: flex; flex-direction: column; }\n .feature-label { font-size: 13px; font-weight: 500; }\n .feature-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n\n /* Toggle switch */\n .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .toggle-track {\n position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;\n background: var(--border); border-radius: 10px; transition: background 0.2s;\n }\n .toggle-track::after {\n content: \"\"; position: absolute; height: 14px; width: 14px;\n left: 3px; bottom: 3px; background: var(--muted); border-radius: 50%;\n transition: transform 0.2s, background 0.2s;\n }\n .toggle input:checked + .toggle-track { background: var(--accent); }\n .toggle input:checked + .toggle-track::after {\n transform: translateX(16px); background: var(--text);\n }\n\n /* Select dropdown */\n .feature-select {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer;\n }\n\n .save-indicator {\n position: fixed; bottom: 24px; right: 24px;\n background: var(--green); color: #000; padding: 8px 16px;\n border-radius: 6px; font-size: 13px; font-weight: 500;\n opacity: 0; transition: opacity 0.3s; pointer-events: none;\n }\n .save-indicator.visible { opacity: 1; }\n\n .reset-btn {\n background: none; border: 1px solid var(--border); color: var(--muted);\n border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;\n }\n .reset-btn:hover { border-color: var(--red); color: var(--red); }\n\n /* Model pricing */\n .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }\n .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }\n .pricing-table tr:last-child td { border-bottom: none; }\n .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }\n .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;\n font-variant-numeric: tabular-nums; }\n .pricing-input:focus { border-color: var(--accent); outline: none; }\n .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }\n .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }\n .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;\n padding-top: 14px; border-top: 1px solid var(--border); }\n .pricing-add input[type=\"text\"] { width: 240px; text-align: left; }\n .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;\n padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }\n .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }\n</style>\n</head>\n<body>\n\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\">\n <svg class=\"mh-logo\" width=\"24\" height=\"24\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <defs>\n <linearGradient id=\"mhGrad\" x1=\"32\" y1=\"4\" x2=\"32\" y2=\"60\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#58a6ff\"/>\n <stop offset=\"1\" stop-color=\"#bc8cff\"/>\n </linearGradient>\n </defs>\n <circle cx=\"32\" cy=\"32\" r=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"3.5\"/>\n <ellipse cx=\"32\" cy=\"32\" rx=\"10.5\" ry=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"2.5\" opacity=\"0.8\"/>\n <path d=\"M7 32h50\" stroke=\"url(#mhGrad)\" stroke-width=\"2\" opacity=\"0.4\"/>\n <circle cx=\"32\" cy=\"7\" r=\"4.5\" fill=\"#58a6ff\"/>\n <circle cx=\"32\" cy=\"57\" r=\"4.5\" fill=\"#bc8cff\"/>\n</svg>\n <span class=\"mh-name\">Meridian</span>\n </a>\n <nav class=\"mh-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </nav>\n <div class=\"mh-right\">\n <a class=\"mh-profile\" id=\"mhProfile\" href=\"/\" title=\"Active profile \u2014 switch from the home page\"></a>\n <span class=\"mh-status\" id=\"mhStatus\"><span class=\"mh-dot\" id=\"mhDot\"></span><span class=\"mh-status-text\" id=\"mhStatusText\"></span></span>\n </div>\n</header>\n\n<div class=\"content\">\n <h1>SDK Features <span style=\"font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(210,153,34,0.15);color:var(--yellow);vertical-align:middle;margin-left:8px\">Experimental</span></h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Unlock Claude Code features for any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md \u2014 normally\n exclusive to Claude Code \u2014 become available to OpenCode, Crush, Droid, and any other harness routed through Meridian.\n Each agent keeps its own toolchain while gaining access to these additional features.<br><br>\n <strong style=\"color:var(--text)\">System prompts:</strong> For these features to work correctly, both the Claude Code prompt and your client prompt\n should be enabled. When both are active, they are appended together \u2014 Claude Code's base instructions come first,\n followed by your agent's specific instructions.\n </p>\n\n <div id=\"adapters\"></div>\n\n <h1 style=\"margin-top:40px\">Model Pricing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the\n built-in rate, or add models the built-in table doesn't know about (they show as \"no pricing\" on the\n dashboard until defined here). Changes apply on the next dashboard refresh.\n </p>\n <div class=\"adapter-card\">\n <table class=\"pricing-table\">\n <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>\n <tbody id=\"pricingRows\"></tbody>\n </table>\n <div class=\"pricing-add\">\n <input type=\"text\" class=\"pricing-input\" id=\"newModelName\" placeholder=\"model id (e.g. claude-opus-9)\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelInput\" placeholder=\"input\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelOutput\" placeholder=\"output\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheRead\" placeholder=\"cache read\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheWrite\" placeholder=\"cache write\" min=\"0\" step=\"0.01\">\n <button class=\"add-btn\" onclick=\"addPricingModel()\">Add Model</button>\n </div>\n <div class=\"pricing-note\">\n Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the\n input rate (the 5-minute cache TTL multipliers). Verify current list prices at\n <a href=\"https://claude.com/pricing\" target=\"_blank\" rel=\"noreferrer\" style=\"color:var(--accent)\">claude.com/pricing</a>.\n </div>\n </div>\n</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '&quot;');\n row.innerHTML = info +\n '<input type=\"text\" class=\"feature-select\" style=\"width:180px\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">';\n }\n\n grid.appendChild(row);\n }\n\n card.appendChild(grid);\n container.appendChild(card);\n }\n}\n\n// ---- Model pricing (telemetry cost estimate) ----\nlet pricingData = { builtin: {}, overrides: {} };\n\nfunction fmtRate(v) { return String(Math.round(v * 10000) / 10000); }\n\nasync function loadPricing() {\n const res = await fetch('/settings/api/pricing');\n pricingData = await res.json();\n renderPricing();\n}\n\nasync function putPricing(model, rates) {\n const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(rates),\n });\n if (!res.ok) {\n const err = await res.json().catch(function () { return {}; });\n alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));\n return false;\n }\n showSaved();\n return true;\n}\n\nasync function removePricing(model) {\n await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });\n showSaved();\n await loadPricing();\n}\n\nfunction rateCell(value, onCommit) {\n const td = document.createElement('td');\n const input = document.createElement('input');\n input.type = 'number';\n input.min = '0';\n input.step = '0.01';\n input.className = 'pricing-input';\n input.value = fmtRate(value);\n input.addEventListener('change', onCommit);\n td.appendChild(input);\n return { td: td, input: input };\n}\n\nfunction renderPricing() {\n const tbody = document.getElementById('pricingRows');\n tbody.innerHTML = '';\n const models = Array.from(new Set(\n Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))\n )).sort();\n\n for (const model of models) {\n const override = pricingData.overrides[model];\n const effective = override || pricingData.builtin[model];\n const tr = document.createElement('tr');\n\n const nameTd = document.createElement('td');\n nameTd.className = 'pricing-model';\n nameTd.textContent = model;\n tr.appendChild(nameTd);\n\n const cells = [];\n const commit = async function () {\n const rates = {\n inputPerMTok: parseFloat(cells[0].input.value),\n outputPerMTok: parseFloat(cells[1].input.value),\n cacheReadPerMTok: parseFloat(cells[2].input.value),\n cacheWritePerMTok: parseFloat(cells[3].input.value),\n };\n for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }\n if (await putPricing(model, rates)) await loadPricing();\n };\n ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {\n const cell = rateCell(effective[key], commit);\n cells.push(cell);\n tr.appendChild(cell.td);\n });\n\n const badgeTd = document.createElement('td');\n const badge = document.createElement('span');\n badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');\n badge.textContent = override ? 'Override' : 'Built-in';\n badgeTd.appendChild(badge);\n tr.appendChild(badgeTd);\n\n const actionTd = document.createElement('td');\n if (override) {\n const btn = document.createElement('button');\n btn.className = 'reset-btn';\n btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';\n btn.addEventListener('click', function () { removePricing(model); });\n actionTd.appendChild(btn);\n }\n tr.appendChild(actionTd);\n\n tbody.appendChild(tr);\n }\n}\n\nasync function addPricingModel() {\n const name = document.getElementById('newModelName').value.trim();\n const inputRate = parseFloat(document.getElementById('newModelInput').value);\n const outputRate = parseFloat(document.getElementById('newModelOutput').value);\n if (!name) { alert('Enter a model id'); return; }\n if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }\n const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };\n const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);\n const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);\n if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;\n if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;\n if (await putPricing(name, rates)) {\n ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {\n document.getElementById(id).value = '';\n });\n await loadPricing();\n }\n}\n\nloadConfig();\nloadPricing();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n\n</script>\n</body>\n</html>";
5
+ export declare const settingsPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Meridian \u2014 SDK Features</title>\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/telemetry/icon.svg\">\n<style>\n \n :root {\n /* Cool-gray neutral palette. High contrast, surface/border separation,\n no color cast muddying the text. Blue is the primary accent; violet\n is the fixed secondary \u2014 used together in the brand gradient and\n individually for hover states and a handful of telemetry badges. */\n --bg: #0d1117;\n --surface: #161b22;\n --surface2: #1c2128;\n --border: #30363d;\n /* Text */\n --text: #e6edf3;\n --muted: #8b949e;\n /* Brand \u2014 blue primary, violet secondary */\n --accent: #58a6ff;\n --accent2: #bc8cff;\n --violet: #bc8cff;\n --lavender: #d2a8ff;\n /* Semantic */\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n /* Telemetry-specific aliases (waterfall + lineage badges) */\n --blue: #58a6ff;\n --purple: #bc8cff;\n --queue: #d29922;\n --ttfb: #58a6ff;\n --upstream: #3fb950;\n }\n /* Banner backsplash \u2014 the brand look: a gentle diagonal wash with soft\n blue (top-left) and violet (bottom-right) glows. Pages must not set\n their own body background so this shows through everywhere. */\n body {\n background:\n radial-gradient(1200px 800px at 12% -8%, rgba(88,166,255,0.07), transparent 60%),\n radial-gradient(1100px 800px at 92% 108%, rgba(188,140,255,0.06), transparent 60%),\n linear-gradient(135deg, #0d1117 0%, #10151d 55%, #161b22 100%);\n background-attachment: fixed;\n background-color: var(--bg);\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n color: var(--text); padding: 0; line-height: 1.5; }\n \n .meridian-header {\n position: sticky; top: 0; z-index: 100;\n display: flex; align-items: center; gap: 20px;\n padding: 10px 24px;\n background: rgba(13, 17, 23, 0.92);\n backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border, #30363d);\n }\n .meridian-header .mh-brand {\n display: flex; align-items: center; gap: 10px;\n text-decoration: none; color: var(--text, #e6edf3);\n }\n .meridian-header .mh-logo { display: block; }\n .meridian-header .mh-name {\n font-size: 15px; font-weight: 700; letter-spacing: 2px;\n text-transform: uppercase;\n }\n .meridian-header .mh-nav { display: flex; align-items: center; gap: 2px; }\n .meridian-header .mh-nav a {\n color: var(--muted, #8b949e); text-decoration: none; font-size: 12px;\n font-weight: 500; padding: 5px 10px; border-radius: 6px;\n transition: color 0.15s, background 0.15s;\n }\n .meridian-header .mh-nav a:hover { color: var(--text, #e6edf3); background: var(--surface, #161b22); }\n .meridian-header .mh-nav a.active { color: var(--accent, #58a6ff); background: var(--surface, #161b22); }\n .meridian-header .mh-right {\n margin-left: auto; display: flex; align-items: center; gap: 10px;\n }\n .meridian-header .mh-profile {\n display: none; align-items: center; gap: 6px;\n font-size: 11px; font-weight: 500; color: var(--text, #e6edf3);\n padding: 3px 10px; border-radius: 20px;\n background: var(--surface, #161b22); border: 1px solid var(--border, #30363d);\n text-decoration: none; transition: border-color 0.15s;\n }\n .meridian-header .mh-profile:hover { border-color: var(--accent, #58a6ff); }\n .meridian-header .mh-profile.visible { display: inline-flex; }\n .meridian-header .mh-profile .mh-profile-type {\n color: var(--muted, #8b949e); font-size: 10px;\n }\n .meridian-header .mh-status {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--muted, #8b949e); white-space: nowrap;\n }\n .meridian-header .mh-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--muted, #8b949e); flex-shrink: 0;\n }\n .meridian-header .mh-dot.healthy { background: var(--green, #3fb950); box-shadow: 0 0 6px rgba(63,185,80,0.5); }\n .meridian-header .mh-dot.degraded { background: var(--yellow, #d29922); }\n .meridian-header .mh-dot.unhealthy { background: var(--red, #f85149); }\n @media (max-width: 720px) {\n .meridian-header { gap: 10px; padding: 10px 16px; flex-wrap: wrap; }\n .meridian-header .mh-name { display: none; }\n .meridian-header .mh-status .mh-status-text { display: none; }\n }\n\n .content { max-width: 900px; margin: 0 auto; padding: 24px; }\n h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }\n .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 24px; }\n .nav { display: flex; gap: 16px; margin-bottom: 24px; font-size: 13px; }\n .nav a { color: var(--muted); text-decoration: none; }\n .nav a:hover { color: var(--accent); }\n .nav a.active { color: var(--accent); }\n\n .adapter-card {\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 20px; margin-bottom: 16px;\n }\n .adapter-header {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 16px;\n }\n .adapter-name { font-size: 16px; font-weight: 600; }\n .adapter-badge {\n font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .badge-active { background: rgba(63, 185, 80, 0.15); color: var(--green); }\n .badge-inactive { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n\n .feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n @media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }\n\n .feature-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 10px 14px; border-radius: 6px;\n background: var(--bg); border: 1px solid var(--border);\n }\n .feature-info { display: flex; flex-direction: column; }\n .feature-label { font-size: 13px; font-weight: 500; }\n .feature-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n\n /* Toggle switch */\n .toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .toggle-track {\n position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;\n background: var(--border); border-radius: 10px; transition: background 0.2s;\n }\n .toggle-track::after {\n content: \"\"; position: absolute; height: 14px; width: 14px;\n left: 3px; bottom: 3px; background: var(--muted); border-radius: 50%;\n transition: transform 0.2s, background 0.2s;\n }\n .toggle input:checked + .toggle-track { background: var(--accent); }\n .toggle input:checked + .toggle-track::after {\n transform: translateX(16px); background: var(--text);\n }\n\n /* Select dropdown */\n .feature-select {\n background: var(--surface); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer;\n }\n\n .save-indicator {\n position: fixed; bottom: 24px; right: 24px;\n background: var(--green); color: #000; padding: 8px 16px;\n border-radius: 6px; font-size: 13px; font-weight: 500;\n opacity: 0; transition: opacity 0.3s; pointer-events: none;\n }\n .save-indicator.visible { opacity: 1; }\n\n .reset-btn {\n background: none; border: 1px solid var(--border); color: var(--muted);\n border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer;\n }\n .reset-btn:hover { border-color: var(--red); color: var(--red); }\n\n /* Model pricing */\n .pricing-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n .pricing-table th { text-align: left; padding: 8px 10px; color: var(--muted); font-weight: 500;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }\n .pricing-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }\n .pricing-table tr:last-child td { border-bottom: none; }\n .pricing-model { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-all; }\n .pricing-input { background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 12px; width: 84px; text-align: right;\n font-variant-numeric: tabular-nums; }\n .pricing-input:focus { border-color: var(--accent); outline: none; }\n .pricing-badge { font-size: 10px; padding: 2px 8px; border-radius: 10px;\n text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }\n .badge-override { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }\n .badge-builtin { background: rgba(139, 148, 158, 0.15); color: var(--muted); }\n .pricing-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px;\n padding-top: 14px; border-top: 1px solid var(--border); }\n .pricing-add input[type=\"text\"] { width: 240px; text-align: left; }\n .add-btn { background: var(--accent); border: none; color: #fff; border-radius: 6px;\n padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }\n .pricing-note { font-size: 11px; color: var(--muted); margin-top: 12px; line-height: 1.6; }\n</style>\n</head>\n<body>\n\n<header class=\"meridian-header\" id=\"meridianHeader\">\n <a class=\"mh-brand\" href=\"/\">\n <svg class=\"mh-logo\" width=\"24\" height=\"24\" viewBox=\"0 0 64 64\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <defs>\n <linearGradient id=\"mhGrad\" x1=\"32\" y1=\"4\" x2=\"32\" y2=\"60\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#58a6ff\"/>\n <stop offset=\"1\" stop-color=\"#bc8cff\"/>\n </linearGradient>\n </defs>\n <circle cx=\"32\" cy=\"32\" r=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"3.5\"/>\n <ellipse cx=\"32\" cy=\"32\" rx=\"10.5\" ry=\"25\" stroke=\"url(#mhGrad)\" stroke-width=\"2.5\" opacity=\"0.8\"/>\n <path d=\"M7 32h50\" stroke=\"url(#mhGrad)\" stroke-width=\"2\" opacity=\"0.4\"/>\n <circle cx=\"32\" cy=\"7\" r=\"4.5\" fill=\"#58a6ff\"/>\n <circle cx=\"32\" cy=\"57\" r=\"4.5\" fill=\"#bc8cff\"/>\n</svg>\n <span class=\"mh-name\">Meridian</span>\n </a>\n <nav class=\"mh-nav\">\n <a href=\"/\" id=\"nav-home\">Home</a>\n <a href=\"/telemetry\" id=\"nav-telemetry\">Telemetry</a>\n <a href=\"/profiles\" id=\"nav-profiles\">Profiles</a>\n <a href=\"/settings\" id=\"nav-settings\">Settings</a>\n <a href=\"/plugins\" id=\"nav-plugins\">Plugins</a>\n </nav>\n <div class=\"mh-right\">\n <a class=\"mh-profile\" id=\"mhProfile\" href=\"/\" title=\"Active profile \u2014 switch from the home page\"></a>\n <span class=\"mh-status\" id=\"mhStatus\"><span class=\"mh-dot\" id=\"mhDot\"></span><span class=\"mh-status-text\" id=\"mhStatusText\"></span></span>\n </div>\n</header>\n\n<div class=\"content\">\n <h1>SDK Features <span style=\"font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(210,153,34,0.15);color:var(--yellow);vertical-align:middle;margin-left:8px\">Experimental</span></h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Unlock Claude Code features for any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md \u2014 normally\n exclusive to Claude Code \u2014 become available to OpenCode, Crush, Droid, and any other harness routed through Meridian.\n Each agent keeps its own toolchain while gaining access to these additional features.<br><br>\n <strong style=\"color:var(--text)\">System prompts:</strong> For these features to work correctly, both the Claude Code prompt and your client prompt\n should be enabled. When both are active, they are appended together \u2014 Claude Code's base instructions come first,\n followed by your agent's specific instructions.\n </p>\n\n <div id=\"adapters\"></div>\n\n <h1 style=\"margin-top:40px\">Routing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n How unpinned requests choose an account. <strong style=\"color:var(--text)\">Active</strong> uses the manually\n selected profile. <strong style=\"color:var(--text)\">Sticky</strong> distributes sessions across profiles evenly\n (cache-affine). <strong style=\"color:var(--text)\">Priority</strong> drains the pool in order \u2014 highest first \u2014\n and fails over per request when an account runs out; conversations keep their account, and new sessions\n return to the preferred account after its window resets. An explicit <code>x-meridian-profile</code> header\n always overrides. Changes apply to the next request \u2014 no restart needed.\n </p>\n <div class=\"adapter-card\" id=\"routing-card\">\n <div id=\"routing-body\">Loading\u2026</div>\n </div>\n\n <h1 style=\"margin-top:40px\">Model Pricing</h1>\n <p class=\"subtitle\" style=\"max-width:720px;line-height:1.6\">\n Rates used by the telemetry cost estimate, in USD per million tokens. Edit a value to override the\n built-in rate, or add models the built-in table doesn't know about (they show as \"no pricing\" on the\n dashboard until defined here). Changes apply on the next dashboard refresh.\n </p>\n <div class=\"adapter-card\">\n <table class=\"pricing-table\">\n <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>\n <tbody id=\"pricingRows\"></tbody>\n </table>\n <div class=\"pricing-add\">\n <input type=\"text\" class=\"pricing-input\" id=\"newModelName\" placeholder=\"model id (e.g. claude-opus-9)\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelInput\" placeholder=\"input\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelOutput\" placeholder=\"output\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheRead\" placeholder=\"cache read\" min=\"0\" step=\"0.01\">\n <input type=\"number\" class=\"pricing-input\" id=\"newModelCacheWrite\" placeholder=\"cache write\" min=\"0\" step=\"0.01\">\n <button class=\"add-btn\" onclick=\"addPricingModel()\">Add Model</button>\n </div>\n <div class=\"pricing-note\">\n Cache read and cache write are optional; when left blank they default to 0.1x and 1.25x of the\n input rate (the 5-minute cache TTL multipliers). Verify current list prices at\n <a href=\"https://claude.com/pricing\" target=\"_blank\" rel=\"noreferrer\" style=\"color:var(--accent)\">claude.com/pricing</a>.\n </div>\n </div>\n</div>\n\n<div class=\"save-indicator\" id=\"saveIndicator\">Saved</div>\n\n<script>\nconst FEATURES = [\n { key: 'codeSystemPrompt', label: 'Claude Code Prompt', desc: 'Include the built-in Claude Code system prompt (tool usage rules, safety guidelines, coding best practices)', type: 'toggle' },\n { key: 'clientSystemPrompt', label: 'Client Prompt', desc: 'Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions)', type: 'toggle' },\n { key: 'claudeMd', label: 'CLAUDE.md', desc: 'Load CLAUDE.md instruction files \u2014 Off: none, Project: ./CLAUDE.md only, Full: ~/.claude/CLAUDE.md + ./CLAUDE.md', type: 'select', options: ['off', 'project', 'full'] },\n { key: 'memory', label: 'Memory', desc: 'Read and write memories across sessions', type: 'toggle' },\n { key: 'dreaming', label: 'Auto-Dream', desc: 'Background memory consolidation', type: 'toggle' },\n { key: 'thinking', label: 'Thinking', desc: 'Extended thinking mode', type: 'select', options: ['disabled', 'adaptive', 'enabled'] },\n { key: 'thinkingPassthrough', label: 'Thinking Passthrough', desc: 'Forward thinking blocks to the client', type: 'toggle' },\n { key: 'sharedMemory', label: 'Shared Memory', desc: 'Share memory with Claude Code (~/.claude) instead of isolated storage', type: 'toggle' },\n { key: 'maxBudgetUsd', label: 'Max Budget (USD)', desc: 'Per-request cost cap \u2014 query aborts if exceeded (0 = disabled)', type: 'number' },\n { key: 'fallbackModel', label: 'Fallback Model', desc: 'Auto-fallback model if primary fails', type: 'select', options: ['', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] },\n { key: 'sdkDebug', label: 'SDK Debug Logging', desc: 'Enable verbose SDK debug output to proxy stderr', type: 'toggle' },\n { key: 'additionalDirectories', label: 'Additional Directories', desc: 'Comma-separated extra paths Claude can access (monorepo libs, etc.)', type: 'text' },\n];\n\nconst ADAPTER_LABELS = {\n opencode: 'OpenCode',\n openai: 'OpenAI (/v1/chat/completions)',\n crush: 'Crush',\n forgecode: 'ForgeCode',\n pi: 'Pi',\n droid: 'Droid',\n passthrough: 'LiteLLM / Passthrough',\n};\n\nlet currentConfig = {};\n\nasync function loadConfig() {\n const res = await fetch('/settings/api/features');\n currentConfig = await res.json();\n render();\n}\n\nasync function saveFeature(adapter, key, value) {\n const patch = {};\n patch[key] = value;\n await fetch('/settings/api/features/' + adapter, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(patch),\n });\n currentConfig[adapter][key] = value;\n showSaved();\n}\n\nasync function resetAdapter(adapter) {\n await fetch('/settings/api/features/' + adapter, { method: 'DELETE' });\n await loadConfig();\n showSaved();\n}\n\nfunction showSaved() {\n const el = document.getElementById('saveIndicator');\n el.classList.add('visible');\n setTimeout(() => el.classList.remove('visible'), 1500);\n}\n\nfunction hasAnyEnabled(features) {\n return features.codeSystemPrompt || !features.clientSystemPrompt || features.claudeMd !== 'off' || features.memory || features.dreaming ||\n features.thinking !== 'disabled' || features.thinkingPassthrough ||\n features.sharedMemory || features.maxBudgetUsd > 0 ||\n features.fallbackModel || features.sdkDebug ||\n features.additionalDirectories;\n}\n\nfunction render() {\n const container = document.getElementById('adapters');\n container.innerHTML = '';\n\n for (const [adapter, label] of Object.entries(ADAPTER_LABELS)) {\n const features = currentConfig[adapter] || {};\n const active = hasAnyEnabled(features);\n\n const card = document.createElement('div');\n card.className = 'adapter-card';\n card.innerHTML = '<div class=\"adapter-header\">' +\n '<span class=\"adapter-name\">' + label + '</span>' +\n '<div style=\"display:flex;gap:8px;align-items:center\">' +\n '<span class=\"adapter-badge ' + (active ? 'badge-active' : 'badge-inactive') + '\">' +\n (active ? 'Active' : 'Default') +\n '</span>' +\n '<button class=\"reset-btn\" onclick=\"resetAdapter(\\''+adapter+'\\')\">Reset</button>' +\n '</div>' +\n '</div>';\n\n const grid = document.createElement('div');\n grid.className = 'feature-grid';\n\n for (const feat of FEATURES) {\n const row = document.createElement('div');\n row.className = 'feature-row';\n\n const info = '<div class=\"feature-info\"><span class=\"feature-label\">' +\n feat.label + '</span><span class=\"feature-desc\">' + feat.desc + '</span></div>';\n\n if (feat.type === 'toggle') {\n const checked = features[feat.key] ? 'checked' : '';\n row.innerHTML = info +\n '<label class=\"toggle\"><input type=\"checkbox\" ' + checked +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.checked)\">' +\n '<span class=\"toggle-track\"></span></label>';\n } else if (feat.type === 'select') {\n const options = feat.options.map(o => {\n const label = o === '' ? '(None)' : o.charAt(0).toUpperCase()+o.slice(1);\n return '<option value=\"'+o+'\"'+(features[feat.key]===o?' selected':'')+'>'+label+'</option>';\n }).join('');\n row.innerHTML = info +\n '<select class=\"feature-select\" onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">' +\n options + '</select>';\n } else if (feat.type === 'number') {\n const value = features[feat.key] ?? 0;\n row.innerHTML = info +\n '<input type=\"number\" class=\"feature-select\" style=\"width:80px;text-align:right\" min=\"0\" step=\"0.01\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', parseFloat(this.value)||0)\">';\n } else if (feat.type === 'text') {\n const value = (features[feat.key] ?? '').toString().replace(/\"/g, '&quot;');\n row.innerHTML = info +\n '<input type=\"text\" class=\"feature-select\" style=\"width:180px\" value=\"'+value+'\"' +\n ' onchange=\"saveFeature(\\''+adapter+'\\', \\''+feat.key+'\\', this.value)\">';\n }\n\n grid.appendChild(row);\n }\n\n card.appendChild(grid);\n container.appendChild(card);\n }\n}\n\n// ---- Model pricing (telemetry cost estimate) ----\nlet pricingData = { builtin: {}, overrides: {} };\n\nfunction fmtRate(v) { return String(Math.round(v * 10000) / 10000); }\n\nasync function loadPricing() {\n const res = await fetch('/settings/api/pricing');\n pricingData = await res.json();\n renderPricing();\n}\n\nasync function putPricing(model, rates) {\n const res = await fetch('/settings/api/pricing/' + encodeURIComponent(model), {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(rates),\n });\n if (!res.ok) {\n const err = await res.json().catch(function () { return {}; });\n alert('Could not save pricing: ' + (err.error || ('HTTP ' + res.status)));\n return false;\n }\n showSaved();\n return true;\n}\n\nasync function removePricing(model) {\n await fetch('/settings/api/pricing/' + encodeURIComponent(model), { method: 'DELETE' });\n showSaved();\n await loadPricing();\n}\n\nfunction rateCell(value, onCommit) {\n const td = document.createElement('td');\n const input = document.createElement('input');\n input.type = 'number';\n input.min = '0';\n input.step = '0.01';\n input.className = 'pricing-input';\n input.value = fmtRate(value);\n input.addEventListener('change', onCommit);\n td.appendChild(input);\n return { td: td, input: input };\n}\n\nfunction renderPricing() {\n const tbody = document.getElementById('pricingRows');\n tbody.innerHTML = '';\n const models = Array.from(new Set(\n Object.keys(pricingData.builtin).concat(Object.keys(pricingData.overrides))\n )).sort();\n\n for (const model of models) {\n const override = pricingData.overrides[model];\n const effective = override || pricingData.builtin[model];\n const tr = document.createElement('tr');\n\n const nameTd = document.createElement('td');\n nameTd.className = 'pricing-model';\n nameTd.textContent = model;\n tr.appendChild(nameTd);\n\n const cells = [];\n const commit = async function () {\n const rates = {\n inputPerMTok: parseFloat(cells[0].input.value),\n outputPerMTok: parseFloat(cells[1].input.value),\n cacheReadPerMTok: parseFloat(cells[2].input.value),\n cacheWritePerMTok: parseFloat(cells[3].input.value),\n };\n for (const k in rates) { if (!isFinite(rates[k]) || rates[k] < 0) return; }\n if (await putPricing(model, rates)) await loadPricing();\n };\n ['inputPerMTok', 'outputPerMTok', 'cacheReadPerMTok', 'cacheWritePerMTok'].forEach(function (key) {\n const cell = rateCell(effective[key], commit);\n cells.push(cell);\n tr.appendChild(cell.td);\n });\n\n const badgeTd = document.createElement('td');\n const badge = document.createElement('span');\n badge.className = 'pricing-badge ' + (override ? 'badge-override' : 'badge-builtin');\n badge.textContent = override ? 'Override' : 'Built-in';\n badgeTd.appendChild(badge);\n tr.appendChild(badgeTd);\n\n const actionTd = document.createElement('td');\n if (override) {\n const btn = document.createElement('button');\n btn.className = 'reset-btn';\n btn.textContent = pricingData.builtin[model] ? 'Reset' : 'Remove';\n btn.addEventListener('click', function () { removePricing(model); });\n actionTd.appendChild(btn);\n }\n tr.appendChild(actionTd);\n\n tbody.appendChild(tr);\n }\n}\n\nasync function addPricingModel() {\n const name = document.getElementById('newModelName').value.trim();\n const inputRate = parseFloat(document.getElementById('newModelInput').value);\n const outputRate = parseFloat(document.getElementById('newModelOutput').value);\n if (!name) { alert('Enter a model id'); return; }\n if (!isFinite(inputRate) || !isFinite(outputRate)) { alert('Enter input and output rates (USD per million tokens)'); return; }\n const rates = { inputPerMTok: inputRate, outputPerMTok: outputRate };\n const cacheRead = parseFloat(document.getElementById('newModelCacheRead').value);\n const cacheWrite = parseFloat(document.getElementById('newModelCacheWrite').value);\n if (isFinite(cacheRead)) rates.cacheReadPerMTok = cacheRead;\n if (isFinite(cacheWrite)) rates.cacheWritePerMTok = cacheWrite;\n if (await putPricing(name, rates)) {\n ['newModelName', 'newModelInput', 'newModelOutput', 'newModelCacheRead', 'newModelCacheWrite'].forEach(function (id) {\n document.getElementById(id).value = '';\n });\n await loadPricing();\n }\n}\n\nasync function loadRouting() {\n const res = await fetch('/settings/api/routing');\n const cfg = await res.json();\n const el = document.getElementById('routing-body');\n const envNote = (on) => on ? ' <span style=\"font-size:11px;color:var(--yellow)\">(env override active \u2014 setting saved but env wins)</span>' : '';\n let h = '<div style=\"display:flex;align-items:center;gap:12px;margin-bottom:14px\">'\n + '<label style=\"color:var(--muted);font-size:13px;width:90px\">Mode</label>'\n + '<select id=\"routing-mode\" style=\"background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px\">'\n + ['active','sticky','priority'].map(m => '<option value=\"'+m+'\"'+(cfg.routing===m?' selected':'')+'>'+m+'</option>').join('')\n + '</select>' + envNote(cfg.envOverride.routing) + '</div>';\n h += '<div id=\"routing-order-wrap\" style=\"'+(cfg.routing==='priority'?'':'display:none')+'\">'\n + '<div style=\"color:var(--muted);font-size:13px;margin-bottom:8px\">Pool order \u2014 highest priority first. Drained top to bottom.'\n + envNote(cfg.envOverride.profileOrder) + '</div>'\n + '<ol id=\"routing-order\" style=\"margin:0;padding-left:22px\">'\n + cfg.profileOrder.map((id, i) =>\n '<li style=\"padding:4px 0;color:var(--text)\">'\n + '<span style=\"font-family:var(--mono, monospace)\">'+id+'</span>'\n + ' <button data-move=\"up\" data-i=\"'+i+'\" style=\"margin-left:10px;background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer\"'+(i===0?' disabled':'')+'>&uarr;</button>'\n + ' <button data-move=\"down\" data-i=\"'+i+'\" style=\"background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:1px 8px;cursor:pointer\"'+(i===cfg.profileOrder.length-1?' disabled':'')+'>&darr;</button>'\n + '</li>').join('')\n + '</ol></div>';\n el.innerHTML = h;\n document.getElementById('routing-mode').addEventListener('change', async (e) => {\n await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ routing: e.target.value }) });\n await loadRouting();\n });\n el.querySelectorAll('button[data-move]').forEach(btn => btn.addEventListener('click', async () => {\n const i = Number(btn.dataset.i);\n const j = btn.dataset.move === 'up' ? i - 1 : i + 1;\n const order = cfg.profileOrder.slice();\n const tmp = order[i]; order[i] = order[j]; order[j] = tmp;\n await fetch('/settings/api/routing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileOrder: order }) });\n await loadRouting();\n }));\n}\n\nloadConfig();\nloadPricing();\nloadRouting();\n\n(function() {\n var profileChip = document.getElementById('mhProfile');\n var statusDot = document.getElementById('mhDot');\n var statusText = document.getElementById('mhStatusText');\n\n // Highlight active nav link\n var path = location.pathname;\n var navLinks = document.querySelectorAll('.mh-nav a');\n navLinks.forEach(function(a) {\n if (a.getAttribute('href') === path || (path === '/telemetry' && a.id === 'nav-telemetry') || (path === '/' && a.id === 'nav-home')) {\n a.classList.add('active');\n }\n });\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n\n function loadHeader() {\n fetch('/health').then(function(r) { return r.json(); }).then(function(h) {\n var st = h.status === 'healthy' ? 'healthy' : h.status === 'degraded' ? 'degraded' : 'unhealthy';\n statusDot.className = 'mh-dot ' + st;\n statusText.textContent = st === 'healthy' ? 'Operational' : st === 'degraded' ? 'Degraded' : 'Offline';\n }).catch(function() {\n statusDot.className = 'mh-dot unhealthy';\n statusText.textContent = 'Offline';\n });\n\n fetch('/profiles/list').then(function(r) { return r.json(); }).then(function(data) {\n var current = (data.profiles || []).find(function(p) { return p.isActive; });\n if (!current) { profileChip.classList.remove('visible'); return; }\n profileChip.innerHTML = esc(current.id) + ' <span class=\"mh-profile-type\">' + esc(current.type || '') + '</span>';\n profileChip.classList.add('visible');\n }).catch(function() {});\n }\n\n loadHeader();\n setInterval(loadHeader, 10000);\n // Pages call this after mutating state (e.g. switching the active profile)\n // so the header chip updates immediately instead of on the next poll.\n window.meridianHeaderRefresh = loadHeader;\n})();\n\n</script>\n</body>\n</html>";
6
6
  //# sourceMappingURL=settingsPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,+p0BAuZrB,CAAA"}
1
+ {"version":3,"file":"settingsPage.d.ts","sourceRoot":"","sources":["../../src/telemetry/settingsPage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,eAAO,MAAM,gBAAgB,yw7BAycrB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.55.1",
3
+ "version": "1.56.1",
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",