Files
magpie/site/public/index.html
T
smallnest 03a01a548d agent: MiMo Code — Xiaomi's fork of OpenCode, sharing its config shape (#98)
* agent: MiMo Code — a terminal coding assistant that is Xiaomi's fork of OpenCode, whose config keeps OpenCode's shape: its own mimocode.json(c) under ~/.config/mimocode, its auth beside it, and a magpie provider block with npm/@ai-sdk/openai-compatible, model and small_model fields. opencode() becomes openCodeLike() for the two to share it, and magpieProviderJSON splits shape from the catalog id (magpieProviderJSONFor), so a visibility narrowed on mimocode (magpie visible mimocode …) narrows its list and leaves OpenCode's alone. The library gives it the same instructions, MCP servers (its mcp key) and skills as OpenCode, except the shared skills go into its own skills/ folder: it reads Claude Code's skills only behind a flag, though it reads its CLAUDE.md like OpenCode, and the page says so. Its requests are told by the mimocode/ User-Agent. Checked against XiaomiMiMo/MiMo-Code

* icon: MiMo Code's own favicon, from packages/ui/src/assets/favicon — its frame with the bottom bar, in the monochrome the app masks to the text colour
2026-09-27 08:42:44 +08:00

1035 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>magpie</title>
<meta name="description" content="One place to pick every agent's model. Codex on DeepSeek, Claude Code on Kimi, Gemini CLI on GLM, from the menu bar, through one local gateway.">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#fafafa">
<script>
// The chosen theme (light, dark, or the system's) is on <html data-theme>
// before anything paints.
(() => {
let mode = "system";
try { mode = localStorage.getItem("theme") || "system"; } catch {}
const q = matchMedia("(prefers-color-scheme: dark)");
const apply = () => {
const t = mode === "system" ? (q.matches ? "dark" : "light") : mode;
document.documentElement.dataset.theme = t;
document.querySelector('meta[name="theme-color"]').content = t === "dark" ? "#0b0b0c" : "#fafafa";
document.querySelectorAll("button.theme").forEach((b) => { b.dataset.mode = mode; b.title = "Theme: " + mode; });
};
q.addEventListener("change", () => mode === "system" && apply());
window.setTheme = (m) => { mode = m; try { m === "system" ? localStorage.removeItem("theme") : localStorage.setItem("theme", m); } catch {} apply(); };
window.themeMode = () => mode;
apply();
})();
</script>
<link rel="icon" href="/favicon.png" type="image/png">
<link rel="apple-touch-icon" href="/img/icon-256.png">
<meta property="og:title" content="magpie">
<meta property="og:description" content="Every agent’s model. One place.">
<meta property="og:image" content="https://usemagpie.ai/img/og.png">
<meta property="og:image:width" content="2400">
<meta property="og:image:height" content="1260">
<meta property="og:image:alt" content="magpie: every agent’s model, one place">
<meta name="twitter:title" content="magpie">
<meta name="twitter:description" content="Every agent’s model. One place.">
<meta name="twitter:image" content="https://usemagpie.ai/img/og.png">
<meta property="og:url" content="https://usemagpie.ai/">
<meta name="twitter:card" content="summary_large_image">
<style>
:root {
--bg: #fafafa;
--bg2: #f1f1f2;
--card: #ffffff;
--ink: #111112;
--ink2: #4a4a50;
--ink3: #85858c;
--line: #e4e4e7;
--btn: #111112;
--btn-ink: #fafafa;
--code: #f4f4f5;
--shadow: 0 1px 2px rgba(0,0,0,.04), 0 12px 40px -12px rgba(0,0,0,.18);
}
:root[data-theme="dark"] {
--bg: #0b0b0c;
--bg2: #121214;
--card: #161618;
--ink: #f2f2f3;
--ink2: #b3b3ba;
--ink3: #7a7a82;
--line: #26262a;
--btn: #f2f2f3;
--btn-ink: #0b0b0c;
--code: #151517;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 20px 60px -20px rgba(0,0,0,.8);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0b0b0c;
--bg2: #121214;
--card: #161618;
--ink: #f2f2f3;
--ink2: #b3b3ba;
--ink3: #7a7a82;
--line: #26262a;
--btn: #f2f2f3;
--btn-ink: #0b0b0c;
--code: #151517;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 20px 60px -20px rgba(0,0,0,.8);
}
}
[data-theme="dark"] .bird { filter: invert(1); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .bird { filter: invert(1); } }
.for-dark { display: none !important; }
[data-theme="dark"] .for-light { display: none !important; }
[data-theme="dark"] .for-dark { display: block !important; }
.theme { display: inline-grid; place-items: center; width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line); background: transparent; color: var(--ink2); cursor: pointer; padding: 0; }
.theme:hover { color: var(--ink); }
.theme svg { width: 16px; height: 16px; display: none; }
.theme.gh svg { display: block; }
nav .wrap > .gh { margin-right: -18px; }
.theme[data-mode="system"] .i-system, .theme[data-mode="light"] .i-light, .theme[data-mode="dark"] .i-dark { display: block; }
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; }
code, pre, kbd { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
nav { position: sticky; top: 0; z-index: 10; backdrop-filter: saturate(1.4) blur(14px); -webkit-backdrop-filter: saturate(1.4) blur(14px); background: color-mix(in srgb, var(--bg) 78%, transparent); border-bottom: 1px solid transparent; transition: border-color .2s; }
nav.scrolled { border-bottom-color: var(--line); }
nav .wrap { display: flex; align-items: center; gap: 28px; height: 60px; }
.brand { display: flex; align-items: center; gap: 9px; font-weight: 650; font-size: 17px; letter-spacing: -.01em; text-decoration: none; }
.brand img { width: 24px; height: 24px; }
nav .links { display: flex; gap: 24px; margin-left: auto; font-size: 14px; color: var(--ink2); }
nav .links a { text-decoration: none; }
nav .links a:hover { color: var(--ink); }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 0 18px; height: 42px; border-radius: 10px; background: var(--btn); color: var(--btn-ink); text-decoration: none; font-weight: 560; font-size: 15px; white-space: nowrap; border: 1px solid var(--btn); transition: transform .12s, opacity .12s; }
.btn:hover { opacity: .88; }
.btn:active { transform: translateY(1px); }
.btn.small { height: 34px; padding: 0 14px; font-size: 14px; border-radius: 8px; }
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn svg { width: 16px; height: 16px; }
.hero { padding: 80px 0 32px; display: grid; grid-template-columns: 1.05fr .95fr; gap: 56px; align-items: center; }
.hero h1 { font-size: clamp(40px, 6vw, 64px); line-height: 1.04; letter-spacing: -.035em; font-weight: 700; margin: 0 0 22px; }
.hero h1 span { color: var(--ink3); }
.hero p.lede { font-size: 19px; color: var(--ink2); margin: 0 0 32px; max-width: 30em; }
.cta { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.meta { margin-top: 14px; font-size: 13px; color: var(--ink3); }
.meta a { color: var(--ink2); }
.shot { position: relative; }
.shot picture img { width: 100%; height: auto; height: auto; max-width: 440px; display: block; margin: 0 auto; border-radius: 14px; box-shadow: var(--shadow); border: 1px solid var(--line); }
.sponsor { padding: 24px 0 8px; text-align: center; }
.sponsor p { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink3); margin: 0 0 14px; }
.sponsor .card { display: inline-flex; align-items: center; gap: 20px; max-width: 100%; padding: 12px 20px 12px 22px; background: var(--card); border: 1px solid var(--line); border-radius: 14px; text-decoration: none; text-align: left; transition: border-color .2s, box-shadow .2s, transform .2s; }
.sponsor .card:hover { border-color: color-mix(in srgb, var(--ink) 18%, var(--line)); box-shadow: var(--shadow); transform: translateY(-1px); }
.sponsor .card img { flex: none; width: 124px; height: auto; opacity: .92; }
.sponsor .card hr { flex: none; width: 1px; height: 28px; margin: 0; border: 0; background: var(--line); }
.sponsor .card span { font-size: 14px; line-height: 1.5; color: var(--ink2); }
.sponsor .card i { flex: none; font-style: normal; color: var(--ink3); transition: color .2s, transform .2s; }
.sponsor .card:hover i { color: var(--ink); transform: translateX(2px); }
.sponsor .more { display: inline-block; margin-top: 10px; font-size: 12.5px; color: var(--ink3); text-decoration: none; }
.sponsor .more:hover { color: var(--ink2); }
.community { text-align: center; padding: 110px 0 0; }
.community a { display: inline-flex; align-items: center; gap: 16px; max-width: 100%; padding: 16px 22px 16px 16px; background: var(--card); border: 1px solid var(--line); border-radius: 16px; text-decoration: none; text-align: left; transition: border-color .2s, box-shadow .2s, transform .2s; }
.community a:hover { border-color: color-mix(in srgb, var(--ink) 18%, var(--line)); box-shadow: var(--shadow); transform: translateY(-1px); }
.community .glyph { flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; color: #fff; background: #5865f2; }
.community .glyph svg { width: 24px; height: 24px; }
.community b { display: block; font-size: 15px; font-weight: 600; color: var(--ink); }
.community span { display: block; font-size: 13.5px; line-height: 1.5; color: var(--ink2); }
.community i { flex: none; margin-left: 8px; font-style: normal; color: var(--ink3); transition: color .2s, transform .2s; }
.community a:hover i { color: var(--ink); transform: translateX(2px); }
.community + .final { padding-top: 90px; }
.agents { padding: 40px 0 16px; text-align: center; }
.agents p { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink3); margin: 0 0 22px; }
.agents ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 22px; }
.agents li { display: flex; align-items: center; gap: 8px; font-size: 14.5px; color: var(--ink2); }
.agents img { width: 22px; height: 22px; border-radius: 5px; }
section { padding: 88px 0 0; }
.eyebrow { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink3); margin: 0 0 12px; }
h2 { font-size: clamp(30px, 4vw, 42px); line-height: 1.1; letter-spacing: -.03em; margin: 0 0 16px; font-weight: 680; }
.sub { font-size: 18px; color: var(--ink2); max-width: 36em; margin: 0 0 40px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; }
.grid > div { background: var(--card); padding: 28px 26px 30px; }
.grid h3 { margin: 0 0 8px; font-size: 16.5px; letter-spacing: -.01em; font-weight: 640; }
.grid p { margin: 0; color: var(--ink2); font-size: 15px; }
.grid code { font-size: 13px; background: var(--code); padding: 1px 5px; border-radius: 4px; }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }
.split img { width: 100%; height: auto; height: auto; max-width: 440px; display: block; margin: 0 auto; border-radius: 14px; box-shadow: var(--shadow); border: 1px solid var(--line); }
.split ul { padding: 0; margin: 0; list-style: none; }
.split li { padding: 14px 0; border-top: 1px solid var(--line); color: var(--ink2); font-size: 15.5px; }
.split li b { color: var(--ink); font-weight: 600; }
pre { margin: 0; background: var(--code); border: 1px solid var(--line); border-radius: 12px; padding: 20px 22px; overflow-x: auto; font-size: 13.5px; line-height: 1.75; color: var(--ink); }
pre .c { color: var(--ink3); }
.table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
.table th, .table td { text-align: left; padding: 11px 12px; border-bottom: 1px solid var(--line); }
.table th { font-weight: 560; color: var(--ink3); font-size: 13px; }
.table td:first-child { font-weight: 560; }
.table code { font-size: 13px; }
.install { display: flex; align-items: center; gap: 12px; background: var(--code); border: 1px solid var(--line); border-radius: 12px; padding: 6px 6px 6px 18px; max-width: 560px; }
.install code { flex: 1; font-size: 14px; overflow-x: auto; white-space: nowrap; }
.install code::before { content: "$ "; color: var(--ink3); }
.copy { flex: none; height: 34px; padding: 0 12px; border-radius: 8px; border: 1px solid var(--line); background: var(--card); color: var(--ink); font: inherit; font-size: 13px; cursor: pointer; }
.copy:hover { border-color: var(--ink3); }
.final { text-align: center; padding: 120px 0 100px; }
.final img { width: 88px; height: 88px; margin-bottom: 18px; }
.final .cta { justify-content: center; }
.btn .os-windows, .btn .os-linux { display: none; }
html[data-os="windows"] .btn .os-mac, html[data-os="linux"] .btn .os-mac { display: none; }
html[data-os="windows"] .btn .os-windows, html[data-os="linux"] .btn .os-linux { display: block; }
.plats { display: flex; justify-content: center; flex-wrap: wrap; gap: 12px; margin-top: 40px; scroll-margin-top: 90px; }
.plats > div { display: grid; grid-template-columns: auto auto; gap: 4px 14px; align-items: baseline; text-align: left; padding: 14px 18px; flex: 0 1 280px; min-width: 250px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); font-size: 13.5px; }
.plats b { font-weight: 600; font-size: 14.5px; color: var(--ink); }
.plats span { color: var(--ink3); font-size: 12.5px; justify-self: end; }
.plats a { color: var(--ink2); text-decoration: none; }
.plats a:hover { color: var(--ink); text-decoration: underline; }
footer { border-top: 1px solid var(--line); padding: 28px 0 40px; font-size: 13.5px; color: var(--ink3); }
footer .wrap { display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: center; }
footer .links { margin-left: auto; display: flex; gap: 20px; }
footer a { text-decoration: none; }
footer a:hover { color: var(--ink); }
.shot.stack { position: relative; }
.shot img.pick { width: 100%; height: auto; height: auto; max-width: 440px; display: block; margin: 0 auto; border-radius: 14px; box-shadow: var(--shadow); border: 1px solid var(--line); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 30px; }
.chip { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink2); background: var(--card); border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px 5px 8px; }
.chip img { width: 17px; height: 17px; }
.chip b { color: var(--ink); font-weight: 580; }
.chip i { font-style: normal; color: var(--ink3); }
.flow { display: grid; grid-template-columns: 1fr auto 1.1fr auto 1fr; gap: 18px; align-items: center; }
.col { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px; }
.col h4 { margin: 0 0 12px; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink3); font-weight: 560; }
.col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.col li { display: flex; align-items: center; gap: 9px; font-size: 14.5px; }
.col li img { width: 18px; height: 18px; }
.col li span { color: var(--ink3); font-size: 13px; margin-left: auto; }
.hub { background: var(--card); color: var(--ink); border: 1px solid color-mix(in srgb, var(--ink) 22%, var(--line)); border-radius: 18px; padding: 24px 22px; text-align: center; box-shadow: var(--shadow); }
.hub img { width: 44px; height: 44px; }
.hub b { display: block; font-size: 18px; margin: 8px 0 4px; letter-spacing: -.01em; }
.hub code { font-size: 12.5px; color: var(--ink3); }
.hub ul { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 6px; font-size: 13px; color: var(--ink2); }
.hub li { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; }
.arrow { color: var(--ink3); font-size: 22px; }
@keyframes live { 70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
.rt { background: var(--card); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); padding: 18px 20px 16px; margin: 0 0 28px; }
.rt-top { display: flex; align-items: center; gap: 12px 16px; flex-wrap: wrap; }
.rt-tabs { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg2); border: 1px solid var(--line); border-radius: 10px; max-width: 100%; overflow-x: auto; }
.rt-tabs button { font: inherit; font-size: 13.5px; border: 0; background: none; color: var(--ink2); padding: 6px 12px; border-radius: 7px; cursor: pointer; white-space: nowrap; }
.rt-tabs button:hover { color: var(--ink); }
.rt-tabs button[aria-selected="true"] { background: var(--card); color: var(--ink); font-weight: 560; box-shadow: 0 0 0 1px var(--line), 0 1px 2px rgb(0 0 0 / .08); }
.rt-mode { margin: 0; flex: 1 1 20em; font-size: 14px; color: var(--ink2); }
.rt-stage { position: relative; display: grid; grid-template-columns: 150px 150px minmax(0, 1fr); column-gap: 76px; align-items: center; padding: 28px 2px; }
.rt-wires { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.rt-wires path { fill: none; stroke: var(--line); stroke-width: 1.5; transition: opacity .3s; }
.rt-wires path.live { stroke: var(--ink3); stroke-dasharray: 3 5; animation: rtflow .7s linear infinite; }
.rt-wires path.rest { stroke-dasharray: 2 6; opacity: .45; }
@keyframes rtflow { to { stroke-dashoffset: -8; } }
.rt-wires .pkt { fill: var(--ink); filter: drop-shadow(0 0 3px var(--ink3)); }
.rt-wires .pkt.back { fill: var(--card); stroke: var(--ink); stroke-width: 2; }
.rt-node { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 16px 10px 14px; background: var(--card); border: 1px solid var(--line); border-radius: 14px; text-align: center; }
.rt-node img { width: 30px; height: 30px; margin-bottom: 4px; }
.rt-node b { font-size: 14px; font-weight: 600; }
.rt-node small { font-size: 12px; color: var(--ink3); }
.rt-hub { border-color: var(--ink); box-shadow: 0 0 0 4px var(--bg2); }
.rt-hub i { font-style: normal; font-size: 11.5px; font-weight: 560; margin-top: 6px; padding: 2px 9px; border-radius: 999px; background: var(--ink); color: var(--card); }
.rt-hub.tick, .rt-src.tick { animation: rttick .4s ease-out; }
@keyframes rttick { 40% { transform: scale(1.04); } }
.rt-accts { position: relative; z-index: 1; list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.rt-accts li { position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; column-gap: 12px; row-gap: 7px; padding: 11px 14px; background: var(--card); border: 1px solid var(--line); border-radius: 12px; font-size: 14px; transition: opacity .3s, border-color .3s; }
.rt-accts b { font-weight: 560; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rt-accts b span { font-weight: 450; color: var(--ink3); }
.rt-accts b img { width: 18px; height: 18px; margin-right: 8px; vertical-align: -3.5px; }
.rt-accts em { font-style: normal; font-size: 12.5px; color: var(--ink3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.rt-accts .bar { grid-column: 2 / 4; height: 4px; border-radius: 2px; background: var(--bg2); overflow: hidden; }
.rt-accts .bar i { display: block; height: 100%; width: 0; border-radius: 2px; background: var(--ink3); transition: width .4s; }
.rt-accts li.nobar .bar { visibility: hidden; }
.rt-accts.models .bar { display: none; }
.rt-accts .dot { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--ink3); }
.rt-accts li.on { border-color: var(--ink); }
.rt-accts li.on em { color: var(--ink); }
.rt-accts li.on .bar i { background: var(--ink); }
.rt-accts li.on .dot { background: var(--ink); border-color: var(--ink); box-shadow: 0 0 0 0 color-mix(in srgb, var(--ink) 40%, transparent); animation: live 1.8s infinite; }
.rt-accts li.low em { color: var(--ink2); }
.rt-accts li.rest { border-style: dashed; }
.rt-accts li.rest > :not(.tag) { opacity: .5; }
.rt-accts li.hit { animation: rthit .45s; }
@keyframes rthit { 20% { transform: translateX(-5px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(1px); } }
.rt-accts .tag { position: absolute; right: 12px; top: -10px; padding: 2px 8px; border-radius: 6px; background: var(--ink); color: var(--card); font: 600 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; opacity: 0; transform: translateY(4px) scale(.9); transition: opacity .25s, transform .25s; pointer-events: none; }
.rt-accts li.tagged .tag { opacity: 1; transform: none; }
.rt-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px 24px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--line); }
.rt-cap { flex: 1 1 24em; margin: 0; min-height: 3em; display: flex; align-items: center; font-size: 14.5px; color: var(--ink); }
.rt-cap.in { animation: rtin .4s ease-out; }
@keyframes rtin { from { opacity: 0; transform: translateY(4px); } }
.rt-stats { display: flex; gap: 22px; font-size: 12px; color: var(--ink3); }
.rt-stats span { display: flex; flex-direction: column; }
.rt-stats b { font-size: 22px; line-height: 1.2; font-weight: 650; letter-spacing: -.02em; color: var(--ink); font-variant-numeric: tabular-nums; }
.grid.rt-points { grid-template-columns: repeat(4, 1fr); }
@media (prefers-reduced-motion: reduce) { .rt-accts li.on .dot, .rt-wires path.live, .rt-accts li.hit, .rt-hub.tick, .rt-src.tick, .rt-cap.in { animation: none; } }
@media (max-width: 860px) {
.rt-stage { grid-template-columns: 1fr 1fr; column-gap: 40px; row-gap: 30px; }
.rt-accts { grid-column: 1 / -1; padding-left: 26px; }
.grid.rt-points { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 560px) {
.rt { padding: 14px 12px 14px; }
.rt-stage { column-gap: 24px; }
.rt-accts li { grid-template-columns: 10px minmax(0, 1fr); padding: 10px 12px; }
.rt-accts em { grid-column: 2; white-space: normal; }
.rt-accts .bar { grid-column: 2; }
.grid.rt-points { grid-template-columns: 1fr; }
}
.wide img { width: 100%; height: auto; height: auto; display: block; border-radius: 14px; box-shadow: var(--shadow); border: 1px solid var(--line); }
.subs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.subs > div { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 22px; }
.subs .who { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }
.subs .who img { width: 20px; height: 20px; }
.subs p { margin: 0; color: var(--ink2); font-size: 15px; }
.subs code { font-size: 13px; }
@media (max-width: 860px) {
.hero { grid-template-columns: 1fr; padding-top: 56px; gap: 44px; }
.split { grid-template-columns: 1fr; }
.split.rev > :first-child { order: 2; }
.flow { grid-template-columns: 1fr; }
.arrow { transform: rotate(90deg); text-align: center; }
.subs { grid-template-columns: 1fr; }
.grid { grid-template-columns: 1fr 1fr; }
nav .links a.hide { display: none; }
}
@media (max-width: 560px) {
.wrap { padding: 0 16px; }
.grid { grid-template-columns: 1fr; }
.sponsor .card { flex-direction: column; gap: 10px; padding: 16px 20px; text-align: center; }
.sponsor .card hr, .sponsor .card i { display: none; }
nav .links { gap: 16px; }
nav .wrap { gap: 14px; }
nav .links a.hide-sm { display: none; }
nav .wrap > .gh { margin-right: -6px; }
section { padding-top: 64px; }
.table .path { display: none; }
}
</style>
</head>
<body>
<nav id="nav"><div class="wrap">
<a class="brand" href="/"><img class="bird" src="/img/magpie-small.svg" alt="">magpie</a>
<div class="links">
<a class="hide" href="#how">How it works</a>
<a class="hide" href="#routing">Routing</a>
<a class="hide" href="#providers">Providers</a>
<a href="#cli">CLI</a>
<a class="hide-sm" href="/docs/start">Docs</a>
<a class="hide-sm" href="https://github.com/yetone/magpie-releases/releases">Releases</a>
</div>
<a class="theme gh" href="https://discord.gg/vGSnD3ZKQF" aria-label="magpie on Discord" title="Join us on Discord"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg></a>
<a class="theme gh" href="https://github.com/yetone/magpie" aria-label="magpie on GitHub" title="Source on GitHub"><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg></a>
<button class="theme" type="button" aria-label="Theme" title="Theme: system"><svg class="i-system" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1.75" y="2.75" width="12.5" height="8.5" rx="1.5"/><path d="M5.5 14h5M8 11.25V14"/></svg><svg class="i-light" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="3"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3 3l1 1M12 12l1 1M3 13l1-1M12 4l1-1"/></svg><svg class="i-dark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M13.5 9.5A5.5 5.5 0 0 1 6.5 2.5a5.5 5.5 0 1 0 7 7Z"/></svg></button>
<a class="btn small dl" href="/download">Download</a>
</div></nav>
<main class="wrap">
<header class="hero">
<div>
<h1>Every agent’s model. <span>One place.</span></h1>
<p class="lede">magpie gives every agent every model. Add a provider once — DeepSeek, Kimi, GLM, Qwen, OpenRouter, Ollama, or the Claude and ChatGPT plans you already pay for — and pick it from Codex, Claude Code, Gemini CLI or any other agent, in one click.</p>
<div class="chips">
<span class="chip"><img src="/icons/codex-color.svg" alt=""><b>Codex</b><i>→</i><img src="/icons/deepseek-color.svg" alt="">DeepSeek V4 Pro</span>
<span class="chip"><img src="/icons/claudecode-color.svg" alt=""><b>Claude Code</b><i>→</i><img class="bird" src="/icons/kimi.svg" alt="">Kimi K3</span>
<span class="chip"><img src="/icons/geminicli-color.svg" alt=""><b>Gemini CLI</b><i>→</i><img src="/icons/zhipu-color.svg" alt="">GLM-5.3</span>
</div>
<div class="cta">
<a class="btn dl" href="/download">
<svg class="os-mac" viewBox="0 0 16 16" fill="currentColor"><path d="M11.2 8.5c0-1.6 1.3-2.4 1.4-2.4-.8-1.1-2-1.3-2.4-1.3-1-.1-2 .6-2.5.6s-1.3-.6-2.2-.6C4.4 4.8 3.3 5.5 2.7 6.6c-1.2 2.1-.3 5.2.9 6.9.6.8 1.2 1.7 2.1 1.7.8 0 1.2-.5 2.2-.5s1.3.5 2.2.5 1.5-.9 2-1.7c.6-.9.9-1.8.9-1.9 0 0-1.8-.7-1.8-3.1ZM9.6 3.6c.5-.6.8-1.3.7-2.1-.7 0-1.5.5-2 1-.4.5-.8 1.3-.7 2.1.8.1 1.5-.4 2-1Z"/></svg>
<svg class="os-windows" viewBox="0 0 16 16" fill="currentColor"><path d="M1 2.3 6.7 1.5v5.6H1V2.3Zm6.4-.9L15 .3v6.8H7.4V1.4ZM1 7.9h5.7v5.6L1 12.7V7.9Zm6.4 0H15v6.8l-7.6-1.1V7.9Z"/></svg>
<svg class="os-linux" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8.5M4.5 7 8 10.5 11.5 7M2.5 13.5h11"/></svg>
<span class="dl-label">Download for Mac</span>
</a>
<a class="btn ghost" href="#cli">Install from terminal</a>
</div>
<div class="meta"><span class="ver">Free</span> · <a href="https://github.com/yetone/magpie">Open source</a> · <span class="os-note">macOS 11+ · <a class="alt" href="/download/mac-intel">Intel Mac</a></span> · <a href="#get">Windows &amp; Linux</a></div>
</div>
<div class="shot">
<img loading="lazy" class="pick for-light" src="/img/picker-light.png" width="440" height="555" alt="Codex's model picker in magpie, listing DeepSeek, Kimi and GLM models">
<img loading="lazy" class="pick for-dark" src="/img/picker-dark.png" width="440" height="555" alt="Codex's model picker in magpie, listing DeepSeek, Kimi and GLM models">
</div>
</header>
<aside class="sponsor" aria-label="Sponsor">
<p>Sponsored by</p>
<a class="card" href="https://yylx.io" target="_blank" rel="noopener sponsored">
<img class="bird" src="/img/sponsor-yylx.png" width="640" height="184" alt="鱼鱼连线 YYLX.IO AI Router">
<hr>
<span>One API gateway for Claude and GPT, tuned for Claude Code.</span>
<i aria-hidden="true">→</i>
</a>
<br><a class="more" href="mailto:yetoneful@gmail.com?subject=Sponsoring%20magpie">Become a sponsor</a>
</aside>
<div class="agents">
<p>Every agent</p>
<ul>
<li><img src="/icons/claudecode-color.svg" alt="">Claude Code</li>
<li><img src="/icons/codex-color.svg" alt="">Codex</li>
<li><img src="/icons/geminicli-color.svg" alt="">Gemini CLI</li>
<li><img class="bird" src="/icons/opencode.svg" alt="">OpenCode</li>
<li><img class="bird" src="/icons/mimocode.svg" alt="">MiMo Code</li>
<li><img class="bird" src="/icons/pi.svg" alt="">Pi</li>
<li><img class="bird" src="/icons/goose.svg" alt="">Goose</li>
<li><img class="bird" src="/icons/cursor.svg" alt="">Cursor CLI</li>
<li><img class="bird" src="/icons/githubcopilot.svg" alt="">Copilot CLI</li>
<li><img src="/icons/crush.png" alt="">Crush</li>
<li><img src="/icons/deepseek-color.svg" alt="">DeepSeek Harness</li>
<li><img class="bird" src="/icons/commandcode.svg" alt="">Command Code</li>
<li><img class="bird" src="/icons/omp.svg" alt="">omp</li>
<li><img class="bird" src="/icons/devin.svg" alt="">Devin</li>
<li><img class="bird" src="/icons/hermes.svg" alt="">Hermes Agent</li>
<li><img class="bird" src="/icons/xai.svg" alt="">Grok Build</li>
<li><img src="/icons/zcode.png" alt="">ZCode</li>
<li><img src="/icons/alma.png" alt="">Alma</li>
</ul>
<p style="margin-top:34px">Every model</p>
<ul>
<li><img src="/icons/deepseek-color.svg" alt="">DeepSeek</li>
<li><img class="bird" src="/icons/kimi.svg" alt="">Kimi</li>
<li><img src="/icons/zhipu-color.svg" alt="">GLM</li>
<li><img src="/icons/qwen-color.svg" alt="">Qwen</li>
<li><img src="/icons/minimax-color.svg" alt="">MiniMax</li>
<li><img src="/icons/claude-color.svg" alt="">Claude</li>
<li><img class="bird" src="/icons/openai.svg" alt="">OpenAI</li>
<li><img src="/icons/gemini-color.svg" alt="">Gemini</li>
<li><img src="/icons/mistral-color.svg" alt="">Mistral</li>
<li><img class="bird" src="/icons/xai.svg" alt="">xAI</li>
<li><img class="bird" src="/icons/groq.svg" alt="">Groq</li>
<li><img class="bird" src="/icons/openrouter.svg" alt="">OpenRouter</li>
<li><img class="bird" src="/icons/ollama.svg" alt="">Ollama</li>
</ul>
</div>
<section id="how">
<p class="eyebrow">How it works</p>
<h2>One gateway speaks every API.</h2>
<p class="sub">Codex only speaks OpenAI Responses. Claude Code only speaks Anthropic Messages. magpie runs a gateway on your machine that speaks all of them and translates in both directions — streaming, tool calls and reasoning included — so any agent can use any vendor’s model.</p>
<div class="flow">
<div class="col">
<h4>Your agents</h4>
<ul>
<li><img src="/icons/codex-color.svg" alt="">Codex<span>Responses</span></li>
<li><img src="/icons/claudecode-color.svg" alt="">Claude Code<span>Messages</span></li>
<li><img src="/icons/geminicli-color.svg" alt="">Gemini CLI<span>Gemini</span></li>
<li><img class="bird" src="/icons/opencode.svg" alt="">OpenCode<span>Chat</span></li>
<li><img class="bird" src="/icons/goose.svg" alt="">Goose<span>Chat</span></li>
</ul>
</div>
<div class="arrow">→</div>
<div class="hub">
<img class="bird" src="/img/magpie.svg" alt="">
<b>magpie gateway</b>
<code>127.0.0.1:3425</code>
<ul><li>OpenAI Chat · Responses</li><li>Anthropic Messages</li><li>Google Gemini</li></ul>
</div>
<div class="arrow">→</div>
<div class="col">
<h4>Any provider</h4>
<ul>
<li><img src="/icons/deepseek-color.svg" alt="">DeepSeek</li>
<li><img class="bird" src="/icons/kimi.svg" alt="">Kimi</li>
<li><img src="/icons/zhipu-color.svg" alt="">GLM</li>
<li><img src="/icons/claude-color.svg" alt="">Your Claude plan</li>
<li><img class="bird" src="/icons/openai.svg" alt="">Your ChatGPT plan</li>
</ul>
</div>
</div>
</section>
<section>
<div class="split rev">
<div>
<img class="for-light" loading="lazy" src="/img/panel-light.png" width="440" height="470" alt="The magpie menu bar panel: each agent on a different vendor's model">
<img class="for-dark" loading="lazy" src="/img/panel-dark.png" width="440" height="470" alt="The magpie menu bar panel: each agent on a different vendor's model">
</div>
<div>
<p class="eyebrow">The menu bar</p>
<h2>Every agent, its own model.</h2>
<p class="sub">One glance shows what each agent runs. Click a value to change it; magpie rewrites that agent’s config — only the key you changed — and the next session uses it.</p>
<ul>
<li><b>Codex</b> gets DeepSeek in its own <code>/model</code> list, with the right reasoning efforts.</li>
<li><b>Claude Code</b> gets Kimi, GLM or GPT behind <code>opus</code>, <code>sonnet</code> and <code>haiku</code>.</li>
<li><b>Pick the native model</b> and magpie puts back exactly what was there before.</li>
<li><b>Profiles</b> switch every agent at once — “Budget”, “Focus”, whatever you save.</li>
</ul>
</div>
</div>
</section>
<section id="subscriptions">
<p class="eyebrow">Subscriptions, shared</p>
<h2>The plans you pay for, in every agent.</h2>
<p class="sub">Signed in to an agent? That login becomes a provider. Its models show up in every other agent’s picker — nothing copied, no key to paste, tokens refreshed the way the agent does it.</p>
<div class="subs">
<div><div class="who"><img src="/icons/claude-color.svg" alt="">Claude Pro / Max</div><p>Your Claude Code sign-in powers OpenCode, Pi or Goose: <code>claude/claude-sonnet-5</code>.</p></div>
<div><div class="who"><img class="bird" src="/icons/openai.svg" alt="">ChatGPT Plus / Pro</div><p>Your Codex sign-in puts GPT in Claude Code: <code>codex/gpt-5.5</code>.</p></div>
<div><div class="who"><img class="bird" src="/icons/githubcopilot.svg" alt="">GitHub Copilot</div><p>Your Copilot seat serves any agent: <code>copilot/claude-sonnet-4.5</code>.</p></div>
</div>
</section>
<section id="routing">
<p class="eyebrow">Smart routing</p>
<h2>The right model for each turn. It never runs dry.</h2>
<p class="sub">Say what each model is for — "a quick question", "writing or fixing tests" — and as each turn begins a small, fast model tells magpie which it is: the turn goes to that model, and its tool rounds stay there. <a href="/docs/intent">How it works</a>. And add every key and subscription you have — several Claude, ChatGPT, Copilot or Grok accounts, several keys for one vendor — and turn them all on. For each request magpie picks the account that should answer, and when one can't, the next answers before your agent ever sees an error.</p>
<div class="rt" id="rt">
<div class="rt-top">
<div class="rt-tabs" role="tablist" aria-label="Routing">
<button role="tab" data-mode="intent" aria-selected="true">By intent</button>
<button role="tab" data-mode="smart" aria-selected="false">Smart</button>
<button role="tab" data-mode="order" aria-selected="false">In order</button>
<button role="tab" data-mode="turn" aria-selected="false">In turn</button>
<button role="tab" data-mode="least" aria-selected="false">Least used first</button>
</div>
<p class="rt-mode">Smart, the default: the account that resets soonest goes first, so no allowance is left to go to waste; any that can't answer are passed over. (Here each request stands for an hour.)</p>
</div>
<div class="rt-stage">
<svg class="rt-wires" aria-hidden="true"></svg>
<div class="rt-node rt-src"><img src="/icons/claudecode-color.svg" alt=""><b>Claude Code</b><small>your agent</small></div>
<div class="rt-node rt-hub"><img class="bird" src="/img/magpie-small.svg" alt=""><b>magpie</b><small>localhost:3425</small><i>Smart</i></div>
<ol class="rt-accts">
<li class="on"><i class="dot"></i><b><img src="/icons/claude-color.svg" alt="">me@work.dev <span>Claude Max</span></b><em>serving</em><div class="bar"><i style="width:72%"></i></div><span class="tag"></span></li>
<li><i class="dot"></i><b><img src="/icons/claude-color.svg" alt="">me@home.dev <span>Claude Pro</span></b><em>38% used · resets in 2 d</em><div class="bar"><i style="width:38%"></i></div><span class="tag"></span></li>
<li><i class="dot"></i><b><img class="bird" src="/icons/openai.svg" alt="">me@home.dev <span>ChatGPT Pro</span></b><em>30% used · resets in 4 d</em><div class="bar"><i style="width:30%"></i></div><span class="tag"></span></li>
<li class="nobar"><i class="dot"></i><b><img class="bird" src="/icons/openrouter.svg" alt="">sk-or-…4f2a <span>OpenRouter</span></b><em>pay as you go</em><div class="bar"><i></i></div><span class="tag"></span></li>
<li><i class="dot"></i><b><img class="bird" src="/icons/githubcopilot.svg" alt="">octo-dev <span>Copilot Business</span></b><em>24% used · resets in 12 d</em><div class="bar"><i style="width:24%"></i></div><span class="tag"></span></li>
</ol>
</div>
<div class="rt-foot">
<p class="rt-cap" aria-live="polite">Claude Code sends every request to magpie; magpie decides which account answers it.</p>
<div class="rt-stats"><span><b>0</b>requests</span><span><b>0</b>rerouted</span><span><b>0</b>errors your agent saw</span></div>
</div>
</div>
<div class="grid rt-points">
<div><h3>Reset-aware</h3><p>The account that resets soonest goes first — what it has left would be lost at the reset. It keeps the requests while it has quota to spare, so its prompt cache stays warm.</p></div>
<div><h3>Out of credit</h3><p>A key whose balance ran out sits out half an hour; requests go to one that can still pay.</p></div>
<div><h3>Rate limits &amp; quotas</h3><p>An account the vendor has limited is passed over for as long as the vendor asks — its Retry-After and reset headers, no guessing, no hammering.</p></div>
<div><h3>Failover</h3><p>A failing account steps back a minute, longer each time it fails again; the request is retried on the next before a byte of the reply reaches your agent.</p></div>
</div>
</section>
<section id="providers">
<p class="eyebrow">Providers</p>
<h2>Paste a key. That’s the setup.</h2>
<p class="sub">Twenty presets — vendors, relays and local servers. magpie asks each vendor which models it serves, so a model released this morning is in every picker on the next refresh. Keys stay in magpie; agents never see them.</p>
<div class="wide">
<img class="for-light" loading="lazy" src="/img/providers-light.png" width="1040" height="507" alt="magpie's Providers tab: DeepSeek, Kimi, GLM, Qwen, MiniMax, OpenRouter, Anthropic and Gemini, each with the agents using it">
<img class="for-dark" loading="lazy" src="/img/providers-dark.png" width="1040" height="507" alt="magpie's Providers tab: DeepSeek, Kimi, GLM, Qwen, MiniMax, OpenRouter, Anthropic and Gemini, each with the agents using it">
</div>
</section>
<section id="features">
<p class="eyebrow">Details</p>
<h2>Small, careful, and out of the way.</h2>
<div class="grid" style="margin-top:32px">
<div><h3>Surgical config edits</h3><p>Comments, ordering and indentation in <code>settings.json</code>, <code>config.toml</code> or <code>config.yaml</code> survive. Writes are atomic.</p></div>
<div><h3>Tiny, native, open</h3><p>Under 15 MB, on the system webview. No Electron, no runtime. MIT-licensed, <a href="https://github.com/yetone/magpie">on GitHub</a>.</p></div>
<div><h3>No keys from your shell</h3><p>magpie never reads API keys from environment variables. What you add is what it uses.</p></div>
<div><h3>Any gateway client</h3><p>Anything with a base-URL setting can use it: <code>OPENAI_BASE_URL</code>, <code>ANTHROPIC_BASE_URL</code>, <code>GOOGLE_GEMINI_BASE_URL</code>.</p></div>
<div><h3>Usage, per agent</h3><p>Tokens, cache hits and cost for each agent and model that went through the gateway.</p></div>
<div><h3>Updates itself</h3><p>New versions download quietly and install when you restart. Signed and notarised on the Mac.</p></div>
</div>
</section>
<section id="cli">
<p class="eyebrow">Terminal</p>
<h2>Also a TUI and a CLI.</h2>
<p class="sub">The same app, from a terminal. The installer puts the <code>magpie</code> command in <code>~/.local/bin</code> and the app where it belongs: <code>/Applications</code> on a Mac, your app menu on Linux.</p>
<div class="install"><code id="cmd">curl -fsSL https://usemagpie.ai/install.sh | sh</code><button class="copy" data-copy="cmd">Copy</button></div>
<p class="meta" style="margin:12px 0 28px">On Windows, <code>magpie.exe</code> takes the same commands; <code>magpie-cli-windows-amd64.exe</code> from the <a href="https://github.com/yetone/magpie-releases/releases/latest">latest release</a> is the terminal-only build.</p>
<pre><span class="c"># add providers</span>
magpie provider add deepseek sk-… <span class="c"># a preset needs only the key</span>
magpie provider add moonshot sk-…
magpie provider add ollama <span class="c"># local servers need none</span>
<span class="c"># give agents third-party models</span>
magpie codex deepseek/deepseek-v4-pro
magpie claude moonshot/kimi-k3
magpie gemini zhipu/glm-5.3
magpie opencode claude/claude-sonnet-5 <span class="c"># your Claude plan, in OpenCode</span>
<span class="c"># one model over several providers</span>
magpie group add opus models=claude/claude-opus-5-5,copilot/claude-opus-5.5 routing=order
magpie claude group/opus
magpie ls <span class="c"># every agent and its model</span>
magpie save work &amp;&amp; magpie use work <span class="c"># profiles</span>
magpie tui <span class="c"># the whole app, in the terminal</span></pre>
</section>
<aside class="community" aria-label="Community">
<a href="https://discord.gg/vGSnD3ZKQF" target="_blank" rel="noopener">
<span class="glyph"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg></span>
<div><b>Join the magpie community</b><span>Ask questions, share your setup, tell us what to build next — on Discord.</span></div>
<i aria-hidden="true">→</i>
</a>
</aside>
<div class="final">
<img class="bird" src="/img/magpie.svg" alt="">
<h2>Every agent. Every model.</h2>
<p class="sub" style="margin:0 auto 32px">Free, open source, native. For macOS, Windows and Linux.</p>
<div class="cta"><a class="btn dl" href="/download"><span class="dl-label">Download for Mac</span></a><a class="btn ghost" href="https://github.com/yetone/magpie"><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg> View on GitHub</a></div>
<div class="plats" id="get">
<div><b>macOS</b><span>11 or later</span><a href="/download/mac-arm64">Apple Silicon</a><a href="/download/mac-intel">Intel</a></div>
<div><b>Windows</b><span>10 or later</span><a href="/download/windows-amd64">x64</a><a href="/download/windows-arm64">ARM</a></div>
<div><b>Linux</b><span>GTK 3 + WebKitGTK 4.1</span><a href="/download/linux-amd64">x86-64</a><a href="/download/linux-arm64">ARM64</a></div>
</div>
<div class="meta"><span class="ver"></span></div>
</div>
</main>
<footer><div class="wrap">
<span>© 2026 magpie · MIT</span>
<div class="links">
<a href="/docs/start">Get started</a>
<a href="/docs/import">Add to magpie</a>
<a href="https://github.com/yetone/magpie">GitHub</a>
<a href="https://discord.gg/vGSnD3ZKQF">Discord</a>
<a href="https://github.com/yetone/magpie-releases/releases">Releases</a>
<a href="/api/latest">Update feed</a>
<a href="https://x.com/yetone">@yetone</a>
</div>
</div></footer>
<script>
(() => {
const nav = document.getElementById("nav");
// The theme button steps system → light → dark.
const next = { system: "light", light: "dark", dark: "system" };
document.querySelectorAll("button.theme").forEach((b) => {
b.dataset.mode = themeMode();
b.title = "Theme: " + themeMode();
b.addEventListener("click", () => setTheme(next[themeMode()]));
});
const onScroll = () => nav.classList.toggle("scrolled", scrollY > 8);
addEventListener("scroll", onScroll, { passive: true }); onScroll();
document.querySelectorAll(".copy").forEach((b) => b.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(document.getElementById(b.dataset.copy).textContent);
b.textContent = "Copied"; setTimeout(() => (b.textContent = "Copy"), 1400);
} catch {}
}));
// Windows and Linux get their own app; arm64 is guessed from the user
// agent and corrected by the browser when it can tell.
const ua = navigator.userAgent;
const os = /Windows/.test(ua) ? "windows" : /Linux|X11|CrOS/.test(ua) && !/Android/.test(ua) ? "linux" : "mac";
if (os !== "mac") {
document.documentElement.dataset.os = os;
const name = os === "windows" ? "Windows" : "Linux";
const arm = /aarch64|arm64|armv8/i.test(ua);
const dl = (a64) => "/download/" + os + (a64 ? "-arm64" : "-amd64");
document.querySelectorAll("a.dl").forEach((a) => (a.href = dl(arm)));
document.querySelectorAll(".dl-label").forEach((e) => (e.textContent = "Download for " + name));
document.querySelectorAll(".os-note").forEach((e) => (e.innerHTML = os === "windows" ? "Windows 10+" : "Needs WebKitGTK 4.1"));
document.querySelectorAll('.meta a[href="#get"]').forEach((a) => (a.textContent = "macOS & other builds"));
try { navigator.userAgentData?.getHighEntropyValues(["architecture"]).then((v) => {
if (v.architecture === "arm") document.querySelectorAll("a.dl").forEach((a) => (a.href = dl(true)));
}).catch(() => {}); } catch {}
}
// Intel Macs get the Intel build; browsers that can tell say so, the rest
// get Apple Silicon with the Intel link beside it.
const setArch = (arch) => {
if (arch !== "x86" || os !== "mac") return;
document.querySelectorAll("a.dl").forEach((a) => (a.href = "/download/mac-intel"));
document.querySelectorAll("a.alt").forEach((a) => { a.href = "/download/mac-arm64"; a.textContent = "Apple Silicon"; });
};
try { navigator.userAgentData?.getHighEntropyValues(["architecture"]).then((v) => setArch(v.architecture)).catch(() => {}); } catch {}
fetch("/api/latest").then((r) => (r.ok ? r.json() : null)).then((rel) => {
if (!rel) return;
document.querySelectorAll(".ver").forEach((e) => (e.textContent = "Version " + rel.version));
}).catch(() => {});
})();
</script>
<script>
// The routing demo: requests travel from the agent through magpie to the
// account the chosen strategy picks; one that can't answer sends the request
// back to magpie, which tries the next.
(() => {
const rt = document.getElementById("rt");
if (!rt) return;
const $ = (s) => rt.querySelector(s);
const stage = $(".rt-stage"), svg = $(".rt-wires"), src = $(".rt-src"), hub = $(".rt-hub"), list = $(".rt-accts");
const cap = $(".rt-cap"), desc = $(".rt-mode"), chip = hub.querySelector("i");
const stat = [...rt.querySelectorAll(".rt-stats b")], statName = [...rt.querySelectorAll(".rt-stats span")].map((e) => e.lastChild);
const who = src.querySelector("small");
const tabs = [...rt.querySelectorAll(".rt-tabs button")];
const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
const NS = "http://www.w3.org/2000/svg";
// Two Claude subscriptions and three other vendors' accounts, each with
// its vendor's icon; the captions name them by plan.
const V = {
claude: ["me@work.dev", "Claude Max", "claude-color", "Claude Max", ""],
claude2: ["me@home.dev", "Claude Pro", "claude-color", "Claude Pro", ""],
gpt: ["me@home.dev", "ChatGPT Pro", "openai", "ChatGPT Pro", "bird"],
or: ["sk-or-…4f2a", "OpenRouter", "openrouter", "The OpenRouter key", "bird"],
gh: ["octo-dev", "Copilot Business", "githubcopilot", "Copilot", "bird"],
};
const A = (v, used, inc, x = {}) => { const [who, plan, icon, name, cls] = V[v]; return { who, plan, icon, name, cls, used, inc, ...x }; };
// A group's models, each with the rule that sends a turn to it; the first
// takes what no rule matches.
const G = (who, plan, icon, cls, rule, short) => ({ who, plan, icon, cls, rule, short, used: 0, inc: 0, key: 1 });
const MODES = {
intent: {
name: "By intent", n: 0, stats: ["turns", "asked the classifier", "tool rounds kept on their model"],
desc: "By intent: as a turn begins, a small fast model (the group's classifier) is asked once which of your rules' intents the message is. The turn goes to that rule's model; its tool rounds stay there, without asking again.",
accts: () => [
G("claude-opus-5-5", "Claude Max", "claude-color", "", "first · whatever no rule takes"),
G("deepseek-v4-flash", "DeepSeek", "deepseek-color", "", "intent · a quick question", "quick question"),
G("glm-5.3", "Zhipu GLM", "zhipu-color", "", "intent · writing or fixing tests", "tests"),
G("kimi-k3", "Kimi", "kimi", "bird", "intent · reviewing a diff", "review"),
],
turns: [
["Add a --dry-run flag to the deploy command", 0, 2],
["What does ^\\d{3}-\\d{4}$ match?", 1, 0],
["TestRouting fails after my change — fix it", 2, 2],
["Review my staged changes before I commit", 3, 1],
["Why does useEffect run twice in dev?", 1, 0],
["Write table-driven tests for parseTokens", 2, 1],
["Refactor the retry loop into its own function", 0, 1],
],
},
smart: {
name: "Smart", n: 30,
desc: "Smart, the default: the account that resets soonest goes first, so no allowance is left to go to waste; any that can't answer are passed over. (Here each request stands for an hour.)",
first: (a) => `${a.name} resets in ${fmt(left(a))} — soonest, so it goes first: what it has left is used, not lost at the reset.`,
accts: () => [
A("claude", 72, 1.5, { r: 9 }),
A("claude2", 38, 3, { r: 48, fail: { at: 3, code: 429, rest: 7000 } }),
A("gpt", 30, 3, { r: 96 }),
A("or", 0, 0, { key: 1, fail: { at: 0, code: 402 } }),
A("gh", 24, 2, { r: 288, fail: { at: 4, code: 503, rest: 5000 } }),
],
},
order: {
name: "In order", n: 18,
desc: "In order: the first answers everything until it can't; then the next takes over.",
first: (a) => `${a.name} answers everything, for as long as it can.`,
accts: () => [A("claude", 76, 3), A("claude2", 44, 3), A("gpt", 30, 3), A("or", 0, 0, { key: 1 }), A("gh", 24, 2)],
},
turn: {
name: "In turn", n: 16,
desc: "In turn: each request goes to the next account, so the load is spread evenly.",
first: () => "Each request goes to the next account in turn.",
accts: () => [A("claude", 30, 2), A("claude2", 40, 2), A("gpt", 52, 2), A("or", 0, 0, { key: 1 }), A("gh", 18, 2)],
},
least: {
name: "Least used", n: 18,
desc: "Least used first: each request goes to the account with the most allowance left; a key by the tokens magpie sent it lately.",
first: () => "Each request goes to whichever has the most left — watch the bars even out.",
accts: () => [A("claude", 64, 6), A("claude2", 30, 6), A("gpt", 22, 6), A("or", 40, 6, { key: 1, bar: 1 }), A("gh", 46, 6)],
},
};
let mode, M, accts = [], t = 0, gen = 0, next = 0, sent = 0, done = 0, rerouted = 0, errs = 0, ptr = 0;
let hours = 0; // the demo's clock in smart mode: an hour a request
const left = (a) => a.r - hours, fmt = (h) => h < 48 ? `${h} h` : `${Math.round(h / 24)} d`;
let began = 0, last = null, trips = [], capQ = [], capAt = -1e9, capLo = false, endAt = 0, wSrc, vis = true;
const resting = (a) => a.until > t;
const path = () => { const p = document.createElementNS(NS, "path"); svg.appendChild(p); return p; };
const tick = (el) => { el.classList.remove("tick"); void el.offsetWidth; el.classList.add("tick"); };
const travel = (dot, p, rev, dur) => new Promise((res) => trips.push({ dot, p, rev, t0: t, dur: still ? 0 : dur, res, g: gen }));
const wait = (ms) => travel(null, null, false, ms);
// A caption about a failure goes ahead of the small ones ("takes over", "is back").
const say = (s, lo) => {
if (lo && capQ.length) return;
if (!lo) capQ = capQ.filter((c) => !c.lo);
capQ.push({ s, lo });
if (capQ.length > 2) capQ.shift();
};
function layout() {
const r = stage.getBoundingClientRect();
const box = (el) => { const b = el.getBoundingClientRect(); return { l: b.left - r.left, r: b.right - r.left, t: b.top - r.top, b: b.bottom - r.top, cx: (b.left + b.right) / 2 - r.left, cy: (b.top + b.bottom) / 2 - r.top }; };
svg.setAttribute("viewBox", `0 0 ${r.width} ${r.height}`);
const s = box(src), h = box(hub);
wSrc.setAttribute("d", h.l > s.r ? `M${s.r} ${s.cy} L${h.l} ${h.cy}` : `M${s.cx} ${s.b} L${h.cx} ${h.t}`);
for (const a of accts) {
const b = box(a.li);
if (b.l > h.r) {
const mx = (h.r + b.l) / 2;
a.wire.setAttribute("d", `M${h.r} ${h.cy} C${mx} ${h.cy} ${mx} ${b.cy} ${b.l} ${b.cy}`);
} else { // the accounts sit under magpie: a lane down their left
const x = b.l - 14;
a.wire.setAttribute("d", `M${h.cx} ${h.b} C${h.cx} ${h.b + 26} ${x} ${h.b + 2} ${x} ${h.b + 28} L${x} ${b.cy - 10} Q${x} ${b.cy} ${b.l} ${b.cy}`);
}
}
}
function reset(m) {
gen++; mode = m; M = MODES[m]; accts = M.accts();
sent = done = rerouted = errs = ptr = began = hours = 0; last = null; endAt = 0; trips = []; capQ = [];
next = t + 600;
list.innerHTML = accts.map((a) => `<li><i class="dot"></i><b><img${a.cls ? ` class="${a.cls}"` : ""} src="/icons/${a.icon}.svg" alt="">${a.who} <span>${a.plan}</span></b><em></em><div class="bar"><i></i></div><span class="tag"></span></li>`).join("");
svg.innerHTML = "";
wSrc = path();
accts.forEach((a, i) => {
a.li = list.children[i]; a.st = a.li.querySelector("em"); a.bi = a.li.querySelector(".bar i"); a.tg = a.li.querySelector(".tag");
a.li.classList.toggle("nobar", !!a.key && !a.bar);
a.wire = path(); a.served = 0; a.until = 0;
});
layout();
list.classList.toggle("models", !!M.turns);
desc.textContent = M.desc; chip.textContent = M.name; who.textContent = "your agent";
(M.stats || ["requests", "rerouted", "errors your agent saw"]).forEach((n, i) => (statName[i].textContent = n));
tabs.forEach((b) => b.setAttribute("aria-selected", String(b.dataset.mode === m)));
show(M.turns ? "Claude Code sends every turn to one model name — a magpie group. Watch where each message goes." : "Claude Code sends every request to magpie; magpie decides which account answers it.");
stats(); render();
}
function show(c) { capLo = !!c.lo; const s = c.s || c; cap.textContent = s; cap.classList.remove("in"); void cap.offsetWidth; cap.classList.add("in"); capAt = t; }
function stats() { stat[0].textContent = done; stat[1].textContent = rerouted; stat[2].textContent = errs; }
function pick() {
const ok = accts.filter((a) => !resting(a));
if (!ok.length) return null;
switch (mode) {
case "smart": return ok.filter((a) => a.used < 90).sort((x, y) => (x.r ?? 1e9) - (y.r ?? 1e9))[0] || ok.slice().sort((x, y) => x.used - y.used)[0];
case "order": return ok[0];
case "least": return ok.slice().sort((x, y) => x.used - y.used)[0];
default:
for (let i = 0; i < accts.length; i++) {
const a = accts[(ptr + i) % accts.length];
if (!resting(a)) { ptr = (accts.indexOf(a) + 1) % accts.length; return a; }
}
}
}
function respond(a) {
if (resting(a)) return a.code;
if (a.fail && !a.failed && a.served >= a.fail.at) { a.failed = 1; return a.fail.code; }
if (!a.key && a.used >= 100) return "quota";
return 200;
}
function serve(a) {
const was = a.used;
a.served++; a.used = Math.min(100, a.used + a.inc);
if (last !== a) {
if (!began) began = 1, say(M.first(a));
else if (mode === "smart" && a.used >= 90) say(`The others are resting, so ${a.name.replace(/^The /, "the ")} answers — running low beats no answer.`);
else if (mode === "smart" && a.r) say(`${a.name} takes over — it resets next, in ${fmt(left(a))}.`, true);
else if (mode === "smart" || mode === "order") say(`${a.name} takes over.`, true);
last = a;
}
if (mode === "smart" && was < 90 && a.used >= 90) say(`${a.name} is at ${Math.round(a.used)}% — kept for last. The next with quota to spare takes over.`);
if (mode !== "smart") return;
hours++;
for (const b of accts) if (b.r && left(b) <= 0) { // its week renews: now the last to reset
const lost = Math.round(100 - b.used);
b.used = 0; b.r += 168;
say(`${b.name}'s week renewed with only ${lost}% unused — it went first, so little was lost. It resets last now; the next to reset takes over.`);
if (last === b) last = null;
}
}
const TAGS = { 429: "429 · rate limited", 402: "402 · no credit", 503: "503 · failed", quota: "429 · quota used up" };
function fail(a, code) {
a.li.classList.remove("hit"); void a.li.offsetWidth; a.li.classList.add("hit", "tagged");
a.tg.textContent = TAGS[code];
setTimeout(() => a.li.classList.remove("tagged"), 1500);
if (resting(a)) return; // it was already out when the request got there
a.code = code;
next = Math.max(next, t + 1500); // a moment to read what happened
if (code === 429) { a.why = "rate"; a.until = t + a.fail.rest; say(`${a.name} answered 429 — it rests for as long as the vendor asks, and the request is retried on the next at once.`); }
if (code === 402) { a.why = "credit"; a.until = Infinity; say(`${a.name} is out of credit — it sits out half an hour, and the request moves on. Claude Code never sees the error.`); }
if (code === 503) { a.why = "fail"; a.until = t + a.fail.rest; say(`${a.name} failed — it steps back a while, longer each time it fails again. Another account answers instead.`); }
if (code === "quota") { a.why = "quota"; a.until = Infinity; say(`${a.name} is out of quota — passed over until it resets. The request goes to the next.`); }
if (last === a) last = null;
}
async function request(g) {
const dot = document.createElementNS(NS, "circle");
dot.setAttribute("r", 4.5); dot.setAttribute("class", "pkt"); dot.setAttribute("cx", -20);
svg.appendChild(dot); tick(src);
await travel(dot, wSrc, false, 380);
for (let tries = 0; g === gen && tries < 8; tries++) {
const a = pick();
if (!a) { await wait(400); continue; }
tick(hub);
await travel(dot, a.wire, false, 420);
if (g !== gen) return;
const code = respond(a);
if (code === 200) {
serve(a);
dot.classList.add("back"); dot.setAttribute("r", 4);
await travel(dot, a.wire, true, 360);
await travel(dot, wSrc, true, 320);
if (g !== gen) return;
dot.remove(); done++; stats(); return;
}
fail(a, code); rerouted++; stats();
await travel(dot, a.wire, true, 300);
}
if (g !== gen) return;
dot.remove(); done++; errs++; stats();
}
// By intent: a turn's first request is classified, once; its tool rounds
// follow it to the same model.
async function turns(g) {
const trip = async (a) => {
const dot = document.createElementNS(NS, "circle");
dot.setAttribute("r", 4.5); dot.setAttribute("class", "pkt"); dot.setAttribute("cx", -20);
svg.appendChild(dot); tick(src);
await travel(dot, wSrc, false, 380);
if (g !== gen) return dot.remove();
tick(hub);
if (!a) return dot;
await travel(dot, a.wire, false, 420);
if (g !== gen) return dot.remove();
last = a; a.served++;
dot.classList.add("back"); dot.setAttribute("r", 4);
await travel(dot, a.wire, true, 360);
await travel(dot, wSrc, true, 320);
dot.remove();
};
for (const [msg, n, rounds] of M.turns) {
if (g !== gen) return;
const a = accts[n];
who.textContent = "new turn"; chip.textContent = "new turn";
show(`A new turn: “${msg}”`);
const dot = await trip(null);
if (g !== gen) return;
chip.textContent = "classifying…";
await wait(900);
if (g !== gen) return dot?.remove();
rerouted++; stats();
chip.textContent = n ? a.short : "no intent";
a.li.classList.remove("hit"); void a.li.offsetWidth; a.li.classList.add("tagged");
a.tg.textContent = n ? "✓ " + a.short : "✓ the group's first";
setTimeout(() => a.li.classList.remove("tagged"), 1600);
show(n ? `The classifier says: ${a.rule.replace("intent · ", "")} — so ${a.who} answers it.` : `None of the intents — it goes to the group's first model, ${a.who}.`);
await travel(dot, a.wire, false, 420);
if (g !== gen) return dot.remove();
last = a; a.served++;
dot.classList.add("back"); dot.setAttribute("r", 4);
await travel(dot, a.wire, true, 360);
await travel(dot, wSrc, true, 320);
dot.remove(); done++; stats();
for (let i = 0; i < rounds; i++) {
if (g !== gen) return;
who.textContent = "tool result";
chip.textContent = "same turn";
if (!i) show(`Its tool results come back as more requests — no new question: they stay on ${a.who}.`);
await trip(a);
errs++; stats();
}
await wait(rounds ? 700 : 1300);
}
if (g !== gen) return;
who.textContent = "your agent"; chip.textContent = M.name;
show(`${done} turns, each on the model its rule names — one question to the classifier per turn, ${errs} tool rounds kept on their model.`);
await wait(4600);
if (g === gen) reset(mode);
}
function render() {
if (M.turns) {
for (const a of accts) {
if (a.st.textContent !== a.rule) a.st.textContent = a.rule;
a.li.classList.toggle("on", last === a);
a.wire.classList.toggle("live", last === a);
}
wSrc.classList.toggle("live", !!last);
return;
}
for (const a of accts) {
const rest = resting(a), n = Math.round(a.used), s = Math.ceil((a.until - t) / 1000);
const label = rest
? { rate: `rate limited · back in ${s} s`, fail: `failing · back in ${s} s`, credit: "out of credit · back in 30 m", quota: "out of quota · resets 3 PM" }[a.why]
: last === a ? (mode === "smart" && a.used >= 90 ? "serving · running low" : "serving")
: a.key ? (a.bar ? `${n}% of tokens lately` : "pay as you go")
: mode === "smart" && a.used >= 90 ? `${n}% used · kept for last`
: mode === "smart" && a.r ? `${n}% used · resets in ${fmt(left(a))}` : `${n}% used`;
if (a.st.textContent !== label) a.st.textContent = label;
a.bi.style.width = a.used + "%";
a.li.classList.toggle("on", last === a && !rest);
a.li.classList.toggle("low", mode === "smart" && a.used >= 90);
a.li.classList.toggle("rest", rest);
a.wire.classList.toggle("live", last === a && !rest);
a.wire.classList.toggle("rest", rest);
}
wSrc.classList.toggle("live", !!last);
}
function step() {
if (M.turns && !sent && t >= next) { sent = 1; turns(gen); }
if (sent < M.n && t >= next) { sent++; request(gen); next = t + (still ? 1400 : 900); }
const now = trips; trips = [];
for (const tr of now) {
if (tr.g !== gen) continue;
const k = tr.dur ? Math.min(1, (t - tr.t0) / tr.dur) : 1;
if (tr.dot) {
const e = k < .5 ? 2 * k * k : 1 - (-2 * k + 2) ** 2 / 2;
const len = tr.p.getTotalLength(), pt = tr.p.getPointAtLength((tr.rev ? 1 - e : e) * len);
tr.dot.setAttribute("cx", pt.x); tr.dot.setAttribute("cy", pt.y);
}
if (k >= 1) tr.res(); else trips.push(tr);
}
for (const a of accts) if (a.until && a.until !== Infinity && t >= a.until) {
a.until = 0;
say(`${a.name} is back — ${a.why === "rate" ? "its rate limit is over" : "it gets another chance"}.`, true);
}
if (capQ.length && t - capAt > (capLo && !capQ[0].lo ? 700 : 2200)) show(capQ.shift());
if (!M.turns && done >= M.n && !endAt) {
endAt = t + 4200;
capQ = []; show(`${done} requests answered, ${rerouted} rerouted — and not one error reached Claude Code.`);
}
if (endAt && t >= endAt) reset(mode);
render();
}
let prev = 0;
function frame(now) {
const dt = Math.min(now - (prev || now), 50); prev = now;
if (vis && !document.hidden) { t += dt; step(); }
requestAnimationFrame(frame);
}
tabs.forEach((b) => b.addEventListener("click", () => reset(b.dataset.mode)));
new IntersectionObserver((es) => { vis = es[0].isIntersecting; }).observe(rt);
new ResizeObserver(() => layout()).observe(stage);
reset("intent");
requestAnimationFrame(frame);
})();
</script>
</body>
</html>