@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.
- package/dist/{cli-3p1793vq.js → cli-h3c4qrv6.js} +331 -143
- package/dist/{cli-f0yqy2d2.js → cli-ngtexmne.js} +68 -2
- package/dist/cli.js +3 -3
- package/dist/{profiles-84hnbd6c.js → profiles-sq9t3fh9.js} +1 -1
- package/dist/proxy/errors.d.ts.map +1 -1
- package/dist/proxy/oauthUsage.d.ts +5 -0
- package/dist/proxy/oauthUsage.d.ts.map +1 -1
- package/dist/proxy/routing.d.ts +42 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/settings.d.ts +5 -2
- package/dist/proxy/settings.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/telemetry/dashboard.d.ts.map +1 -1
- package/dist/telemetry/landing.d.ts.map +1 -1
- package/dist/telemetry/settingsPage.d.ts +1 -1
- package/dist/telemetry/settingsPage.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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':'')+'>↑</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':'')+'>↓</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
|
-
|
|
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
|
|
4602
|
+
return staleOr("refresh_failed");
|
|
4537
4603
|
}
|
|
4538
4604
|
const newToken = await readAccessToken(store);
|
|
4539
4605
|
if (!newToken)
|
|
4540
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
9524
|
-
|
|
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('logs')">'
|
|
9542
9587
|
+ 'Logs<span class="tab-badge">' + logs.length + '</span></div>'
|
|
9543
|
-
+ '<div class="tab' + (activeTab === 'usage' ? ' active' : '') + '" data-tab="usage" onclick="switchTab('usage')">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
|
|
10036
|
-
var
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
5
|
-
import"./cli-
|
|
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-
|
|
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 });
|
|
@@ -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,
|
|
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;
|
|
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"}
|
package/dist/proxy/routing.d.ts
CHANGED
|
@@ -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;
|
|
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,
|
|
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"}
|
package/dist/proxy/settings.d.ts
CHANGED
|
@@ -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)
|
|
14
|
-
* MERIDIAN_ROUTING env var takes precedence
|
|
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;
|
|
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-
|
|
15
|
-
import"./cli-
|
|
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,
|
|
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,
|
|
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, '"');\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, '"');\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':'')+'>↑</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':'')+'>↓</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
|
|
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