@bitkyc08/opencodex 2.6.7 → 2.6.8
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/gui/dist/assets/index-CgCKvnPc.js +9 -0
- package/gui/dist/assets/{index-CSI5RHdZ.css → index-DIBiVVC0.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/azure.ts +2 -2
- package/src/adapters/base.ts +6 -1
- package/src/adapters/google-antigravity-replay.ts +136 -0
- package/src/adapters/google-antigravity-wire.ts +94 -0
- package/src/adapters/google-errors.ts +95 -0
- package/src/adapters/google-http.ts +109 -0
- package/src/adapters/google-tool-schema.ts +89 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google.ts +163 -20
- package/src/adapters/kiro-tools.ts +55 -2
- package/src/cli-status.ts +4 -0
- package/src/cli.ts +7 -0
- package/src/codex-history-provider.ts +195 -22
- package/src/codex-plugins-doctor.ts +242 -0
- package/src/config.ts +1 -0
- package/src/lib/gcp-adc.ts +292 -0
- package/src/oauth/google-antigravity.ts +225 -0
- package/src/oauth/index.ts +19 -1
- package/src/oauth/store.ts +1 -0
- package/src/oauth/types.ts +2 -0
- package/src/provider-context-cap.ts +30 -1
- package/src/providers/antigravity-models.ts +30 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/registry.ts +7 -0
- package/src/redact.ts +26 -0
- package/src/router.ts +6 -0
- package/src/server.ts +45 -9
- package/src/types.ts +12 -0
- package/src/usage-summary.ts +30 -3
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Ct8koBOe.js +0 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#f6f7f9)var(--lightningcss-dark,#0c0d11);--rail:var(--lightningcss-light,#fff)var(--lightningcss-dark,#101218);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#15171d);--raised:var(--lightningcss-light,#f1f2f5)var(--lightningcss-dark,#1c1f27);--raised-hover:var(--lightningcss-light,#e8eaee)var(--lightningcss-dark,#242833);--border:var(--lightningcss-light,#e2e4e9)var(--lightningcss-dark,#2a2e39);--border-soft:var(--lightningcss-light,#ededf1)var(--lightningcss-dark,#20242d);--hover:var(--lightningcss-light,#11131c09)var(--lightningcss-dark,#ffffff06);--text:var(--lightningcss-light,#16181d)var(--lightningcss-dark,#edeef2);--muted:var(--lightningcss-light,#5b6270)var(--lightningcss-dark,#a3a9b5);--faint:var(--lightningcss-light,#868d9b)var(--lightningcss-dark,#6b7280);--accent:var(--lightningcss-light,#4f46e5)var(--lightningcss-dark,#6366f1);--accent-hover:var(--lightningcss-light,#4338ca)var(--lightningcss-dark,#818cf8);--accent-ink:#fff;--accent-soft:var(--lightningcss-light,#4f46e51a)var(--lightningcss-dark,#6366f129);--accent-ring:var(--lightningcss-light,#4f46e566)var(--lightningcss-dark,#6366f180);--green:var(--lightningcss-light,#047857)var(--lightningcss-dark,#34d399);--green-soft:var(--lightningcss-light,#0596691a)var(--lightningcss-dark,#34d39921);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#b45309)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--radius:8px;--radius-sm:5px;--radius-xs:4px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, "Helvetica Neue", sans-serif;--mono:ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}a{color:var(--accent-hover);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:-.01em;margin:0;font-weight:650}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border:2px solid var(--bg);border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;align-self:start;gap:4px;height:100dvh;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{letter-spacing:-.02em;font-size:15px;font-weight:700;line-height:26px}.brand .ver{font-family:var(--mono);color:var(--muted);background:var(--raised);border:1px solid var(--border);border-radius:99px;align-self:center;padding:2px 6px;font-size:10px;line-height:1}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;font-weight:500;transition:background .12s,color .12s;display:flex}.nav-item:hover{background:var(--raised);color:var(--text)}.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--accent)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;display:flex}.sidebar-link:hover{background:var(--raised);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;border-radius:var(--radius-sm);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;display:flex}.theme-toggle:hover{background:var(--raised);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:19px}.page-sub{color:var(--muted);max-width:70ch;margin:4px 0 22px;font-size:13.5px}.page-sub b{color:var(--text);font-weight:600}.btn{border-radius:var(--radius-sm);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:550;transition:background .12s,border-color .12s,opacity .12s;display:inline-flex}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--raised);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised-hover)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{border-radius:var(--radius-xs);padding:4px 9px;font-size:12px}.btn-icon{color:var(--muted);border-radius:var(--radius-xs);padding:5px;transition:background .12s,color .12s}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{background:linear-gradient(180deg, var(--accent-soft), transparent 120%), var(--surface);border-color:#7c5cff47}.stat-row{grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px;display:grid}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--accent-ring)}.stat .label{color:var(--muted);text-transform:uppercase;letter-spacing:.05em;align-items:center;gap:6px;margin-bottom:9px;font-size:11px;font-weight:600;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:-.02em;font-size:24px;font-weight:700;line-height:1.1}.stat .value.mono{font-family:var(--mono);font-size:19px}.model-group-head{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--faint);font-weight:500}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--accent-ring);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:-.01em;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:.01em;border:1px solid #0000;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent-hover)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.badge-clickable{cursor:pointer;appearance:none;transition:filter .12s}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{background:var(--amber-soft);width:44px;height:44px;color:var(--amber);border-radius:50%;justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);padding:8px 10px;transition:border-color .12s}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);align-items:center;gap:6px;margin-bottom:3px;font-size:12px;font-weight:600;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:11px;font-family:var(--mono);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:600}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);padding:9px 12px;font-size:11.5px;font-weight:600}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.tbl .num{text-align:right;font-family:var(--mono)}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;padding:8px 11px;font-size:13px;transition:border-color .12s}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--accent);outline:none}textarea.input{resize:vertical;font-family:var(--mono);line-height:1.55}.field-label{color:var(--muted);margin-bottom:5px;font-size:12px;font-weight:500;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);color:var(--text);font:inherit;cursor:pointer;padding:5px 8px;font-size:13px;transition:border-color .12s}.select-sm:focus{border-color:var(--accent);outline:none}.select-sm:disabled{opacity:.5;cursor:default}.switch{cursor:pointer;background:var(--lightningcss-light,#c5c9d2)var(--lightningcss-dark,#3a3f4b);border:none;border-radius:99px;flex-shrink:0;width:34px;height:19px;padding:0;transition:background .15s;position:relative}.switch.on{background:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:#fff;border-radius:50%;width:15px;height:15px;transition:left .15s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #1018284d}.switch.on .knob{left:17px}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.stack{flex-direction:column;display:flex}.chip{font-family:var(--mono);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px;font-size:12px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);margin-bottom:6px;font-weight:600}.notice{border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;font-size:13px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.notice-ok{background:var(--green-soft);color:var(--green)}.notice-err{background:var(--red-soft);color:var(--red)}.h-section{color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;font-size:13px;font-weight:600;display:flex}.h-section .count{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:12px}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}.modal-overlay{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:var(--lightningcss-light,#11131c73)var(--lightningcss-dark,#0009);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:100%;max-width:520px;box-shadow:var(--shadow);max-height:84vh;padding:20px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:16px}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:14px 16px 6px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px;font-size:12px}.card-active{border-color:var(--accent-ring)}.card-right{color:var(--faint);align-items:center;gap:4px;margin-left:auto;font-size:11px;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;font-size:12px;font-weight:600}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:2px 16px 12px;display:grid}.quota-row{grid-template-columns:minmax(34px,max-content) 34px minmax(34px,44px) minmax(38px,42px) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{color:var(--muted);font-size:11px;font-weight:600}.quota-val{font-size:11px;font-family:var(--mono);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;overflow:hidden}.quota-reset-day,.quota-reset-time{color:var(--muted);white-space:nowrap;font-size:11px}.quota-reset-time{font-family:var(--mono)}.bar{background:var(--raised);border-radius:3px;min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:3px;height:100%;transition:width .3s}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{background:var(--raised);border:1px solid var(--border);cursor:pointer;border-radius:10px;flex-shrink:0;width:36px;height:20px;transition:background .15s;position:relative}.toggle.on{background:var(--accent);border-color:var(--accent)}.toggle-knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:left .15s;position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{left:17px}.notice-warn{border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;font-size:12px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{color:var(--muted);margin-bottom:14px;font-size:13px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.setup-guide{border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px;font-size:13px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:500}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;transition:background .12s,border-color .12s;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-size:14px;font-weight:600}.list-row .sub{color:var(--muted);margin-top:2px;font-size:12px}.prov-card{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.link-btn{color:var(--accent-hover);font:inherit;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}@media (width<=760px){.app{grid-template-columns:1fr}.sidebar{z-index:20;border-right:none;border-bottom:1px solid var(--border);background:var(--rail);flex-flow:wrap;align-items:center;gap:0;min-width:0;height:auto;padding:0 10px;position:sticky;top:0}.brand{flex:auto;order:1;width:auto;padding:10px 4px}.sidebar-foot{flex-direction:row;flex:none;order:2;gap:4px;margin:0;padding:0}.sidebar-foot .sidebar-link{display:none}.theme-toggle{justify-content:center;min-width:44px;min-height:44px;padding:8px}.theme-toggle .mode{display:none}.sidebar nav{overscroll-behavior-x:contain;border-top:1px solid var(--border-soft);scrollbar-width:none;flex-direction:row;flex:100%;order:3;gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:flex;overflow-x:auto}.sidebar nav::-webkit-scrollbar{display:none}.nav-item{white-space:nowrap;width:auto;min-height:44px;padding:9px 14px;font-size:14px}.main-inner{padding:22px 18px 48px}.stat-row{grid-template-columns:repeat(2,minmax(0,1fr))}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}.usage-cards{grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{margin-top:4px;font-size:22px;font-weight:600}.usage-range{border:1px solid var(--border);background:var(--surface);border-radius:999px;gap:2px;padding:2px;display:inline-flex}.usage-range-btn{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:none;border-radius:999px;padding:4px 12px}.usage-range-btn.active{background:var(--raised);color:var(--text);font-weight:600}.panel-title{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{color:var(--muted);margin-bottom:-2px;font-size:11px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:3px;display:flex}.heatmap-days{color:var(--muted);flex-shrink:0;grid-template-rows:repeat(7,1fr);align-items:center;width:25px;font-size:10px;display:grid}.heatmap-grid{flex:1;gap:3px;min-width:0;display:grid}.heatmap-week{grid-template-rows:repeat(7,1fr);gap:3px;display:grid}.heatmap-cell{aspect-ratio:1;background:var(--border);border-radius:2px;min-width:0}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{align-self:flex-end;align-items:center;gap:4px;font-size:12px;display:inline-flex}.heatmap-legend .heatmap-cell{aspect-ratio:auto;width:10px;height:10px}.usage-bar{background:var(--border);border-radius:999px;width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:999px;height:100%}
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#f6f7f9)var(--lightningcss-dark,#0c0d11);--rail:var(--lightningcss-light,#fff)var(--lightningcss-dark,#101218);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#15171d);--raised:var(--lightningcss-light,#f1f2f5)var(--lightningcss-dark,#1c1f27);--raised-hover:var(--lightningcss-light,#e8eaee)var(--lightningcss-dark,#242833);--border:var(--lightningcss-light,#e2e4e9)var(--lightningcss-dark,#2a2e39);--border-soft:var(--lightningcss-light,#ededf1)var(--lightningcss-dark,#20242d);--hover:var(--lightningcss-light,#11131c09)var(--lightningcss-dark,#ffffff06);--text:var(--lightningcss-light,#16181d)var(--lightningcss-dark,#edeef2);--muted:var(--lightningcss-light,#5b6270)var(--lightningcss-dark,#a3a9b5);--faint:var(--lightningcss-light,#868d9b)var(--lightningcss-dark,#6b7280);--accent:var(--lightningcss-light,#4f46e5)var(--lightningcss-dark,#6366f1);--accent-hover:var(--lightningcss-light,#4338ca)var(--lightningcss-dark,#818cf8);--accent-ink:#fff;--accent-soft:var(--lightningcss-light,#4f46e51a)var(--lightningcss-dark,#6366f129);--accent-ring:var(--lightningcss-light,#4f46e566)var(--lightningcss-dark,#6366f180);--green:var(--lightningcss-light,#047857)var(--lightningcss-dark,#34d399);--green-soft:var(--lightningcss-light,#0596691a)var(--lightningcss-dark,#34d39921);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#b45309)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--radius:8px;--radius-sm:5px;--radius-xs:4px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, "Helvetica Neue", sans-serif;--mono:ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}a{color:var(--accent-hover);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:-.01em;margin:0;font-weight:650}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border:2px solid var(--bg);border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;align-self:start;gap:4px;height:100dvh;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{letter-spacing:-.02em;font-size:15px;font-weight:700;line-height:26px}.brand .ver{font-family:var(--mono);color:var(--muted);background:var(--raised);border:1px solid var(--border);border-radius:99px;align-self:center;padding:2px 6px;font-size:10px;line-height:1}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;font-weight:500;transition:background .12s,color .12s;display:flex}.nav-item:hover{background:var(--raised);color:var(--text)}.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--accent)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;display:flex}.sidebar-link:hover{background:var(--raised);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;border-radius:var(--radius-sm);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;display:flex}.theme-toggle:hover{background:var(--raised);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:19px}.page-sub{color:var(--muted);max-width:70ch;margin:4px 0 22px;font-size:13.5px}.page-sub b{color:var(--text);font-weight:600}.btn{border-radius:var(--radius-sm);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:550;transition:background .12s,border-color .12s,opacity .12s;display:inline-flex}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--raised);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised-hover)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{border-radius:var(--radius-xs);padding:4px 9px;font-size:12px}.btn-icon{color:var(--muted);border-radius:var(--radius-xs);padding:5px;transition:background .12s,color .12s}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{background:linear-gradient(180deg, var(--accent-soft), transparent 120%), var(--surface);border-color:#7c5cff47}.stat-row{grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px;display:grid}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--accent-ring)}.stat .label{color:var(--muted);text-transform:uppercase;letter-spacing:.05em;align-items:center;gap:6px;margin-bottom:9px;font-size:11px;font-weight:600;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:-.02em;font-size:24px;font-weight:700;line-height:1.1}.stat .value.mono{font-family:var(--mono);font-size:19px}.model-group-head{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--faint);font-weight:500}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--accent-ring);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:-.01em;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:.01em;border:1px solid #0000;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent-hover)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.badge-clickable{cursor:pointer;appearance:none;transition:filter .12s}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{background:var(--amber-soft);width:44px;height:44px;color:var(--amber);border-radius:50%;justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);padding:8px 10px;transition:border-color .12s}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);align-items:center;gap:6px;margin-bottom:3px;font-size:12px;font-weight:600;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:11px;font-family:var(--mono);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:600}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);padding:9px 12px;font-size:11.5px;font-weight:600}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.tbl .num{text-align:right;font-family:var(--mono)}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;padding:8px 11px;font-size:13px;transition:border-color .12s}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--accent);outline:none}textarea.input{resize:vertical;font-family:var(--mono);line-height:1.55}.field-label{color:var(--muted);margin-bottom:5px;font-size:12px;font-weight:500;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);color:var(--text);font:inherit;cursor:pointer;padding:5px 8px;font-size:13px;transition:border-color .12s}.select-sm:focus{border-color:var(--accent);outline:none}.select-sm:disabled{opacity:.5;cursor:default}.switch{cursor:pointer;background:var(--lightningcss-light,#c5c9d2)var(--lightningcss-dark,#3a3f4b);border:none;border-radius:99px;flex-shrink:0;width:34px;height:19px;padding:0;transition:background .15s;position:relative}.switch.on{background:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:#fff;border-radius:50%;width:15px;height:15px;transition:left .15s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #1018284d}.switch.on .knob{left:17px}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.stack{flex-direction:column;display:flex}.chip{font-family:var(--mono);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px;font-size:12px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);margin-bottom:6px;font-weight:600}.notice{border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;font-size:13px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.notice-ok{background:var(--green-soft);color:var(--green)}.notice-err{background:var(--red-soft);color:var(--red)}.h-section{color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;font-size:13px;font-weight:600;display:flex}.h-section .count{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:12px}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}.modal-overlay{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:var(--lightningcss-light,#11131c73)var(--lightningcss-dark,#0009);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:100%;max-width:520px;box-shadow:var(--shadow);max-height:84vh;padding:20px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:16px}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:14px 16px 6px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px;font-size:12px}.card-active{border-color:var(--accent-ring)}.card-right{color:var(--faint);align-items:center;gap:4px;margin-left:auto;font-size:11px;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;font-size:12px;font-weight:600}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:2px 16px 12px;display:grid}.quota-row{grid-template-columns:minmax(34px,max-content) 34px minmax(34px,44px) minmax(38px,42px) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{color:var(--muted);font-size:11px;font-weight:600}.quota-val{font-size:11px;font-family:var(--mono);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;overflow:hidden}.quota-reset-day,.quota-reset-time{color:var(--muted);white-space:nowrap;font-size:11px}.quota-reset-time{font-family:var(--mono)}.bar{background:var(--raised);border-radius:3px;min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:3px;height:100%;transition:width .3s}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{background:var(--raised);border:1px solid var(--border);cursor:pointer;border-radius:10px;flex-shrink:0;width:36px;height:20px;transition:background .15s;position:relative}.toggle.on{background:var(--accent);border-color:var(--accent)}.toggle-knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:left .15s;position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{left:17px}.notice-warn{border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;font-size:12px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{color:var(--muted);margin-bottom:14px;font-size:13px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.setup-guide{border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px;font-size:13px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:500}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;transition:background .12s,border-color .12s;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-size:14px;font-weight:600}.list-row .sub{color:var(--muted);margin-top:2px;font-size:12px}.prov-card{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.link-btn{color:var(--accent-hover);font:inherit;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}@media (width<=760px){.app{grid-template-columns:1fr}.sidebar{z-index:20;border-right:none;border-bottom:1px solid var(--border);background:var(--rail);flex-flow:wrap;align-items:center;gap:0;min-width:0;height:auto;padding:0 10px;position:sticky;top:0}.brand{flex:auto;order:1;width:auto;padding:10px 4px}.sidebar-foot{flex-direction:row;flex:none;order:2;gap:4px;margin:0;padding:0}.sidebar-foot .sidebar-link{display:none}.theme-toggle{justify-content:center;min-width:44px;min-height:44px;padding:8px}.theme-toggle .mode{display:none}.sidebar nav{overscroll-behavior-x:contain;border-top:1px solid var(--border-soft);scrollbar-width:none;flex-direction:row;flex:100%;order:3;gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:flex;overflow-x:auto}.sidebar nav::-webkit-scrollbar{display:none}.nav-item{white-space:nowrap;width:auto;min-height:44px;padding:9px 14px;font-size:14px}.main-inner{padding:22px 18px 48px}.stat-row{grid-template-columns:repeat(2,minmax(0,1fr))}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}.usage-cards{grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{margin-top:4px;font-size:22px;font-weight:600}.usage-range{border:1px solid var(--border);background:var(--surface);border-radius:999px;gap:2px;padding:2px;display:inline-flex}.usage-range-btn{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:none;border-radius:999px;padding:4px 12px}.usage-range-btn.active{background:var(--raised);color:var(--text);font-weight:600}.panel-title{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{color:var(--muted);margin-bottom:-2px;font-size:11px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:3px;display:flex}.heatmap-days{color:var(--muted);flex-shrink:0;grid-template-rows:repeat(7,1fr);align-items:center;width:25px;font-size:10px;display:grid}.heatmap-grid{flex:1;gap:3px;min-width:0;display:grid}.heatmap-week{grid-template-rows:repeat(7,1fr);gap:3px;display:grid}.heatmap-cell{aspect-ratio:1;background:var(--border);border-radius:2px;min-width:0}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{align-self:flex-end;align-items:center;gap:4px;font-size:12px;display:inline-flex}.heatmap-legend .heatmap-cell{aspect-ratio:auto;width:10px;height:10px}.usage-bar{background:var(--border);border-radius:999px;width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:999px;height:100%}.daybars{grid-template-columns:repeat(7,1fr);align-items:end;gap:10px;height:180px;padding-top:8px;display:grid}.daybar{flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%;display:flex;position:relative}.daybar-track{background:var(--border);border-radius:6px;flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:6px 6px 0 0;flex-direction:column-reverse;width:100%;min-height:2px;transition:height .2s;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{color:var(--text);font-size:12px;font-weight:600}.daybar-label{white-space:nowrap;font-size:11px}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);pointer-events:none;border-radius:8px;min-width:160px;padding:8px 10px;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);box-shadow:0 6px 20px #00000059}.daybar-tip-date{color:var(--text);white-space:nowrap;margin-bottom:6px;font-size:12px;font-weight:600}.daybar-tip-row{align-items:center;gap:8px;font-size:12px;line-height:1.7;display:flex}.daybar-tip-swatch{border-radius:2px;flex-shrink:0;width:10px;height:10px}.daybar-tip-name{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:1;max-width:160px;overflow:hidden}.daybar-tip-val{color:var(--muted);font-variant-numeric:tabular-nums}.usage-scroll{max-height:360px;overflow-y:auto}.usage-scroll thead th{z-index:1;background:var(--surface);position:sticky;top:0}
|
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-CgCKvnPc.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DIBiVVC0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/adapters/azure.ts
CHANGED
|
@@ -12,8 +12,8 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
12
12
|
...inner,
|
|
13
13
|
name: "azure-openai",
|
|
14
14
|
|
|
15
|
-
buildRequest(parsed: OcxParsedRequest) {
|
|
16
|
-
const request = inner.buildRequest(parsed);
|
|
15
|
+
async buildRequest(parsed: OcxParsedRequest) {
|
|
16
|
+
const request = await inner.buildRequest(parsed);
|
|
17
17
|
const headers = { ...request.headers };
|
|
18
18
|
if (provider.apiKey) {
|
|
19
19
|
headers["api-key"] = provider.apiKey;
|
package/src/adapters/base.ts
CHANGED
|
@@ -8,7 +8,12 @@ export interface IncomingMeta {
|
|
|
8
8
|
export interface ProviderAdapter {
|
|
9
9
|
name: string;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Build the upstream request. May be async: adapters that resolve a short-lived credential
|
|
13
|
+
* (e.g. Vertex AI ADC token) return a Promise. Sync adapters return the object directly; callers
|
|
14
|
+
* must `await` the result (awaiting a non-Promise is a no-op).
|
|
15
|
+
*/
|
|
16
|
+
buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta): AdapterRequest | Promise<AdapterRequest>;
|
|
12
17
|
|
|
13
18
|
fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
|
|
14
19
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity (Cloud Code Assist) thoughtSignature reasoning-replay cache.
|
|
3
|
+
*
|
|
4
|
+
* Gemini-3 interleaved thinking is stateless upstream: each model content part carries a
|
|
5
|
+
* `thoughtSignature` that MUST be echoed back on the matching part in the next request, or the
|
|
6
|
+
* upstream rejects the turn (HTTP 400). We observe signatures on the response stream, cache them
|
|
7
|
+
* per `model + session`, and re-inject them into the outgoing `request.contents` on the next turn.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors CLIProxyAPI `internal/runtime/executor/antigravity_reasoning_replay.go`. Gemini-only;
|
|
10
|
+
* Claude-on-Antigravity uses inline signature sanitization instead (see google-antigravity-wire).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface ReplayEntry {
|
|
14
|
+
/** thoughtSignature keyed by functionCall identity (name + canonical args). */
|
|
15
|
+
byCall: Map<string, string>;
|
|
16
|
+
expiresAtMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MIN_SIGNATURE_LEN = 16;
|
|
20
|
+
const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
21
|
+
const REPLAY_MAX_ENTRIES = 10_240;
|
|
22
|
+
const REPLAY_EVICT_BATCH = 128;
|
|
23
|
+
|
|
24
|
+
const replayCache = new Map<string, ReplayEntry>();
|
|
25
|
+
|
|
26
|
+
function replayKey(model: string, sessionId: string): string {
|
|
27
|
+
return `${model}::session:${sessionId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Recursively canonicalize a JSON value: object keys sorted, arrays preserved. */
|
|
31
|
+
function canonicalJson(value: unknown): string {
|
|
32
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
33
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
34
|
+
const entries = Object.keys(value as Record<string, unknown>).sort()
|
|
35
|
+
.map(k => `${JSON.stringify(k)}:${canonicalJson((value as Record<string, unknown>)[k])}`);
|
|
36
|
+
return `{${entries.join(",")}}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stable identity for a functionCall part: name + recursively canonicalized args. */
|
|
40
|
+
function functionCallKey(name: unknown, args: unknown): string | undefined {
|
|
41
|
+
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
42
|
+
let argsKey = "";
|
|
43
|
+
try {
|
|
44
|
+
argsKey = canonicalJson(args ?? {});
|
|
45
|
+
} catch {
|
|
46
|
+
argsKey = "";
|
|
47
|
+
}
|
|
48
|
+
return `${name}::${argsKey}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractSignature(part: Record<string, unknown>): string | undefined {
|
|
52
|
+
const direct = part.thoughtSignature ?? part.thought_signature;
|
|
53
|
+
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
|
|
54
|
+
const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
|
|
55
|
+
const nested = extra?.google?.thought_signature;
|
|
56
|
+
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function evictIfNeeded(): void {
|
|
61
|
+
if (replayCache.size <= REPLAY_MAX_ENTRIES) return;
|
|
62
|
+
const oldest = [...replayCache.entries()]
|
|
63
|
+
.sort((a, b) => a[1].expiresAtMs - b[1].expiresAtMs)
|
|
64
|
+
.slice(0, REPLAY_EVICT_BATCH);
|
|
65
|
+
for (const [key] of oldest) replayCache.delete(key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Gemini/Flash/Agent use the replay cache; Claude does not (inline sanitization instead). */
|
|
69
|
+
export function antigravityUsesReplayCache(model: string): boolean {
|
|
70
|
+
return !/claude/i.test(model);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
|
|
75
|
+
* the functionCall identity (name + args). Accumulates across the whole session so a sequential
|
|
76
|
+
* multi-step tool loop keeps EVERY prior call's signature, not just the latest part-index slot.
|
|
77
|
+
* `parts` is the already-unwrapped `response.candidates[0].content.parts`.
|
|
78
|
+
*/
|
|
79
|
+
export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
|
|
80
|
+
if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
|
|
81
|
+
const key = replayKey(model, sessionId);
|
|
82
|
+
const entry = replayCache.get(key) ?? { byCall: new Map<string, string>(), expiresAtMs: 0 };
|
|
83
|
+
let changed = false;
|
|
84
|
+
for (const raw of parts) {
|
|
85
|
+
if (!raw || typeof raw !== "object") continue;
|
|
86
|
+
const part = raw as Record<string, unknown>;
|
|
87
|
+
const sig = extractSignature(part);
|
|
88
|
+
if (!sig) continue;
|
|
89
|
+
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
90
|
+
const ck = fc ? functionCallKey(fc.name, fc.args) : undefined;
|
|
91
|
+
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
92
|
+
if (entry.byCall.get(ck) !== sig) { entry.byCall.set(ck, sig); changed = true; }
|
|
93
|
+
}
|
|
94
|
+
if (!changed && replayCache.has(key)) return;
|
|
95
|
+
entry.expiresAtMs = Date.now() + REPLAY_TTL_MS;
|
|
96
|
+
replayCache.set(key, entry);
|
|
97
|
+
evictIfNeeded();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Re-inject cached thought signatures into the outgoing `request.contents`, matched by functionCall
|
|
102
|
+
* identity across ALL model turns (not just the last one). Only fills a functionCall part that
|
|
103
|
+
* lacks a real signature. Returns the same array reference (mutated in place).
|
|
104
|
+
*/
|
|
105
|
+
export function applyAntigravityReplay(model: string, sessionId: string, contents: unknown[]): unknown[] {
|
|
106
|
+
if (!antigravityUsesReplayCache(model) || !Array.isArray(contents)) return contents;
|
|
107
|
+
const entry = replayCache.get(replayKey(model, sessionId));
|
|
108
|
+
if (!entry || entry.expiresAtMs <= Date.now()) {
|
|
109
|
+
if (entry) replayCache.delete(replayKey(model, sessionId));
|
|
110
|
+
return contents;
|
|
111
|
+
}
|
|
112
|
+
for (const c of contents as { role?: string; parts?: unknown[] }[]) {
|
|
113
|
+
if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
|
|
114
|
+
for (const raw of c.parts) {
|
|
115
|
+
if (!raw || typeof raw !== "object") continue;
|
|
116
|
+
const part = raw as Record<string, unknown>;
|
|
117
|
+
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
118
|
+
if (!fc) continue;
|
|
119
|
+
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
|
|
120
|
+
const ck = functionCallKey(fc.name, fc.args);
|
|
121
|
+
const sig = ck ? entry.byCall.get(ck) : undefined;
|
|
122
|
+
if (sig) part.thoughtSignature = sig;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return contents;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Drop the cache entry when upstream rejects a signature (clear-on-invalid). */
|
|
129
|
+
export function clearAntigravityReplay(model: string, sessionId: string): void {
|
|
130
|
+
replayCache.delete(replayKey(model, sessionId));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Test seam. */
|
|
134
|
+
export function __resetAntigravityReplayCache(): void {
|
|
135
|
+
replayCache.clear();
|
|
136
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxContentPart, OcxParsedRequest } from "../types";
|
|
3
|
+
|
|
4
|
+
/** Antigravity request User-Agent (overridable). Mirrors the Antigravity desktop client UA. */
|
|
5
|
+
export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT || "antigravity";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
|
|
9
|
+
* synthetic Responses item id (`fc_...`, `call_...`, `rs_...`, etc) that the bridge/parser stashes
|
|
10
|
+
* on the field. Only real signatures may be forwarded to Gemini/Antigravity — sending a synthetic
|
|
11
|
+
* id as `thoughtSignature` breaks multi-turn reasoning continuity (upstream rejects it). Real
|
|
12
|
+
* signatures are opaque base64-ish blobs with no Responses-id prefix.
|
|
13
|
+
*/
|
|
14
|
+
export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
|
|
15
|
+
if (typeof sig !== "string" || sig.length < 16) return false;
|
|
16
|
+
// Reject synthetic Responses/tool-call ids in both `_` and `-` separated spellings
|
|
17
|
+
// (e.g. `fc_...`, `call_...`, `function-call-...`, `tool-call-...`).
|
|
18
|
+
if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(sig)) return false;
|
|
19
|
+
// Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
|
|
20
|
+
// Anything containing other characters (or whitespace) is not a real signature.
|
|
21
|
+
return /^[A-Za-z0-9+/_=-]+$/.test(sig);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function firstUserText(parsed: OcxParsedRequest): string | undefined {
|
|
25
|
+
for (const msg of parsed.context.messages) {
|
|
26
|
+
if (msg.role !== "user") continue;
|
|
27
|
+
if (typeof msg.content === "string") return msg.content;
|
|
28
|
+
const first = (msg.content as OcxContentPart[]).find(p => p.type === "text" && typeof p.text === "string");
|
|
29
|
+
if (first && first.type === "text") return first.text;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deterministic Cloud Code Assist session id from the first user message text. Mirrors
|
|
36
|
+
* CLIProxyAPI `generateStableSessionID`: sha256(firstUserText) → BigEndian uint64 masked with
|
|
37
|
+
* 0x7FFFFFFFFFFFFFFF, prefixed with "-". Falls back to a random "-<digits>" id when there is no text.
|
|
38
|
+
*/
|
|
39
|
+
export function antigravitySessionId(parsed: OcxParsedRequest): string {
|
|
40
|
+
// Anchored on the first user message text: this is the one value that stays STABLE across every
|
|
41
|
+
// turn of a conversation, which the replay cache requires (it observes signatures on turn N's
|
|
42
|
+
// response and re-injects them on turn N+1's request, so both turns must map to the same id).
|
|
43
|
+
// Cross-conversation collisions (two threads opening with identical text) are made harmless by
|
|
44
|
+
// the replay cache keying signatures on functionCall identity (name+args), not on this id alone.
|
|
45
|
+
const text = firstUserText(parsed);
|
|
46
|
+
if (!text) return `-${Math.floor(Math.random() * 9e18).toString()}`;
|
|
47
|
+
const digest = createHash("sha256").update(text, "utf8").digest();
|
|
48
|
+
const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
|
|
49
|
+
return `-${masked.toString()}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A Gemini content part as it appears in an Antigravity request body. */
|
|
53
|
+
interface GeminiPart {
|
|
54
|
+
thought?: boolean;
|
|
55
|
+
thoughtSignature?: string;
|
|
56
|
+
thought_signature?: string;
|
|
57
|
+
text?: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
interface GeminiContent {
|
|
61
|
+
role?: string;
|
|
62
|
+
parts?: GeminiPart[];
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasSignature(part: GeminiPart): boolean {
|
|
67
|
+
return typeof part.thoughtSignature === "string" && part.thoughtSignature.length > 0
|
|
68
|
+
|| typeof part.thought_signature === "string" && part.thought_signature.length > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Claude-on-Antigravity signature sanitization (the no-cache path). Mirrors CLIProxyAPI's
|
|
73
|
+
* `StripEmptySignatureThinkingBlocks` + non-model signature stripping: drop thinking parts that
|
|
74
|
+
* carry no valid signature (they would 400 upstream), and strip signature fields from non-model
|
|
75
|
+
* (user) content. Mutates and returns `contents`.
|
|
76
|
+
*/
|
|
77
|
+
export function sanitizeAntigravityClaudeSignatures(contents: unknown[]): unknown[] {
|
|
78
|
+
if (!Array.isArray(contents)) return contents;
|
|
79
|
+
for (const raw of contents as GeminiContent[]) {
|
|
80
|
+
if (!raw || typeof raw !== "object" || !Array.isArray(raw.parts)) continue;
|
|
81
|
+
const isModel = raw.role === "model";
|
|
82
|
+
if (!isModel) {
|
|
83
|
+
// Non-model parts must not carry thought signatures.
|
|
84
|
+
for (const part of raw.parts) {
|
|
85
|
+
delete part.thoughtSignature;
|
|
86
|
+
delete part.thought_signature;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Model turn: drop thinking blocks lacking a valid signature.
|
|
91
|
+
raw.parts = raw.parts.filter(part => !(part.thought === true && !hasSignature(part)));
|
|
92
|
+
}
|
|
93
|
+
return contents;
|
|
94
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { redactSecretString } from "../redact";
|
|
2
|
+
|
|
3
|
+
const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|\/root\/[^ "';,]*|[A-Za-z]:\\Users\\[^ "';,]+)/g;
|
|
4
|
+
|
|
5
|
+
function sanitizeGoogleErrorText(value: string): string {
|
|
6
|
+
return redactSecretString(value).replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function safeString(value: unknown): string | undefined {
|
|
10
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
|
|
14
|
+
function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
|
|
15
|
+
const trimmed = payloadText.trim();
|
|
16
|
+
if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
|
|
17
|
+
return { message: trimmed || undefined };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(trimmed) as { error?: { message?: unknown; status?: unknown } };
|
|
21
|
+
const err = parsed.error;
|
|
22
|
+
return { message: safeString(err?.message), status: safeString(err?.status) };
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
29
|
+
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
|
|
30
|
+
const quotaExhausted =
|
|
31
|
+
lower.includes("quotafailure") ||
|
|
32
|
+
lower.includes("quota exceeded") ||
|
|
33
|
+
lower.includes("exceeded your current quota") ||
|
|
34
|
+
lower.includes("billing");
|
|
35
|
+
if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
|
|
36
|
+
if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
|
|
37
|
+
return `${label} rate limit exceeded`;
|
|
38
|
+
}
|
|
39
|
+
if (status === 401 || enumStatus === "UNAUTHENTICATED" || lower.includes("unauthenticated") || lower.includes("invalid authentication") || lower.includes("expired")) {
|
|
40
|
+
return `${label} authentication failed`;
|
|
41
|
+
}
|
|
42
|
+
if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission denied") || lower.includes("access denied")) {
|
|
43
|
+
return `${label} access denied`;
|
|
44
|
+
}
|
|
45
|
+
if (status === 503 || enumStatus === "UNAVAILABLE" || lower.includes("overloaded") || lower.includes("unavailable")) {
|
|
46
|
+
return `${label} server overloaded`;
|
|
47
|
+
}
|
|
48
|
+
if (status === 400 || status === 404 || enumStatus === "INVALID_ARGUMENT" || enumStatus === "NOT_FOUND" || lower.includes("invalid") || lower.includes("not found") || lower.includes("malformed")) {
|
|
49
|
+
return `${label} invalid request`;
|
|
50
|
+
}
|
|
51
|
+
return `${label} upstream error`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalize a Google/Vertex/Antigravity HTTP error body into a short, classified, secret-redacted
|
|
56
|
+
* message. Mirrors `kiro-errors.ts`. `label` is the provider-facing prefix ("Vertex AI",
|
|
57
|
+
* "Antigravity").
|
|
58
|
+
*/
|
|
59
|
+
export function safeGoogleHttpErrorMessage(label: string, status: number, payloadText: string): string {
|
|
60
|
+
const { message, status: enumStatus } = googleErrorDetail(payloadText);
|
|
61
|
+
const prefix = classifyGoogle(label, status, enumStatus, [message, enumStatus].filter(Boolean).join(" "));
|
|
62
|
+
const detail = message ? sanitizeGoogleErrorText(message).slice(0, 500) : `HTTP ${status}`;
|
|
63
|
+
return `${prefix}: ${detail}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Vertex AI HTTP error message (label = "Vertex AI"). */
|
|
67
|
+
export function safeVertexHttpErrorMessage(status: number, payloadText: string): string {
|
|
68
|
+
return safeGoogleHttpErrorMessage("Vertex AI", status, payloadText);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Antigravity (Cloud Code Assist) HTTP error message (label = "Antigravity"). */
|
|
72
|
+
export function safeAntigravityHttpErrorMessage(status: number, payloadText: string): string {
|
|
73
|
+
return safeGoogleHttpErrorMessage("Antigravity", status, payloadText);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Google-family retryable HTTP set (mirrors Kiro). Quota-exhausted is classified above and not retried. */
|
|
77
|
+
export function retryableGoogleStatus(status: number): boolean {
|
|
78
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* True when a 429 body indicates hard quota exhaustion (not a transient rate limit). Quota
|
|
83
|
+
* exhaustion is generally not expected to recover for hours (AIP-194), so it must NOT be retried —
|
|
84
|
+
* unlike a plain rate limit. The HTTP status alone can't distinguish the two, so the retry layer
|
|
85
|
+
* inspects the body with this.
|
|
86
|
+
*/
|
|
87
|
+
export function isQuotaExhaustedBody(payloadText: string): boolean {
|
|
88
|
+
const { message, status } = googleErrorDetail(payloadText);
|
|
89
|
+
if (status !== "RESOURCE_EXHAUSTED") return false;
|
|
90
|
+
const lower = (message ?? "").toLowerCase();
|
|
91
|
+
return lower.includes("quotafailure")
|
|
92
|
+
|| lower.includes("quota exceeded")
|
|
93
|
+
|| lower.includes("exceeded your current quota")
|
|
94
|
+
|| lower.includes("billing");
|
|
95
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
|
+
import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
|
|
3
|
+
|
|
4
|
+
const GOOGLE_RETRY_ATTEMPTS = 3;
|
|
5
|
+
const GOOGLE_RETRY_BASE_MS = 250;
|
|
6
|
+
const GOOGLE_RETRY_MAX_MS = 2_000;
|
|
7
|
+
|
|
8
|
+
function retryAfterMs(headers: Headers): number | undefined {
|
|
9
|
+
const raw = headers.get("retry-after")?.trim();
|
|
10
|
+
if (!raw) return undefined;
|
|
11
|
+
const seconds = Number(raw);
|
|
12
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
|
|
13
|
+
const dateMs = Date.parse(raw);
|
|
14
|
+
if (!Number.isFinite(dateMs)) return undefined;
|
|
15
|
+
return Math.max(0, dateMs - Date.now());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function retryDelayMs(attempt: number, headers?: Headers): number {
|
|
19
|
+
const retryAfter = headers ? retryAfterMs(headers) : undefined;
|
|
20
|
+
if (retryAfter !== undefined) return Math.min(retryAfter, GOOGLE_RETRY_MAX_MS);
|
|
21
|
+
const exp = Math.min(GOOGLE_RETRY_BASE_MS * (2 ** attempt), GOOGLE_RETRY_MAX_MS);
|
|
22
|
+
return Math.floor(exp * (0.8 + Math.random() * 0.4));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function abortError(signal?: AbortSignal): unknown {
|
|
26
|
+
return signal?.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
30
|
+
if (ms <= 0) return;
|
|
31
|
+
if (signal?.aborted) throw abortError(signal);
|
|
32
|
+
await new Promise<void>((resolve, reject) => {
|
|
33
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
34
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", onAbort); };
|
|
35
|
+
const onAbort = () => { cleanup(); reject(abortError(signal)); };
|
|
36
|
+
timer = setTimeout(() => { cleanup(); resolve(); }, ms);
|
|
37
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function signalWithAttemptTimeout(parent: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
|
42
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
43
|
+
return parent ? AbortSignal.any([parent, timeout]) : timeout;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function normalizeFinalGoogleError(label: string, res: Response): Promise<Response> {
|
|
47
|
+
if (res.ok) return res;
|
|
48
|
+
const payloadText = await res.clone().text().catch(() => "");
|
|
49
|
+
const headers = new Headers(res.headers);
|
|
50
|
+
headers.delete("content-encoding");
|
|
51
|
+
headers.delete("content-length");
|
|
52
|
+
return new Response(safeGoogleHttpErrorMessage(label, res.status, payloadText), {
|
|
53
|
+
status: res.status, statusText: res.statusText, headers,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetch a Google-family upstream (Vertex / Antigravity) with Kiro-style hardening: per-attempt
|
|
59
|
+
* timeout (`AbortSignal.any([parent, timeout])`), bounded retry on transient status / network
|
|
60
|
+
* errors, `Retry-After` honoring, jittered exponential backoff, and a classified + redacted final
|
|
61
|
+
* error body. `label` is the provider-facing prefix used in error messages.
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchGoogleWithRetry(label: string, request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
64
|
+
const timeoutMs = ctx.timeoutMs ?? 100_000;
|
|
65
|
+
let lastError: unknown;
|
|
66
|
+
for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
|
|
67
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(request.url, {
|
|
70
|
+
method: request.method, headers: request.headers, body: request.body,
|
|
71
|
+
signal: signalWithAttemptTimeout(ctx.abortSignal, timeoutMs),
|
|
72
|
+
});
|
|
73
|
+
if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
|
|
74
|
+
return normalizeFinalGoogleError(label, res);
|
|
75
|
+
}
|
|
76
|
+
// A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
|
|
77
|
+
// it won't recover for hours and burns retries). Peek the body to tell them apart.
|
|
78
|
+
if (res.status === 429) {
|
|
79
|
+
const peek = await res.clone().text().catch(() => "");
|
|
80
|
+
if (isQuotaExhaustedBody(peek)) {
|
|
81
|
+
const headers = new Headers(res.headers);
|
|
82
|
+
headers.delete("content-encoding");
|
|
83
|
+
headers.delete("content-length");
|
|
84
|
+
return new Response(safeGoogleHttpErrorMessage(label, res.status, peek), {
|
|
85
|
+
status: res.status, statusText: res.statusText, headers,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await res.body?.cancel().catch(() => {});
|
|
90
|
+
await sleepWithAbort(retryDelayMs(attempt, res.headers), ctx.abortSignal);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (ctx.abortSignal?.aborted) throw err;
|
|
93
|
+
lastError = err;
|
|
94
|
+
if (attempt === GOOGLE_RETRY_ATTEMPTS - 1) throw err;
|
|
95
|
+
await sleepWithAbort(retryDelayMs(attempt), ctx.abortSignal);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw lastError ?? new Error(`${label} fetch failed`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Vertex AI retry wrapper. */
|
|
102
|
+
export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
103
|
+
return fetchGoogleWithRetry("Vertex AI", request, ctx);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Antigravity (Cloud Code Assist) retry wrapper. */
|
|
107
|
+
export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
108
|
+
return fetchGoogleWithRetry("Antigravity", request, ctx);
|
|
109
|
+
}
|