@rynfar/meridian 1.55.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli-w63v1ftp.js → cli-aefk7t3r.js} +331 -147
- 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/agentMatch.d.ts +6 -1
- package/dist/proxy/agentMatch.d.ts.map +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
|
}
|
|
@@ -9081,9 +9147,15 @@ var KNOWN_ALIASES = {
|
|
|
9081
9147
|
executor: "build"
|
|
9082
9148
|
};
|
|
9083
9149
|
var STRIP_SUFFIXES = ["-agent", "-tool", "-worker", "-task", " agent", " tool"];
|
|
9084
|
-
function resolveAgentAlias(input) {
|
|
9150
|
+
function resolveAgentAlias(input, validAgents) {
|
|
9085
9151
|
const lowered = input.toLowerCase();
|
|
9086
|
-
|
|
9152
|
+
const exact = validAgents.find((a) => a.toLowerCase() === lowered);
|
|
9153
|
+
if (exact)
|
|
9154
|
+
return exact;
|
|
9155
|
+
const alias = KNOWN_ALIASES[lowered];
|
|
9156
|
+
if (alias && validAgents.includes(alias))
|
|
9157
|
+
return alias;
|
|
9158
|
+
return lowered;
|
|
9087
9159
|
}
|
|
9088
9160
|
function fuzzyMatchAgentName(input, validAgents) {
|
|
9089
9161
|
if (!input)
|
|
@@ -9388,27 +9460,8 @@ var dashboardHtml = `<!DOCTYPE html>
|
|
|
9388
9460
|
transition: all 0.15s; }
|
|
9389
9461
|
.log-filter:hover { border-color: var(--accent); color: var(--text); }
|
|
9390
9462
|
.log-filter.active { background: rgba(88,166,255,0.1); border-color: var(--accent); color: var(--accent); }
|
|
9391
|
-
|
|
9392
|
-
/* Usage tab */
|
|
9393
|
-
.usage-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
9394
|
-
.ucard { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; }
|
|
9395
|
-
.ucard-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
|
9396
|
-
.ucard-title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
9397
|
-
.ucard-reset { font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
9398
|
-
.ucard-pct { font-size: 32px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 8px; color: var(--green); }
|
|
9399
|
-
.ucard.warn .ucard-pct { color: var(--yellow); }
|
|
9400
|
-
.ucard.high .ucard-pct { color: var(--red); }
|
|
9401
|
-
.ucard-sub { font-size: 12px; color: var(--muted); margin-top: 8px; min-height: 16px; }
|
|
9402
|
-
.ubar { position: relative; height: 8px; border-radius: 4px; background: var(--border); overflow: visible; margin-top: 12px; }
|
|
9403
|
-
.ubar-fill { height: 100%; border-radius: 4px; background: var(--green); transition: width 0.4s ease; max-width: 100%; }
|
|
9404
|
-
.ucard.warn .ubar-fill { background: var(--yellow); }
|
|
9405
|
-
.ucard.high .ubar-fill { background: var(--red); }
|
|
9406
|
-
.ubar-marker { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); opacity: 0.55; border-radius: 1px; }
|
|
9407
|
-
.pace-pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
|
|
9408
|
-
.pace-pill.on, .pace-pill.under { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
9409
|
-
.pace-pill.ahead { background: rgba(210,153,34,0.18); color: var(--yellow); }
|
|
9410
|
-
.pace-pill.over { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
9411
9463
|
.usage-note { font-size: 11px; color: var(--muted); }
|
|
9464
|
+
|
|
9412
9465
|
` + profileBarCss + `
|
|
9413
9466
|
</style>
|
|
9414
9467
|
</head>
|
|
@@ -9501,22 +9554,20 @@ function setLogFilter(filter) {
|
|
|
9501
9554
|
async function refresh() {
|
|
9502
9555
|
const w = $('#window').value;
|
|
9503
9556
|
try {
|
|
9504
|
-
const [summary, reqs, logs
|
|
9557
|
+
const [summary, reqs, logs] = await Promise.all([
|
|
9505
9558
|
fetch('/telemetry/summary?window=' + w).then(r => r.json()),
|
|
9506
9559
|
fetch('/telemetry/requests?limit=50&since=' + (Date.now() - Number(w))).then(r => r.json()),
|
|
9507
9560
|
fetch('/telemetry/logs?limit=200&since=' + (Date.now() - Number(w))).then(r => r.json()),
|
|
9508
|
-
fetch('/v1/usage/quota').then(r => r.json()).catch(() => null),
|
|
9509
9561
|
]);
|
|
9510
|
-
render(summary, reqs, logs
|
|
9562
|
+
render(summary, reqs, logs);
|
|
9511
9563
|
$('#lastUpdate').textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
9512
9564
|
} catch (e) {
|
|
9513
9565
|
$('#content').innerHTML = '<div class="empty">Failed to load telemetry</div>';
|
|
9514
9566
|
}
|
|
9515
9567
|
}
|
|
9516
9568
|
|
|
9517
|
-
function render(s, reqs, logs
|
|
9518
|
-
|
|
9519
|
-
if (s.totalRequests === 0 && (!logs || logs.length === 0) && !hasUsage) {
|
|
9569
|
+
function render(s, reqs, logs) {
|
|
9570
|
+
if (s.totalRequests === 0 && (!logs || logs.length === 0)) {
|
|
9520
9571
|
$('#content').innerHTML = '<div class="empty">No requests recorded yet. Send a request through the proxy to see telemetry.</div>';
|
|
9521
9572
|
return;
|
|
9522
9573
|
}
|
|
@@ -9534,7 +9585,6 @@ function render(s, reqs, logs, quota) {
|
|
|
9534
9585
|
+ 'Requests<span class="tab-badge">' + reqs.length + '</span></div>'
|
|
9535
9586
|
+ '<div class="tab' + (activeTab === 'logs' ? ' active' : '') + '" data-tab="logs" onclick="switchTab('logs')">'
|
|
9536
9587
|
+ 'Logs<span class="tab-badge">' + logs.length + '</span></div>'
|
|
9537
|
-
+ '<div class="tab' + (activeTab === 'usage' ? ' active' : '') + '" data-tab="usage" onclick="switchTab('usage')">Usage</div>'
|
|
9538
9588
|
+ '</div>';
|
|
9539
9589
|
|
|
9540
9590
|
// ==================== Overview tab ====================
|
|
@@ -9724,11 +9774,6 @@ function render(s, reqs, logs, quota) {
|
|
|
9724
9774
|
}
|
|
9725
9775
|
html += '</div>'; // end logs panel
|
|
9726
9776
|
|
|
9727
|
-
// ==================== Usage tab ====================
|
|
9728
|
-
html += '<div id="panel-usage" class="tab-panel' + (activeTab === 'usage' ? ' active' : '') + '">';
|
|
9729
|
-
html += renderUsage(quota);
|
|
9730
|
-
html += '</div>'; // end usage panel
|
|
9731
|
-
|
|
9732
9777
|
$('#content').innerHTML = html;
|
|
9733
9778
|
}
|
|
9734
9779
|
|
|
@@ -9739,101 +9784,6 @@ function card(label, value, detail) {
|
|
|
9739
9784
|
+ '</div>';
|
|
9740
9785
|
}
|
|
9741
9786
|
|
|
9742
|
-
// ---- Usage tab helpers (mirror src/telemetry/profileUsage.ts; unit-tested there) ----
|
|
9743
|
-
function classifyUtil(u) {
|
|
9744
|
-
if (u == null || !isFinite(u)) return '';
|
|
9745
|
-
if (u >= 0.85) return 'high';
|
|
9746
|
-
if (u >= 0.6) return 'warn';
|
|
9747
|
-
return '';
|
|
9748
|
-
}
|
|
9749
|
-
function resetIn(resetsAt) {
|
|
9750
|
-
if (resetsAt == null || !isFinite(resetsAt)) return '';
|
|
9751
|
-
var ms = resetsAt - Date.now();
|
|
9752
|
-
if (ms <= 0) return 'resetting…';
|
|
9753
|
-
var m = Math.floor(ms / 60000);
|
|
9754
|
-
if (m < 60) return 'resets in ' + Math.max(1, m) + 'm';
|
|
9755
|
-
var h = Math.floor(m / 60), rm = m % 60;
|
|
9756
|
-
if (h < 24) return 'resets in ' + h + 'h' + (rm ? ' ' + rm + 'm' : '');
|
|
9757
|
-
var d = Math.floor(h / 24), rh = h % 24;
|
|
9758
|
-
return 'resets in ' + d + 'd' + (rh ? ' ' + rh + 'h' : '');
|
|
9759
|
-
}
|
|
9760
|
-
function pct(u) { return Math.round(Math.max(0, u) * 100); }
|
|
9761
|
-
|
|
9762
|
-
function usageCard(title, bucket) {
|
|
9763
|
-
if (!bucket || bucket.utilization == null) {
|
|
9764
|
-
return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">' + title + '</span></div>'
|
|
9765
|
-
+ '<div class="ucard-pct" style="color:var(--muted)">—</div>'
|
|
9766
|
-
+ '<div class="ucard-sub">No data yet</div></div>';
|
|
9767
|
-
}
|
|
9768
|
-
var u = bucket.utilization;
|
|
9769
|
-
var cls = classifyUtil(u);
|
|
9770
|
-
var fill = Math.min(100, pct(u));
|
|
9771
|
-
return '<div class="ucard ' + cls + '">'
|
|
9772
|
-
+ '<div class="ucard-head"><span class="ucard-title">' + title + '</span>'
|
|
9773
|
-
+ '<span class="ucard-reset">' + resetIn(bucket.resetsAt) + '</span></div>'
|
|
9774
|
-
+ '<div class="ucard-pct">' + pct(u) + '<span style="font-size:16px;font-weight:500;color:var(--muted)">%</span></div>'
|
|
9775
|
-
+ '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%"></div></div>'
|
|
9776
|
-
+ '<div class="ucard-sub">of your ' + title.split('·')[1].trim() + ' allowance used</div>'
|
|
9777
|
-
+ '</div>';
|
|
9778
|
-
}
|
|
9779
|
-
|
|
9780
|
-
// Weekly pace: actual vs. expected (even) consumption at this point in the 7-day window.
|
|
9781
|
-
function paceCard(weekly) {
|
|
9782
|
-
if (!weekly || weekly.utilization == null || weekly.resetsAt == null) {
|
|
9783
|
-
return '<div class="ucard"><div class="ucard-head"><span class="ucard-title">Weekly Pace</span></div>'
|
|
9784
|
-
+ '<div class="ucard-pct" style="color:var(--muted)">—</div>'
|
|
9785
|
-
+ '<div class="ucard-sub">Needs weekly usage data</div></div>';
|
|
9786
|
-
}
|
|
9787
|
-
var WEEK = 7 * 86400000;
|
|
9788
|
-
var start = weekly.resetsAt - WEEK;
|
|
9789
|
-
var elapsed = Math.max(0, Math.min(1, (Date.now() - start) / WEEK));
|
|
9790
|
-
var actual = pct(weekly.utilization);
|
|
9791
|
-
var expected = Math.round(elapsed * 100);
|
|
9792
|
-
var delta = actual - expected;
|
|
9793
|
-
var projected = elapsed >= 0.1 ? Math.round((Math.max(0, weekly.utilization) / elapsed) * 100) : null;
|
|
9794
|
-
|
|
9795
|
-
var pill, label;
|
|
9796
|
-
if (delta > 7) { pill = 'ahead'; label = '+' + delta + '% ahead of pace'; }
|
|
9797
|
-
else if (delta < -7) { pill = 'under'; label = Math.abs(delta) + '% under pace'; }
|
|
9798
|
-
else { pill = 'on'; label = 'On pace'; }
|
|
9799
|
-
if (projected != null && projected >= 100) { pill = 'over'; label = 'On track to run out'; }
|
|
9800
|
-
|
|
9801
|
-
var fill = Math.min(100, actual);
|
|
9802
|
-
var mark = Math.min(100, expected);
|
|
9803
|
-
var proj = projected == null ? '—' : projected + '%';
|
|
9804
|
-
return '<div class="ucard">'
|
|
9805
|
-
+ '<div class="ucard-head"><span class="ucard-title">Weekly Pace</span>'
|
|
9806
|
-
+ '<span class="ucard-reset">' + Math.round(elapsed * 100) + '% through week</span></div>'
|
|
9807
|
-
+ '<div style="margin-top:8px"><span class="pace-pill ' + pill + '">' + label + '</span></div>'
|
|
9808
|
-
+ '<div class="ubar"><div class="ubar-fill" style="width:' + fill + '%;background:' + (pill === 'over' ? 'var(--red)' : pill === 'ahead' ? 'var(--yellow)' : 'var(--green)') + '"></div>'
|
|
9809
|
-
+ '<div class="ubar-marker" style="left:' + mark + '%" title="Expected at even pace"></div></div>'
|
|
9810
|
-
+ '<div class="ucard-sub">' + actual + '% used vs ' + expected + '% expected · at this rate ~' + proj + ' by reset</div>'
|
|
9811
|
-
+ '</div>';
|
|
9812
|
-
}
|
|
9813
|
-
|
|
9814
|
-
function renderUsage(quota) {
|
|
9815
|
-
if (!quota || !quota.buckets) {
|
|
9816
|
-
return '<div class="empty">Usage data unavailable.</div>';
|
|
9817
|
-
}
|
|
9818
|
-
var by = {};
|
|
9819
|
-
quota.buckets.forEach(function (b) { by[b.type] = b; });
|
|
9820
|
-
var session = by['five_hour'], weekly = by['seven_day'];
|
|
9821
|
-
if ((!session || session.utilization == null) && (!weekly || weekly.utilization == null)) {
|
|
9822
|
-
return '<div class="empty">No usage data yet — Anthropic reports it after your first request through Meridian.</div>';
|
|
9823
|
-
}
|
|
9824
|
-
var h = '<div class="usage-cards">'
|
|
9825
|
-
+ usageCard('Session · 5h', session)
|
|
9826
|
-
+ usageCard('Weekly · 7d', weekly)
|
|
9827
|
-
+ paceCard(weekly)
|
|
9828
|
-
+ '</div>';
|
|
9829
|
-
var asOf = quota.asOf ? new Date(quota.asOf).toLocaleTimeString() : '';
|
|
9830
|
-
h += '<div class="usage-note">'
|
|
9831
|
-
+ (quota.profile ? 'Profile: ' + quota.profile + ' · ' : '')
|
|
9832
|
-
+ 'Reported by Anthropic' + (asOf ? ' · as of ' + asOf : '')
|
|
9833
|
-
+ '</div>';
|
|
9834
|
-
return h;
|
|
9835
|
-
}
|
|
9836
|
-
|
|
9837
9787
|
$('#autoRefresh').addEventListener('change', function() {
|
|
9838
9788
|
clearInterval(timer);
|
|
9839
9789
|
if (this.checked) timer = setInterval(refresh, 5000);
|
|
@@ -9941,6 +9891,10 @@ var landingHtml = `<!DOCTYPE html>
|
|
|
9941
9891
|
.usage-row .w-label { color: var(--muted); width: 64px; flex-shrink: 0; }
|
|
9942
9892
|
.usage-row .w-bar { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
|
|
9943
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 .pace-text { flex: 1; font-weight: 600; font-size: 11px; }
|
|
9896
|
+
.pool-chip { font-size: 10px; padding: 2px 8px; border-radius: 10px; background: var(--surface2); color: var(--muted); margin-left: 6px; vertical-align: middle; }
|
|
9897
|
+
.pool-chip.exhausted { color: var(--red); background: rgba(248,81,73,0.12); }
|
|
9944
9898
|
.usage-row .w-pct { width: 38px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
9945
9899
|
.usage-row .w-reset { color: var(--muted); font-size: 11px; width: 76px; text-align: right; }
|
|
9946
9900
|
.no-usage { font-size: 12px; color: var(--muted); padding: 4px 0; }
|
|
@@ -9955,6 +9909,7 @@ var landingHtml = `<!DOCTYPE html>
|
|
|
9955
9909
|
.strip-value.green { color: var(--green); }
|
|
9956
9910
|
.strip-value.red { color: var(--red); }
|
|
9957
9911
|
.strip-detail { font-size: 11px; color: var(--muted); }
|
|
9912
|
+
.strip-detail.red { color: var(--red); }
|
|
9958
9913
|
|
|
9959
9914
|
.section { margin-bottom: 24px; }
|
|
9960
9915
|
.section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase;
|
|
@@ -9979,6 +9934,29 @@ function usd(v){if(v==null)return '—';if(v>0&&v<0.01)return '$'+v.toFixed(4);i
|
|
|
9979
9934
|
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'};
|
|
9980
9935
|
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()})}
|
|
9981
9936
|
function utilColor(u){return u>=0.85?'var(--red)':u>=0.6?'var(--yellow)':'var(--green)'}
|
|
9937
|
+
// Mirrors computeWeeklyPace in src/telemetry/profileUsage.ts (unit-tested
|
|
9938
|
+
// there): actual vs expected (even) consumption at this point in the 7-day
|
|
9939
|
+
// window, with the dashboard's over-promotion when the projection hits 100%.
|
|
9940
|
+
function weeklyPace(u,resetsAt){
|
|
9941
|
+
var WEEK=7*86400000;
|
|
9942
|
+
if(u==null||resetsAt==null)return null;
|
|
9943
|
+
var el=Math.max(0,Math.min(1,(Date.now()-(resetsAt-WEEK))/WEEK));
|
|
9944
|
+
var actual=Math.round(Math.max(0,u)*100);
|
|
9945
|
+
var expected=Math.round(el*100);
|
|
9946
|
+
var delta=actual-expected;
|
|
9947
|
+
var proj=el>=0.1?Math.round((Math.max(0,u)/el)*100):null;
|
|
9948
|
+
var st=delta>7?'ahead':delta<-7?'under':'on';
|
|
9949
|
+
if(proj!=null&&proj>=100)st='over';
|
|
9950
|
+
return {actual:actual,expected:expected,delta:delta,proj:proj,status:st};
|
|
9951
|
+
}
|
|
9952
|
+
function paceText(pc){
|
|
9953
|
+
if(pc.status==='over')return 'on track to run out';
|
|
9954
|
+
if(pc.status==='ahead')return '+'+pc.delta+'% ahead of pace';
|
|
9955
|
+
if(pc.status==='under')return Math.abs(pc.delta)+'% under pace';
|
|
9956
|
+
return 'on pace';
|
|
9957
|
+
}
|
|
9958
|
+
function paceColor(pc){return pc.status==='over'?'var(--red)':pc.status==='ahead'?'var(--yellow)':'var(--green)'}
|
|
9959
|
+
|
|
9982
9960
|
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':'')}
|
|
9983
9961
|
|
|
9984
9962
|
function introSection(h){
|
|
@@ -10025,9 +10003,22 @@ function profileSection(q,s,pl,h){
|
|
|
10025
10003
|
+'<span class="w-pct" style="color:'+utilColor(w.utilization)+'">'+pct+'%</span>'
|
|
10026
10004
|
+'<span class="w-reset">'+resetIn(w.resetsAt)+'</span></div>';
|
|
10027
10005
|
}
|
|
10006
|
+
var weekly=null;
|
|
10007
|
+
for(var j=0;j<wins.length;j++){if(wins[j].type==='seven_day')weekly=wins[j]}
|
|
10008
|
+
var pc=weekly?weeklyPace(weekly.utilization,weekly.resetsAt):null;
|
|
10009
|
+
if(pc)rows+='<div class="usage-row pace-row"><span class="w-label">pace</span>'
|
|
10010
|
+
+'<span class="pace-text" style="color:'+paceColor(pc)+'">'+paceText(pc)+'</span>'
|
|
10011
|
+
+'<span class="w-reset">'+(pc.proj!=null?'~'+pc.proj+'% by reset':'')+'</span></div>';
|
|
10028
10012
|
if(!rows)rows='<div class="no-usage">no usage data yet</div>';
|
|
10029
|
-
var
|
|
10030
|
-
var
|
|
10013
|
+
var isPriority=pl&&pl.routing==='priority';
|
|
10014
|
+
var switchable=multi&&p.configured&&!p.isActive&&!isPriority;
|
|
10015
|
+
var badge=isPriority?'':p.isActive?'<span class="active-pill">Active</span>':switchable?'<span class="switch-hint">Click to activate</span>':'';
|
|
10016
|
+
if(isPriority){
|
|
10017
|
+
var orderIdx=(pl.profileOrder||[]).indexOf(p.id);
|
|
10018
|
+
if(orderIdx>=0)badge+='<span class="pool-chip">#'+(orderIdx+1)+' in pool</span>';
|
|
10019
|
+
var exh=(pl.exhausted||[]).filter(function(e){return e.id===p.id})[0];
|
|
10020
|
+
if(exh)badge+=' <span class="pool-chip exhausted">exhausted · resets '+resetIn(exh.until)+'</span>';
|
|
10021
|
+
}
|
|
10031
10022
|
cards+='<div class="profile-card'+(p.isActive?' active':'')+(switchable?' switchable':'')+'"'+(switchable?' data-profile="'+esc(p.id)+'" role="button" tabindex="0"':'')+'>'
|
|
10032
10023
|
+'<div class="profile-head"><span class="profile-name"><span class="prof-dot"></span>'+esc(p.label||p.id)+' '+badge+'</span>'
|
|
10033
10024
|
+'<span class="profile-cost">'+usd(cost?cost.estimatedUsd:0)+'</span></div>'
|
|
@@ -10041,7 +10032,7 @@ function profileSection(q,s,pl,h){
|
|
|
10041
10032
|
function strip(items){
|
|
10042
10033
|
var o='<div class="strip">';
|
|
10043
10034
|
for(var i=0;i<items.length;i++){var it=items[i];
|
|
10044
|
-
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>';
|
|
10035
|
+
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>';
|
|
10045
10036
|
}
|
|
10046
10037
|
return o+'</div>';
|
|
10047
10038
|
}
|
|
@@ -10072,7 +10063,9 @@ function render(h,s,q,pl){
|
|
|
10072
10063
|
var tu=s.tokenUsage||{};
|
|
10073
10064
|
var cache=tu.avgCacheHitRate!=null?Math.round(tu.avgCacheHitRate*100)+'%':'—';
|
|
10074
10065
|
var items=[
|
|
10075
|
-
|
|
10066
|
+
// The big number is the TOTAL — never error-colored (a red 1714 reads as
|
|
10067
|
+
// 1714 failures). The error signal lives on the detail line only.
|
|
10068
|
+
['Requests',String(s.totalRequests),'',s.errorCount>0?s.errorCount+' error'+(s.errorCount===1?'':'s'):'no errors',s.errorCount>0?'red':''],
|
|
10076
10069
|
['Tokens Out',tokens(tu.totalOutputTokens),'',tokens(tu.totalInputTokens)+' in'],
|
|
10077
10070
|
['Cache Hit',cache,tu.avgCacheHitRate>=0.5?'green':'','prompt cache'],
|
|
10078
10071
|
['Est. API Value',usd(s.costEstimate?.totalUsd),'','list prices'],
|
|
@@ -10211,7 +10204,7 @@ function classifyError(errMsg) {
|
|
|
10211
10204
|
message: "Claude authentication expired or invalid. Run 'claude login' in your terminal to re-authenticate, then restart the proxy."
|
|
10212
10205
|
};
|
|
10213
10206
|
}
|
|
10214
|
-
if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests")) {
|
|
10207
|
+
if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("hit your session limit") || lower.includes("usage limit reached")) {
|
|
10215
10208
|
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." : "";
|
|
10216
10209
|
return {
|
|
10217
10210
|
status: 429,
|
|
@@ -19848,6 +19841,127 @@ function createProxyServer(config = {}) {
|
|
|
19848
19841
|
app.use("/settings/*", requireAuth);
|
|
19849
19842
|
app.use("/settings", requireAuth);
|
|
19850
19843
|
app.use("/design-login", requireAuth);
|
|
19844
|
+
const priorityExhaustion = new ProfileExhaustion;
|
|
19845
|
+
const priorityAssignments = new Map;
|
|
19846
|
+
const PRIORITY_ASSIGNMENTS_MAX = 5000;
|
|
19847
|
+
const PRIORITY_DEFAULT_COOLDOWN_MS = 10 * 60000;
|
|
19848
|
+
const PRIORITY_COOLDOWN_CAP_MS = 6 * 60 * 60000;
|
|
19849
|
+
function priorityProfileOrderSetting() {
|
|
19850
|
+
const env2 = process.env.MERIDIAN_PROFILE_ORDER;
|
|
19851
|
+
if (env2 && env2.trim())
|
|
19852
|
+
return env2.split(",").map((s) => s.trim()).filter(Boolean);
|
|
19853
|
+
const setting = getSetting("profileOrder");
|
|
19854
|
+
return Array.isArray(setting) && setting.length > 0 ? setting : undefined;
|
|
19855
|
+
}
|
|
19856
|
+
function priorityCooldownUntil(now) {
|
|
19857
|
+
const fiveHour = rateLimitStore.getAll().find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now);
|
|
19858
|
+
const until = fiveHour?.resetsAt ?? now + PRIORITY_DEFAULT_COOLDOWN_MS;
|
|
19859
|
+
return Math.min(until, now + PRIORITY_COOLDOWN_CAP_MS);
|
|
19860
|
+
}
|
|
19861
|
+
async function sniffQuotaFailure(res) {
|
|
19862
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
19863
|
+
if (!contentType.includes("text/event-stream")) {
|
|
19864
|
+
if (res.status === 429) {
|
|
19865
|
+
const body = await res.clone().json().catch(() => null);
|
|
19866
|
+
if (body?.error?.type === "rate_limit_error")
|
|
19867
|
+
return { failed: true, errorPayload: body, response: res };
|
|
19868
|
+
}
|
|
19869
|
+
return { failed: false, errorPayload: null, response: res };
|
|
19870
|
+
}
|
|
19871
|
+
const reader = res.body?.getReader();
|
|
19872
|
+
if (!reader)
|
|
19873
|
+
return { failed: false, errorPayload: null, response: res };
|
|
19874
|
+
const decoder = new TextDecoder;
|
|
19875
|
+
const consumed = [];
|
|
19876
|
+
let text = "";
|
|
19877
|
+
let failedPayload = null;
|
|
19878
|
+
while (true) {
|
|
19879
|
+
const { done, value } = await reader.read();
|
|
19880
|
+
if (done)
|
|
19881
|
+
break;
|
|
19882
|
+
consumed.push(value);
|
|
19883
|
+
text += decoder.decode(value, { stream: true });
|
|
19884
|
+
const frameEnd = text.indexOf(`
|
|
19885
|
+
|
|
19886
|
+
`);
|
|
19887
|
+
if (frameEnd === -1)
|
|
19888
|
+
continue;
|
|
19889
|
+
const frame = text.slice(0, frameEnd);
|
|
19890
|
+
if (/^event: error$/m.test(frame)) {
|
|
19891
|
+
const dataLine = frame.split(`
|
|
19892
|
+
`).find((l) => l.startsWith("data: "));
|
|
19893
|
+
try {
|
|
19894
|
+
const parsed = dataLine ? JSON.parse(dataLine.slice(6)) : null;
|
|
19895
|
+
if (parsed?.error?.type === "rate_limit_error") {
|
|
19896
|
+
failedPayload = parsed;
|
|
19897
|
+
}
|
|
19898
|
+
} catch {}
|
|
19899
|
+
}
|
|
19900
|
+
break;
|
|
19901
|
+
}
|
|
19902
|
+
if (failedPayload) {
|
|
19903
|
+
await reader.cancel().catch(() => {});
|
|
19904
|
+
return { failed: true, errorPayload: failedPayload, response: res };
|
|
19905
|
+
}
|
|
19906
|
+
const rest = new ReadableStream({
|
|
19907
|
+
start(ctrl) {
|
|
19908
|
+
for (const chunk of consumed)
|
|
19909
|
+
ctrl.enqueue(chunk);
|
|
19910
|
+
},
|
|
19911
|
+
async pull(ctrl) {
|
|
19912
|
+
const { done, value } = await reader.read();
|
|
19913
|
+
if (done)
|
|
19914
|
+
ctrl.close();
|
|
19915
|
+
else
|
|
19916
|
+
ctrl.enqueue(value);
|
|
19917
|
+
},
|
|
19918
|
+
cancel(reason) {
|
|
19919
|
+
reader.cancel(reason).catch(() => {});
|
|
19920
|
+
}
|
|
19921
|
+
});
|
|
19922
|
+
return { failed: false, errorPayload: null, response: new Response(rest, { status: res.status, headers: res.headers }) };
|
|
19923
|
+
}
|
|
19924
|
+
async function dispatchPriority(c, orderedCandidateIds, sessionKey, wantsStream) {
|
|
19925
|
+
const bodyBuf = await c.req.arrayBuffer();
|
|
19926
|
+
let lastError = null;
|
|
19927
|
+
let previous = null;
|
|
19928
|
+
for (const candidate of orderedCandidateIds) {
|
|
19929
|
+
const headers = new Headers(c.req.raw.headers);
|
|
19930
|
+
headers.set("x-meridian-profile", candidate);
|
|
19931
|
+
headers.set("x-meridian-priority-dispatch", "1");
|
|
19932
|
+
const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
|
|
19933
|
+
const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
|
|
19934
|
+
if (!failed) {
|
|
19935
|
+
if (sessionKey) {
|
|
19936
|
+
priorityAssignments.set(sessionKey, candidate);
|
|
19937
|
+
if (priorityAssignments.size > PRIORITY_ASSIGNMENTS_MAX) {
|
|
19938
|
+
const oldest = priorityAssignments.keys().next().value;
|
|
19939
|
+
if (oldest !== undefined)
|
|
19940
|
+
priorityAssignments.delete(oldest);
|
|
19941
|
+
}
|
|
19942
|
+
}
|
|
19943
|
+
if (previous) {
|
|
19944
|
+
claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
|
|
19945
|
+
plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
|
|
19946
|
+
}
|
|
19947
|
+
return response;
|
|
19948
|
+
}
|
|
19949
|
+
priorityExhaustion.mark(candidate, priorityCooldownUntil(Date.now()), "rate_limit_error");
|
|
19950
|
+
claudeLog("priority.exhausted", { profile: candidate, until: priorityCooldownUntil(Date.now()) });
|
|
19951
|
+
lastError = errorPayload;
|
|
19952
|
+
previous = candidate;
|
|
19953
|
+
}
|
|
19954
|
+
if (wantsStream) {
|
|
19955
|
+
return new Response(`event: error
|
|
19956
|
+
data: ${JSON.stringify(lastError)}
|
|
19957
|
+
|
|
19958
|
+
`, {
|
|
19959
|
+
status: 200,
|
|
19960
|
+
headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" }
|
|
19961
|
+
});
|
|
19962
|
+
}
|
|
19963
|
+
return new Response(JSON.stringify(lastError), { status: 429, headers: { "content-type": "application/json" } });
|
|
19964
|
+
}
|
|
19851
19965
|
app.use("/auth/*", requireAuth);
|
|
19852
19966
|
app.get("/", (c) => {
|
|
19853
19967
|
const accept = c.req.header("accept") || "";
|
|
@@ -19915,6 +20029,25 @@ function createProxyServer(config = {}) {
|
|
|
19915
20029
|
}
|
|
19916
20030
|
const outputFormat = parsedOutputFormat.value;
|
|
19917
20031
|
const routingMode = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
|
|
20032
|
+
if (routingMode === "priority" && !c.req.header("x-meridian-profile")) {
|
|
20033
|
+
const effectivePool = getEffectiveProfiles(finalConfig.profiles);
|
|
20034
|
+
if (effectivePool.length > 1) {
|
|
20035
|
+
const { order, unknown } = resolvePriorityOrder(effectivePool.map((p) => p.id), priorityProfileOrderSetting());
|
|
20036
|
+
if (unknown.length > 0)
|
|
20037
|
+
claudeLog("priority.unknown_order_ids", { unknown });
|
|
20038
|
+
const sessionKey = adapter.getSessionId(c, body) || null;
|
|
20039
|
+
const assigned = sessionKey ? priorityAssignments.get(sessionKey) : undefined;
|
|
20040
|
+
let first;
|
|
20041
|
+
if (assigned && order.includes(assigned) && !priorityExhaustion.isExhausted(assigned)) {
|
|
20042
|
+
first = assigned;
|
|
20043
|
+
} else {
|
|
20044
|
+
const pick = choosePriorityProfile(order, (id) => priorityExhaustion.isExhausted(id));
|
|
20045
|
+
first = pick?.id ?? order[0];
|
|
20046
|
+
}
|
|
20047
|
+
const candidates = [first, ...order.filter((id) => id !== first && !priorityExhaustion.isExhausted(id))];
|
|
20048
|
+
return dispatchPriority(c, candidates, sessionKey, body.stream === true);
|
|
20049
|
+
}
|
|
20050
|
+
}
|
|
19918
20051
|
const profile = resolveProfile(finalConfig.profiles, finalConfig.defaultProfile, c.req.header("x-meridian-profile") || undefined, routingMode === "sticky" ? { routingMode, stickySessionKey: adapter.getSessionId(c, body) } : undefined);
|
|
19919
20052
|
const authStatus = await getClaudeAuthStatusAsync(profile.id !== "default" ? profile.id : undefined, Object.keys(profile.env).length > 0 ? profile.env : undefined);
|
|
19920
20053
|
const agentMode = c.req.header("x-opencode-agent-mode") ?? null;
|
|
@@ -20044,7 +20177,7 @@ function createProxyServer(config = {}) {
|
|
|
20044
20177
|
const lineageType = lineageResult.type === "diverged" && !cachedSession ? "new" : lineageResult.type;
|
|
20045
20178
|
const msgCount = Array.isArray(body.messages) ? body.messages.length : 0;
|
|
20046
20179
|
const toolCount = body.tools?.length ?? 0;
|
|
20047
|
-
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}`;
|
|
20180
|
+
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}`;
|
|
20048
20181
|
plog(`[PROXY] ${requestLogLine} msgs=${msgSummary}`);
|
|
20049
20182
|
diagnosticLog2.session(`${requestLogLine}`, requestMeta.requestId);
|
|
20050
20183
|
if (lineageResult.type === "diverged" && profileSessionId && !isIndependentSession) {
|
|
@@ -20241,7 +20374,7 @@ function createProxyServer(config = {}) {
|
|
|
20241
20374
|
const clientTool = requestTools.find((t) => t.name === toolName);
|
|
20242
20375
|
let toolInput = normalizeToolInput(input.tool_input, clientTool?.input_schema);
|
|
20243
20376
|
if (toolName.toLowerCase() === "task" && toolInput?.subagent_type && typeof toolInput.subagent_type === "string") {
|
|
20244
|
-
toolInput = { ...toolInput, subagent_type: resolveAgentAlias(toolInput.subagent_type) };
|
|
20377
|
+
toolInput = { ...toolInput, subagent_type: resolveAgentAlias(toolInput.subagent_type, validAgentNames) };
|
|
20245
20378
|
}
|
|
20246
20379
|
const signature = toolUseSignature(toolName, toolInput);
|
|
20247
20380
|
const isExactDuplicate = capturedSignatures.has(signature);
|
|
@@ -21356,7 +21489,7 @@ data: ${JSON.stringify({ type: "message_stop" })}
|
|
|
21356
21489
|
try {
|
|
21357
21490
|
const parsed = JSON.parse(buffered);
|
|
21358
21491
|
if (typeof parsed.subagent_type === "string") {
|
|
21359
|
-
parsed.subagent_type = resolveAgentAlias(parsed.subagent_type);
|
|
21492
|
+
parsed.subagent_type = resolveAgentAlias(parsed.subagent_type, validAgentNames);
|
|
21360
21493
|
}
|
|
21361
21494
|
fixed = JSON.stringify(parsed);
|
|
21362
21495
|
} catch {}
|
|
@@ -21942,6 +22075,44 @@ data: ${JSON.stringify({
|
|
|
21942
22075
|
resetAdapterFeatures2(adapter);
|
|
21943
22076
|
return c.json({ ok: true });
|
|
21944
22077
|
});
|
|
22078
|
+
app.get("/settings/api/routing", (c) => {
|
|
22079
|
+
const profiles = listProfiles(finalConfig.profiles, finalConfig.defaultProfile);
|
|
22080
|
+
return c.json({
|
|
22081
|
+
routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing")),
|
|
22082
|
+
profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
|
|
22083
|
+
profiles: profiles.map((p) => p.id),
|
|
22084
|
+
envOverride: {
|
|
22085
|
+
routing: Boolean(process.env.MERIDIAN_ROUTING),
|
|
22086
|
+
profileOrder: Boolean(process.env.MERIDIAN_PROFILE_ORDER)
|
|
22087
|
+
}
|
|
22088
|
+
});
|
|
22089
|
+
});
|
|
22090
|
+
app.put("/settings/api/routing", async (c) => {
|
|
22091
|
+
let body;
|
|
22092
|
+
try {
|
|
22093
|
+
body = await c.req.json();
|
|
22094
|
+
} catch {
|
|
22095
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
22096
|
+
}
|
|
22097
|
+
if (body.routing !== undefined) {
|
|
22098
|
+
if (typeof body.routing !== "string" || !["active", "sticky", "priority"].includes(body.routing)) {
|
|
22099
|
+
return c.json({ error: 'routing must be "active", "sticky", or "priority"' }, 400);
|
|
22100
|
+
}
|
|
22101
|
+
setSetting("routing", body.routing);
|
|
22102
|
+
}
|
|
22103
|
+
if (body.profileOrder !== undefined) {
|
|
22104
|
+
if (!Array.isArray(body.profileOrder) || body.profileOrder.some((x) => typeof x !== "string")) {
|
|
22105
|
+
return c.json({ error: "profileOrder must be an array of profile ids" }, 400);
|
|
22106
|
+
}
|
|
22107
|
+
const known = new Set(listProfiles(finalConfig.profiles, finalConfig.defaultProfile).map((p) => p.id));
|
|
22108
|
+
const unknown = body.profileOrder.filter((id) => !known.has(id));
|
|
22109
|
+
if (unknown.length > 0)
|
|
22110
|
+
return c.json({ error: `Unknown profiles: ${unknown.join(", ")}` }, 400);
|
|
22111
|
+
setSetting("profileOrder", body.profileOrder);
|
|
22112
|
+
}
|
|
22113
|
+
plog(`[PROXY] Routing settings updated: routing=${getSetting("routing") ?? "active"} order=${(getSetting("profileOrder") ?? []).join(",") || "(config order)"}`);
|
|
22114
|
+
return c.json({ success: true });
|
|
22115
|
+
});
|
|
21945
22116
|
app.get("/settings/api/pricing", (c) => {
|
|
21946
22117
|
const { BUILTIN_MODEL_PRICING: BUILTIN_MODEL_PRICING2 } = (init_pricing(), __toCommonJS(exports_pricing));
|
|
21947
22118
|
const { getPricingOverrides: getPricingOverrides2 } = (init_pricingStore(), __toCommonJS(exports_pricingStore));
|
|
@@ -22032,10 +22203,16 @@ data: ${JSON.stringify({
|
|
|
22032
22203
|
lastSuccessAt: cacheInfo.lastSuccessAt || null
|
|
22033
22204
|
};
|
|
22034
22205
|
}));
|
|
22206
|
+
const routingModeNow = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
|
|
22207
|
+
const priorityInfo = routingModeNow === "priority" ? {
|
|
22208
|
+
profileOrder: resolvePriorityOrder(profiles.map((p) => p.id), priorityProfileOrderSetting()).order,
|
|
22209
|
+
exhausted: priorityExhaustion.snapshot()
|
|
22210
|
+
} : {};
|
|
22035
22211
|
return c.json({
|
|
22036
22212
|
profiles: enriched,
|
|
22037
22213
|
activeProfile: getActiveProfileId() || finalConfig.defaultProfile || profiles[0]?.id || "default",
|
|
22038
|
-
routing:
|
|
22214
|
+
routing: routingModeNow,
|
|
22215
|
+
...priorityInfo
|
|
22039
22216
|
});
|
|
22040
22217
|
});
|
|
22041
22218
|
app.get("/profiles", async (c) => {
|
|
@@ -22059,10 +22236,17 @@ data: ${JSON.stringify({
|
|
|
22059
22236
|
if (!effective.find((p) => p.id === body.profile)) {
|
|
22060
22237
|
return c.json({ error: `Unknown profile: ${body.profile}. Available: ${effective.map((p) => p.id).join(", ")}` }, 400);
|
|
22061
22238
|
}
|
|
22239
|
+
const previousProfile = getActiveProfileId() ?? null;
|
|
22062
22240
|
setActiveProfile(body.profile);
|
|
22063
22241
|
clearSessionCache();
|
|
22064
22242
|
rateLimitStore.clear();
|
|
22065
|
-
|
|
22243
|
+
claudeLog("profile.switched", {
|
|
22244
|
+
from: previousProfile,
|
|
22245
|
+
to: body.profile,
|
|
22246
|
+
userAgent: c.req.header("user-agent")?.slice(0, 120) ?? null,
|
|
22247
|
+
origin: c.req.header("origin") ?? c.req.header("referer")?.slice(0, 120) ?? null
|
|
22248
|
+
});
|
|
22249
|
+
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)`);
|
|
22066
22250
|
return c.json({ success: true, activeProfile: body.profile });
|
|
22067
22251
|
});
|
|
22068
22252
|
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-aefk7t3r.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 });
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* the SDK may validate against registered alias variants (e.g., "general-purpose"
|
|
23
23
|
* is registered by `addCaseVariants`), but the client expects the canonical
|
|
24
24
|
* agent name from its config ("general").
|
|
25
|
+
*
|
|
26
|
+
* The alias table exists to REPAIR invalid names, never to remap valid ones
|
|
27
|
+
* (#671): a name that already matches a registered agent is returned in the
|
|
28
|
+
* config's canonical casing, and an alias is applied only when its target is
|
|
29
|
+
* itself registered — renaming to a nonexistent agent can only ever fail.
|
|
25
30
|
*/
|
|
26
|
-
export declare function resolveAgentAlias(input: string): string;
|
|
31
|
+
export declare function resolveAgentAlias(input: string, validAgents: string[]): string;
|
|
27
32
|
export declare function fuzzyMatchAgentName(input: string, validAgents: string[]): string;
|
|
28
33
|
//# sourceMappingURL=agentMatch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentMatch.d.ts","sourceRoot":"","sources":["../../src/proxy/agentMatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0CH
|
|
1
|
+
{"version":3,"file":"agentMatch.d.ts","sourceRoot":"","sources":["../../src/proxy/agentMatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0CH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAO9E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAsChF"}
|
|
@@ -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-aefk7t3r.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,QA6PhB,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