@bitkyc08/opencodex 2.7.18 → 2.7.20
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/README.ko.md +2 -1
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/gui/dist/assets/index-BH-RbAg4.js +40 -0
- package/gui/dist/assets/index-BpqkH5f_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +16 -4
- package/src/adapters/cursor/native-exec-desktop.ts +10 -3
- package/src/adapters/mimo-free.ts +228 -0
- package/src/adapters/openai-chat.ts +104 -1
- package/src/claude/inbound.ts +3 -1
- package/src/cli/claude.ts +21 -2
- package/src/cli/v2.ts +21 -5
- package/src/codex/catalog.ts +14 -1
- package/src/codex/project-config-warnings.ts +18 -6
- package/src/lib/win-exec.ts +105 -0
- package/src/oauth/callback-server.ts +17 -7
- package/src/oauth/index.ts +93 -0
- package/src/oauth/types.ts +1 -1
- package/src/providers/derive.ts +4 -0
- package/src/providers/registry.ts +37 -1
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management-api.ts +34 -4
- package/src/server/relay.ts +6 -2
- package/src/server/request-log.ts +4 -1
- package/gui/dist/assets/index-BUBsQALh.css +0 -1
- package/gui/dist/assets/index-DEbBFENM.js +0 -40
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#fff)var(--lightningcss-dark,#212121);--rail:var(--lightningcss-light,#f9f9f9)var(--lightningcss-dark,#171717);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#262626);--raised:var(--lightningcss-light,#f4f4f4)var(--lightningcss-dark,#303030);--raised-hover:var(--lightningcss-light,#ececec)var(--lightningcss-dark,#3a3a3a);--border:var(--lightningcss-light,#e6e6e6)var(--lightningcss-dark,#3d3d3d);--border-soft:var(--lightningcss-light,#f0f0f0)var(--lightningcss-dark,#333);--hover:var(--lightningcss-light,#0d0d0d08)var(--lightningcss-dark,#ffffff08);--text:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--muted:var(--lightningcss-light,#6e6e6e)var(--lightningcss-dark,#a6a6a6);--faint:var(--lightningcss-light,#707070)var(--lightningcss-dark,#9a9a9a);--accent:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--accent-hover:var(--lightningcss-light,#3d3d3d)var(--lightningcss-dark,#fff);--accent-ink:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d);--accent-soft:var(--lightningcss-light,#0d0d0d0f)var(--lightningcss-dark,#ffffff17);--accent-ring:var(--lightningcss-light,#0d0d0d4d)var(--lightningcss-dark,#ffffff61);--green:var(--lightningcss-light,#0a7d5c)var(--lightningcss-dark,#4ecb9d);--green-soft:var(--lightningcss-light,#10a37f1a)var(--lightningcss-dark,#4ecb9d21);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#9a4a08)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--space-0-5:2px;--space-1:4px;--space-1-5:6px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--radius-2xs:4px;--radius:12px;--radius-sm:8px;--radius-xs:6px;--radius-lg:16px;--radius-round:50%;--radius-pill:999px;--font-ui:"OpenAI Sans", "Pretendard Variable", Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;--font-code:ui-monospace, "SFMono-Regular", "Cascadia Code", "JetBrains Mono", "Noto Sans Mono CJK KR", Menlo, Consolas, monospace;--font:var(--font-ui);--mono:var(--font-code);--text-micro:10px;--text-caption:11px;--text-label:12px;--text-control:13px;--text-body:14px;--text-subtitle:16px;--text-title:20px;--text-display:24px;--weight-regular:400;--weight-medium:500;--weight-semibold:600;--weight-bold:700;--leading-tight:1.2;--leading-ui:1.35;--leading-body:1.5;--leading-relaxed:1.6;--tracking-normal:0;--tracking-wide:.04em;--control-sm:28px;--control-md:34px;--control-lg:40px;--control-touch:44px;--icon-sm:14px;--icon-md:16px;--icon-lg:20px;--motion-fast:.12s;--motion-normal:.18s;--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);--toggle-w:36px;--toggle-h:20px;--toggle-dot:14px;--toggle-off-bg:var(--lightningcss-light,#d4d4d4)var(--lightningcss-dark,#4a4a4a);--toggle-on-bg:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#4ecb9d);--toggle-dot-color:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d)}@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}:root{--glass-rail:var(--lightningcss-light,#f9f9f9a8)var(--lightningcss-dark,#1717179e);--glass-panel:var(--lightningcss-light,#ffffffc7)var(--lightningcss-dark,#262626d1);--glass-blur:saturate(1.6) blur(22px)}*{box-sizing:border-box}html,body,#root{height:100%}html{background:var(--bg);overflow-x:hidden}body{color:var(--text);font-family:var(--font-ui);font-size:var(--text-body);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background:0 0;margin:0;overflow-x:hidden}body:before{content:"";z-index:-1;pointer-events:none;filter:blur(70px);background:radial-gradient(42% 38% at 12% 6%,var(--lightningcss-light,#a4c4ff6b)var(--lightningcss-dark,#6082c829),#0000 70%),radial-gradient(46% 42% at 88% 18%,var(--lightningcss-light,#a8e2c561)var(--lightningcss-dark,#58a08221),#0000 70%),radial-gradient(40% 36% at 70% 92%,var(--lightningcss-light,#ffe0c24d)var(--lightningcss-dark,#b48c6414),#0000 72%);position:fixed;inset:-20%}a{color:var(--text);text-decoration:underline;-webkit-text-decoration-color:var(--faint);text-decoration-color:var(--faint);text-underline-offset:2px}a:hover{-webkit-text-decoration-color:var(--text);text-decoration-color:var(--text)}code{font-family:var(--font-code);font-size:var(--text-label)}.mono{font-family:var(--font-code);font-variant-numeric:tabular-nums}h1,h2,h3,h4{font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight);margin:0}.text-micro{line-height:var(--leading-ui);font-size:var(--text-micro)!important}.text-caption{line-height:var(--leading-ui);font-size:var(--text-caption)!important}.text-label{line-height:var(--leading-ui);font-size:var(--text-label)!important}.text-control{line-height:var(--leading-ui);font-size:var(--text-control)!important}.text-body{line-height:var(--leading-body);font-size:var(--text-body)!important}.text-subtitle{line-height:var(--leading-tight);font-size:var(--text-subtitle)!important}.text-title{line-height:var(--leading-tight);font-size:var(--text-title)!important}.text-display{line-height:var(--leading-tight);font-size:var(--text-display)!important}.font-regular{font-weight:var(--weight-regular)!important}.font-medium{font-weight:var(--weight-medium)!important}.font-semibold{font-weight:var(--weight-semibold)!important}.font-bold{font-weight:var(--weight-bold)!important}.leading-tight{line-height:var(--leading-tight)!important}.leading-ui{line-height:var(--leading-ui)!important}.leading-body{line-height:var(--leading-body)!important}.leading-relaxed{line-height:var(--leading-relaxed)!important}::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-radius:var(--radius-pill);border:2px solid var(--bg)}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--glass-rail);height:100dvh;-webkit-backdrop-filter:var(--glass-blur);flex-direction:column;align-self:start;gap:4px;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{font-weight:var(--weight-semibold);font-size:var(--text-subtitle);letter-spacing:0;line-height:26px}.brand .ver{font-family:var(--font-code);font-size:var(--text-micro);color:var(--muted);line-height:var(--leading-tight);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-pill);align-self:center;padding:2px 6px}.sidebar nav{flex-direction:column;gap:4px;display:flex}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;display:flex}.nav-item:hover,.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item.active{font-weight:var(--weight-semibold)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--text)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.drawer-head{align-items:flex-start;display:flex}.drawer-head .brand{flex:auto;min-width:0}.mobile-topbar{display:none}.menu-toggle{border-radius:var(--radius-sm);min-width:44px;min-height:44px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:8px;display:none}.menu-toggle:hover{background:var(--accent-soft);color:var(--text)}.menu-toggle svg{width:20px;height:20px}.drawer-scrim{z-index:30;background:var(--lightningcss-light,#14141452)var(--lightningcss-dark,#00000085);display:none;position:fixed;inset:0}.sidebar-link{color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;text-decoration:none;display:flex}.sidebar-link:hover{background:var(--accent-soft);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.lang-toggle{width:100%;color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);align-items:center;gap:9px;padding:8px 10px;display:flex;position:relative}.lang-toggle:hover{background:var(--accent-soft);color:var(--text)}.lang-toggle svg{flex-shrink:0;width:16px;height:16px}.lang-toggle .custom-select{width:100%;position:static!important}.lang-toggle .select-trigger{width:100%;color:inherit;font-size:var(--text-control);min-height:auto;box-shadow:none;-webkit-backdrop-filter:none;background:0 0;border:none;justify-content:space-between;padding:0}.lang-toggle .select-trigger:hover:not(:disabled){color:inherit;box-shadow:none;background:0 0;border:none}.lang-toggle .select-dropdown{background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);border:1px solid var(--border);box-shadow:var(--shadow-sm)}.lang-toggle .select-dropdown-beside{min-width:10rem;top:0;left:calc(100% + 6px);right:auto}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;display:flex}.theme-toggle:hover{background:var(--accent-soft);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:var(--text-title)}.page-sub{color:var(--muted);font-size:var(--text-body);max-width:70ch;margin:4px 0 22px}.page-sub b{color:var(--text);font-weight:var(--weight-semibold)}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{font-size:var(--text-label);color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px}.btn{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast);white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:8px 16px;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(--bg);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{font-size:var(--text-label);border-radius:var(--radius-pill);padding:4px 12px}.btn-icon{appearance:none;font:inherit;cursor:pointer;width:28px;height:28px;color:var(--muted);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.btn-icon:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.btn-icon svg{width:16px;height:16px;display:block}.btn.btn-ghost.btn-icon{width:28px;height:28px;padding:0}.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{border-color:color-mix(in srgb, var(--accent) 28%, var(--border));background:color-mix(in srgb, var(--accent) 5%, var(--surface))}.api-panel{flex-direction:column;gap:10px;display:flex;overflow:hidden}.api-panel .panel-title,.api-panel .muted{margin:0}.api-form-row{align-items:center;gap:8px;min-width:0;display:flex}.api-form-row .input{flex:1;min-width:0}.api-code{min-width:0;color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-relaxed);white-space:pre;margin:0;padding:9px 11px;display:block;overflow-x:auto}.api-code-inline{white-space:nowrap}.api-actions{justify-content:flex-end;align-items:center;gap:4px;display:flex}.maintenance-head{flex-wrap:wrap;align-items:flex-start}.maintenance-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.maintenance-notice{align-items:flex-start;margin:14px 0 0}.update-row{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.update-row .field-label{margin:0}.update-empty{margin-bottom:14px;padding:18px}.update-box{flex-direction:column;gap:12px;display:flex}.update-command{font-size:var(--text-label);flex-wrap:wrap;align-items:flex-start;gap:6px;display:flex}.update-command .chip{white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;flex:auto;min-width:0;max-width:100%;line-height:1.5}.update-recheck{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.update-recheck .btn{flex:none}.update-recheck-reason{min-width:0;font-size:var(--text-label);color:var(--muted);flex:200px}.update-restart{border-top:1px solid var(--border-soft);padding-top:12px}.injection-head{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.injection-label{font-weight:var(--weight-semibold)}@media (width<=800px){.injection-head{gap:8px}.injection-label{flex:1 0 calc(100% - 70px);order:1}.injection-head>.badge{order:1;margin-left:auto}.injection-head>.custom-select{order:2}}.stat-row{grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:28px;display:grid}.stat-row>.stat{flex-direction:column;justify-content:center;min-height:80px;display:flex}@media (width<=1100px){.stat-row{grid-template-columns:repeat(3,1fr)}}@media (width<=640px){.stat-row{grid-template-columns:repeat(2,1fr)}}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);transition:border-color var(--motion-fast);padding:14px 16px}.stat:hover{border-color:var(--faint)}.stat .label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);align-items:center;gap:6px;margin-bottom:6px;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{font-size:var(--text-title);font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight)}.stat .value.mono{font-family:var(--font-code);font-size:var(--text-subtitle)}.model-group-head{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text);align-items:baseline;gap:8px;margin:0 0 8px;display:flex}.model-group-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.group-head{cursor:pointer;background:var(--surface);transition:background var(--motion-fast);padding:10px 12px}.group-head:hover{background:var(--hover)}.group-head.open{border-bottom:1px solid var(--border-soft)}.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);transition:border-color var(--motion-fast), background var(--motion-fast);padding:10px 12px}.model-card:hover{border-color:var(--faint);background:var(--hover)}.model-card .id{font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;color:var(--text)}.badge{font-size:var(--text-caption);font-weight:var(--weight-semibold);line-height:var(--leading-ui);border-radius:var(--radius-pill);font-family:var(--font-code);letter-spacing:0;border:1px solid #0000;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--text)}.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;transition:filter var(--motion-fast);appearance:none}.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{border-radius:var(--radius-round);background:var(--amber-soft);width:44px;height:44px;color:var(--amber);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);transition:border-color var(--motion-fast);padding:8px 10px}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);font-size:var(--text-label);font-weight:var(--weight-semibold);align-items:center;gap:6px;margin-bottom:3px;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:var(--text-caption);font-family:var(--font-code);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:var(--weight-semibold)}.dot{border-radius:var(--radius-round);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:var(--text-control)}.tbl thead th{text-align:left;color:var(--muted);font-weight:var(--weight-medium);font-size:var(--text-label);border-bottom:1px solid var(--border);padding:9px 12px}.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(--font-code);font-variant-numeric:tabular-nums}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);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;font-size:var(--text-control);line-height:var(--leading-ui);transition:border-color var(--motion-fast);padding:8px 11px}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--faint);box-shadow:0 0 0 3px var(--accent-soft);outline:none}textarea.input{resize:vertical;font-family:var(--font-code);line-height:var(--leading-relaxed)}.field-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);margin-bottom:5px;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);padding:6px 12px;box-shadow:0 2px 8px #0000000f}.select-sm:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}.select-sm:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-sm:disabled{opacity:.5;cursor:default;-webkit-backdrop-filter:none}.select-trigger{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;align-items:center;gap:6px;padding:6px 12px;display:inline-flex;box-shadow:0 2px 8px #0000000f}.select-trigger:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-trigger:disabled{opacity:.5;cursor:default}.select-dropdown{z-index:40;background:color-mix(in oklab, canvas 84%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius);min-width:100%;max-height:280px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;overflow-y:auto;box-shadow:0 8px 32px #00000029}.select-dropdown-right{right:0;left:auto!important}.select-dropdown-beside{min-width:10rem;top:0;left:calc(100% + 6px);right:auto}.select-option{text-align:left;border-radius:var(--radius-sm);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast);white-space:nowrap;background:0 0;border:none;padding:7px 12px;display:block}.select-option:hover{background:var(--hover)}.select-option.active{background:var(--accent-soft);color:var(--accent);font-weight:var(--weight-semibold)}.switch{border-radius:var(--radius-pill);border:1px solid var(--border);cursor:pointer;background:var(--raised);vertical-align:middle;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);appearance:none;flex-shrink:0;align-items:center;padding:2px;line-height:0;display:inline-flex}.switch.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{border-radius:var(--radius-round);width:14px;height:14px;transition:transform var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);transform:translate(0);box-shadow:0 0 0 1px #10182814,0 1px 1px #1018282e}.switch.on .knob{background:var(--toggle-dot-color);transform:translate(14px);box-shadow:0 0 0 1px #0000001f}.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}.setting-hint{font-size:var(--text-control);line-height:var(--leading-body);margin-top:3px}.stack{flex-direction:column;display:flex}.chip{font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-ui);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px}.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);font-weight:var(--weight-semibold);margin-bottom:6px}.notice{font-size:var(--text-control);line-height:var(--leading-body);border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;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{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;display:flex}.h-section .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);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}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.sidebar,.lang-toggle .select-dropdown{background:var(--rail)}.modal-card{background:var(--surface)}}@media (prefers-reduced-transparency:reduce){body:before{display:none}.sidebar,.lang-toggle .select-dropdown{background:var(--rail);-webkit-backdrop-filter:none}.modal-card{background:var(--surface);-webkit-backdrop-filter:none}.modal-overlay{-webkit-backdrop-filter:none}}.modal-overlay{-webkit-backdrop-filter:blur(40px)saturate(1.2);z-index:50;background:var(--lightningcss-light,#11131cc7)var(--lightningcss-dark,#000000d1);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:color-mix(in oklab, canvas 92%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-lg);width:100%;max-width:520px;max-height:84vh;padding:20px;overflow-y:auto;box-shadow:0 8px 32px #0000002e}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:var(--text-subtitle)}.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{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px}.card-active{border-color:var(--accent-ring)}.card-right{font-size:var(--text-caption);color:var(--faint);align-items:center;gap:4px;margin-left:auto;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{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.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{font-size:var(--text-caption);color:var(--muted);font-weight:var(--weight-semibold)}.quota-val{font-size:var(--text-caption);font-family:var(--font-code);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-caption);color:var(--faint);overflow:hidden}.quota-reset-day,.quota-reset-time{font-size:var(--text-caption);color:var(--muted);white-space:nowrap}.quota-reset-time{font-family:var(--font-code)}.bar{background:var(--raised);border-radius:var(--radius-2xs);min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:var(--radius-2xs);height:100%;transition:width var(--motion-normal)}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{width:var(--toggle-w);height:var(--toggle-h);border-radius:var(--radius-pill);background:var(--toggle-off-bg);border:1px solid var(--border);cursor:pointer;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0;position:relative}.toggle.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.toggle-knob{border-radius:var(--radius-round);width:16px;height:16px;transition:left var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{background:var(--toggle-dot-color);left:17px}.notice-warn{font-size:var(--text-label);line-height:var(--leading-body);border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{font-size:var(--text-control);line-height:var(--leading-body);color:var(--muted);margin-bottom:14px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.log-reqid{-webkit-line-clamp:2;word-break:break-all;-webkit-box-orient:vertical;max-width:14ch;display:-webkit-box;overflow:hidden}.log-col-model{overflow-wrap:break-word;word-break:normal;max-width:18ch}.log-col-tokens{min-width:10ch}.log-status-cell{flex-direction:column;align-items:flex-start;gap:2px;display:inline-flex}.log-detail-btn{cursor:pointer;color:var(--accent-hover);font:inherit;font-size:var(--text-caption);white-space:nowrap;background:0 0;border:none;padding:0;text-decoration:underline}.log-detail-grid{font-size:var(--text-control);grid-template-columns:max-content minmax(0,1fr);gap:6px 14px;display:grid}.log-detail-break{word-break:break-all}.log-detail-json{background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-caption);line-height:var(--leading-body);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;overflow:auto}.setup-guide{font-size:var(--text-control);line-height:var(--leading-body);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:var(--weight-medium)}.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;transition:background var(--motion-fast), border-color var(--motion-fast);justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-weight:var(--weight-semibold);font-size:var(--text-body)}.list-row .sub{font-size:var(--text-label);color:var(--muted);margin-top:2px}.prov-card{overflow:hidden}.prov-card-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.prov-card-info{flex:360px;align-items:flex-start;gap:11px;min-width:0;display:flex}.prov-card-copy{flex:1;min-width:0}.prov-title{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;margin-bottom:5px;display:flex}.provider-icon{border-radius:var(--radius-xs);background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);flex:none;justify-content:center;align-items:center;display:inline-flex}.provider-icon img{object-fit:contain;width:19px;height:19px;display:block}.prov-meta{flex-wrap:wrap;align-items:center;gap:5px;min-width:0;display:flex}.prov-meta>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;overflow:hidden}.provider-quota{padding-left:58px}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.link-btn{color:var(--accent-hover);font:inherit;font-size:var(--text-control);cursor:pointer;background:0 0;border:none;padding:6px 2px;text-decoration:underline}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;font-size:var(--text-label);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform var(--motion-normal) ease;display:inline-flex}.prov-accounts-toggle .chev svg{width:12px;height:12px;transform:rotate(90deg)}.prov-accounts-toggle.open .chev svg{transform:rotate(-90deg)}.prov-accounts-list{border-top:1px solid var(--border-soft);flex-direction:column;gap:2px;padding:6px 16px 10px;display:flex}.prov-account-row{text-align:left;border-radius:var(--radius-xs);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;min-height:32px;padding:6px 8px;display:flex}.prov-account-row:hover{background:var(--raised)}.prov-account-row.active{cursor:default}.prov-account-row .prov-account-email{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.prov-account-row .badge{flex:none}.prov-account-remove{color:var(--muted);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:4px;display:inline-flex}.prov-account-remove:hover{color:var(--red);background:var(--red-soft)}.prov-account-add{color:var(--muted);font-size:var(--text-label)}.prov-account-add:hover{color:var(--accent-hover)}.prov-account-keyform{cursor:default;gap:6px}.prov-account-keyform:hover{background:0 0}.prov-account-keyform .input-sm{min-width:0;font-size:var(--text-label);height:var(--control-sm);flex:auto;padding:4px 8px}.oauth-grid{grid-template-columns:minmax(140px,max-content) minmax(0,1fr) max-content;align-items:center;gap:10px 16px;display:grid}.oauth-row{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;min-height:30px;display:grid}.oauth-name{font-size:var(--text-control);font-weight:var(--weight-semibold);align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:var(--radius-xs);width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{font-size:var(--text-control);align-items:center;gap:7px;min-width:0;display:inline-flex}.oauth-email{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.oauth-actions{justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-login-hint{font-size:var(--text-label);line-height:var(--leading-body);flex-direction:column;grid-column:1/-1;align-items:stretch;gap:8px;display:flex}.oauth-login-hint-links{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}.oauth-login-paste{align-items:center;gap:8px;width:100%;display:flex}.oauth-login-paste .input{min-width:0;font-size:var(--text-label);flex:1;padding:6px 10px}@media (width<=760px){.app{grid-template-rows:auto 1fr;grid-template-columns:1fr}.mobile-topbar{z-index:20;border-bottom:1px solid var(--border);background:var(--glass-rail);min-width:0;-webkit-backdrop-filter:var(--glass-blur);align-items:center;gap:2px;padding:4px 10px;display:flex;position:sticky;top:0}.mobile-topbar .brand{flex:auto;min-width:0;padding:4px}.mobile-topbar .stop-toggle{justify-content:center;width:auto;min-width:44px;min-height:44px;padding:8px}.menu-toggle{display:inline-flex}.sidebar{z-index:40;width:min(280px,84vw);height:100dvh;padding-bottom:calc(18px + env(safe-area-inset-bottom));visibility:hidden;transition:transform var(--motion-normal) ease, visibility var(--motion-normal);background:var(--lightningcss-light,#f9f9f9f7)var(--lightningcss-dark,#171717f5);outline:none;position:fixed;top:0;bottom:0;left:0;overflow-y:auto;transform:translate(-100%)}.sidebar.open{visibility:visible;transform:translate(0);box-shadow:0 12px 40px var(--lightningcss-light,#14141438)var(--lightningcss-dark,#00000080)}.drawer-scrim{display:block}.main-inner{padding:22px 18px 48px}.setting-row{flex-wrap:wrap}.setting-row .setting-copy{flex:100%!important}.setting-row .setting-controls{width:100%}.setting-row .setting-controls .select-sm{flex:1 1 0;min-width:0}.setting-row>.select-sm{width:100%}.api-form-row{flex-direction:column;align-items:stretch}.api-form-row .btn{width:100%;min-height:40px}.stat-row>.stat{min-width:100px}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))!important}.provider-quota{padding-left:16px}.prov-meta{grid-template-columns:max-content minmax(0,1fr);width:100%;display:grid}.prov-meta .chip~span:not(:last-child){display:none}.oauth-grid{grid-template-columns:minmax(0,auto) minmax(0,1fr) max-content;column-gap:8px}.sidebar .lang-toggle .select-dropdown-beside{inset:auto auto calc(100% + 6px) 0}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{font-size:var(--text-title);font-weight:var(--weight-semibold);margin-top:4px}.usage-range{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.usage-range-btn{color:var(--muted);border-radius:var(--radius-pill);cursor:pointer;font:inherit;background:0 0;border:none;padding:4px 12px}.usage-range-btn.active{background:var(--raised);color:var(--text);font-weight:var(--weight-semibold)}.panel-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 12px}.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{--hm-cell:11px;--hm-gap:3px;flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{font-size:var(--text-caption);color:var(--muted);width:max-content;margin-bottom:-2px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:var(--hm-gap);width:max-content;display:flex}.heatmap-days{grid-template-rows:repeat(7, var(--hm-cell));row-gap:var(--hm-gap);font-size:var(--text-micro);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;display:grid;position:sticky;left:0}.heatmap-grid{gap:var(--hm-gap);display:grid}.heatmap-week{grid-template-rows:repeat(7, var(--hm-cell));gap:var(--hm-gap);display:grid}.heatmap-cell{width:var(--hm-cell);height:var(--hm-cell);border-radius:var(--radius-2xs);background:var(--border)}.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{font-size:var(--text-label);align-self:flex-end;align-items:center;gap:4px;display:inline-flex;position:sticky;right:0}.heatmap-legend .heatmap-cell{width:10px;height:10px}.heatmap-tip{z-index:10;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:nowrap;font-size:var(--text-label);padding:6px 10px;position:fixed;transform:translate(-50%,-100%)translateY(-8px);box-shadow:0 6px 20px #00000059}.heatmap-tip-date{font-weight:var(--weight-semibold);color:var(--text);margin-bottom:2px}.heatmap-tip-val{color:var(--text);font-variant-numeric:tabular-nums}.heatmap-tip-req{font-size:var(--text-caption)}.usage-bar{background:var(--border);border-radius:var(--radius-pill);width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:var(--radius-pill);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:var(--radius-xs);flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:var(--radius-xs) var(--radius-xs) 0 0;width:100%;min-height:2px;transition:height var(--motion-normal) ease;flex-direction:column-reverse;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text)}.daybar-label{font-size:var(--text-caption);white-space:nowrap}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);pointer-events:none;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{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);white-space:nowrap;margin-bottom:6px}.daybar-tip-row{font-size:var(--text-label);line-height:var(--leading-relaxed);align-items:center;gap:8px;display:flex}.daybar-tip-swatch{border-radius:var(--radius-2xs);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}.toggle{width:var(--toggle-w);height:var(--toggle-h);flex-shrink:0;display:inline-block;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle .slider{background:var(--toggle-off-bg);border-radius:var(--radius-pill);cursor:pointer;transition:background var(--motion-normal) ease;position:absolute;inset:0}.toggle .slider:after{content:"";width:var(--toggle-dot);height:var(--toggle-dot);background:var(--toggle-dot-color);border-radius:var(--radius-round);transition:transform var(--motion-normal) ease;position:absolute;top:3px;left:3px}.toggle input:checked+.slider{background:var(--toggle-on-bg)}.toggle input:checked+.slider:after{transform:translateX(calc(var(--toggle-w) - var(--toggle-dot) - 6px))}.toggle input:focus-visible+.slider{outline:2px solid var(--accent-ring);outline-offset:2px}.setting-row{justify-content:space-between;align-items:center;gap:16px;padding:12px 16px;display:flex}.setting-row+.setting-row{border-top:1px solid var(--border-soft)}.setting-label{flex-direction:column;gap:2px;min-width:0;display:flex}.setting-label .title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.setting-label .desc{font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}
|
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-BH-RbAg4.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BpqkH5f_.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.7.
|
|
4
|
-
"description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
|
|
3
|
+
"version": "2.7.20",
|
|
4
|
+
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"prepack": "bun run prepare:package",
|
|
46
46
|
"prepublishOnly": "bun run typecheck && bun run build:gui",
|
|
47
47
|
"release": "bun scripts/release.ts",
|
|
48
|
-
"release:watch": "bun scripts/release.ts watch"
|
|
48
|
+
"release:watch": "bun scripts/release.ts watch",
|
|
49
|
+
"prepush": "bun run typecheck && bun run test && bun run privacy:scan",
|
|
50
|
+
"setup:hooks": "bun scripts/setup-hooks.ts"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@bufbuild/protobuf": "^2.12.0",
|
|
@@ -64,7 +66,17 @@
|
|
|
64
66
|
"llm",
|
|
65
67
|
"ollama",
|
|
66
68
|
"anthropic",
|
|
67
|
-
"responses-api"
|
|
69
|
+
"responses-api",
|
|
70
|
+
"claude-code",
|
|
71
|
+
"claude",
|
|
72
|
+
"codex-cli",
|
|
73
|
+
"gemini",
|
|
74
|
+
"grok",
|
|
75
|
+
"deepseek",
|
|
76
|
+
"chatgpt",
|
|
77
|
+
"llm-proxy",
|
|
78
|
+
"ai-gateway",
|
|
79
|
+
"openrouter"
|
|
68
80
|
],
|
|
69
81
|
"repository": {
|
|
70
82
|
"type": "git",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { shellInvocation } from "../../lib/win-exec";
|
|
2
3
|
import { create } from "@bufbuild/protobuf";
|
|
3
4
|
import {
|
|
4
5
|
ComputerUseErrorSchema,
|
|
@@ -25,7 +26,7 @@ const DEFAULT_DESKTOP_TIMEOUT_MS = 30_000;
|
|
|
25
26
|
* command receives the request as JSON on stdin and must print a JSON result on stdout.
|
|
26
27
|
*/
|
|
27
28
|
export interface DesktopExecutorConfig {
|
|
28
|
-
/** Command (run via
|
|
29
|
+
/** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */
|
|
29
30
|
computerUseCommand?: string;
|
|
30
31
|
/** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */
|
|
31
32
|
recordScreenCommand?: string;
|
|
@@ -121,14 +122,20 @@ function recordScreenFailure(error: string): RecordScreenResult {
|
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Spawn `command` via the platform shell (sh -c on POSIX, cmd.exe /d /s /c on win32 —
|
|
127
|
+
* the configured command is platform-native shell syntax; devlog
|
|
128
|
+
* 260715_cross_platform_audit/020), write `payload` as JSON to stdin, return parsed stdout JSON.
|
|
129
|
+
*/
|
|
125
130
|
function runExternalJson(command: string, payload: unknown, config: DesktopExecutorConfig): Promise<unknown> {
|
|
126
131
|
const timeoutMs = config.timeoutMs ?? DEFAULT_DESKTOP_TIMEOUT_MS;
|
|
127
132
|
return new Promise((resolve, reject) => {
|
|
128
|
-
const
|
|
133
|
+
const inv = shellInvocation(command);
|
|
134
|
+
const child = spawn(inv.file, inv.args, {
|
|
129
135
|
cwd: config.cwd,
|
|
130
136
|
env: config.env ? { ...process.env, ...config.env } : process.env,
|
|
131
137
|
stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
...inv.options,
|
|
132
139
|
});
|
|
133
140
|
let stdout = "";
|
|
134
141
|
let stderr = "";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getConfigDir } from "../config";
|
|
5
|
+
import type { OcxProviderConfig, OcxParsedRequest } from "../types";
|
|
6
|
+
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
7
|
+
import type { ProviderAdapter, AdapterRequest } from "./base";
|
|
8
|
+
|
|
9
|
+
const BOOTSTRAP_URL = "https://api.xiaomimimo.com/api/free-ai/bootstrap";
|
|
10
|
+
export const MIMO_CHAT_URL = "https://api.xiaomimimo.com/api/free-ai/openai/chat";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Anti-abuse gate: the free chat endpoint returns 403 "Illegal access" unless
|
|
14
|
+
* a system message contains this exact string as a substring.
|
|
15
|
+
*/
|
|
16
|
+
export const MIMO_SYSTEM_MARKER =
|
|
17
|
+
"You are MiMoCode, an interactive CLI tool that helps users with software engineering tasks.";
|
|
18
|
+
|
|
19
|
+
// Chrome-like User-Agent required by the upstream anti-abuse gate.
|
|
20
|
+
const USER_AGENTS = [
|
|
21
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
22
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
23
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const JWT_FALLBACK_TTL_MS = 3_000_000; // 50 min
|
|
27
|
+
const JWT_EXPIRY_BUFFER_MS = 300_000; // 5 min early refresh
|
|
28
|
+
const BOOTSTRAP_TIMEOUT_MS = 15_000;
|
|
29
|
+
|
|
30
|
+
// In-process JWT cache -- survives across requests, reset on restart.
|
|
31
|
+
let cachedJwt: string | null = null;
|
|
32
|
+
let jwtExpiresAt = 0;
|
|
33
|
+
// Single-flight guard: concurrent first requests share one bootstrap.
|
|
34
|
+
let inFlightJwt: Promise<string> | null = null;
|
|
35
|
+
|
|
36
|
+
function randomUserAgent(): string {
|
|
37
|
+
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]!;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Anonymous per-install client id for the bootstrap `client` field. A random UUID
|
|
42
|
+
* persisted under the config dir (OPENCODEX_HOME-aware) — deliberately NOT derived
|
|
43
|
+
* from machine attributes (hostname/username/CPU), which would be a stable
|
|
44
|
+
* pseudonymous device fingerprint. Delete the file to rotate the id.
|
|
45
|
+
*/
|
|
46
|
+
let cachedClientId: string | null = null;
|
|
47
|
+
export function getMimoClientId(): string {
|
|
48
|
+
if (cachedClientId) return cachedClientId;
|
|
49
|
+
const dir = getConfigDir();
|
|
50
|
+
const file = join(dir, "mimo-client-id");
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(file)) {
|
|
53
|
+
const stored = readFileSync(file, "utf8").trim();
|
|
54
|
+
if (/^[0-9a-f-]{36}$/i.test(stored)) {
|
|
55
|
+
cachedClientId = stored;
|
|
56
|
+
return stored;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch { /* fall through to regenerate */ }
|
|
60
|
+
const fresh = randomUUID();
|
|
61
|
+
try {
|
|
62
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
63
|
+
writeFileSync(file, `${fresh}\n`, "utf8");
|
|
64
|
+
} catch { /* persist best-effort; still usable for this process */ }
|
|
65
|
+
cachedClientId = fresh;
|
|
66
|
+
return fresh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Test hook: clear the in-process client-id cache (file state is the test's concern). */
|
|
70
|
+
export function resetMimoClientIdCache(): void {
|
|
71
|
+
cachedClientId = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseJwtExp(jwt: string): number {
|
|
75
|
+
try {
|
|
76
|
+
const parts = jwt.split(".");
|
|
77
|
+
if (parts.length < 2) return 0;
|
|
78
|
+
const payload = JSON.parse(Buffer.from(parts[1]!, "base64").toString()) as { exp?: number };
|
|
79
|
+
if (payload.exp) return payload.exp * 1000;
|
|
80
|
+
} catch { /* ignore */ }
|
|
81
|
+
return Date.now() + JWT_FALLBACK_TTL_MS;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resetMimoJwtCache(): void {
|
|
85
|
+
cachedJwt = null;
|
|
86
|
+
jwtExpiresAt = 0;
|
|
87
|
+
inFlightJwt = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function fetchJwt(signal?: AbortSignal): Promise<string> {
|
|
91
|
+
// Bounded bootstrap: request-abort propagates, and a stalled bootstrap can never
|
|
92
|
+
// hang past BOOTSTRAP_TIMEOUT_MS.
|
|
93
|
+
const timeout = AbortSignal.timeout(BOOTSTRAP_TIMEOUT_MS);
|
|
94
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
95
|
+
const response = await fetch(BOOTSTRAP_URL, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
"User-Agent": randomUserAgent(),
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({ client: getMimoClientId() }),
|
|
102
|
+
signal: combined,
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
try { await response.body?.cancel(); } catch { /* already consumed */ }
|
|
106
|
+
throw new Error(`MiMo bootstrap failed: ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
const data = await response.json() as { jwt?: string };
|
|
109
|
+
if (!data.jwt) throw new Error("MiMo bootstrap returned no JWT");
|
|
110
|
+
return data.jwt;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getMimoJwt(signal?: AbortSignal): Promise<string> {
|
|
114
|
+
if (cachedJwt && Date.now() < jwtExpiresAt - JWT_EXPIRY_BUFFER_MS) {
|
|
115
|
+
return cachedJwt;
|
|
116
|
+
}
|
|
117
|
+
// Single-flight: concurrent callers await the same bootstrap instead of issuing
|
|
118
|
+
// parallel bootstraps.
|
|
119
|
+
if (!inFlightJwt) {
|
|
120
|
+
inFlightJwt = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const jwt = await fetchJwt(signal);
|
|
123
|
+
cachedJwt = jwt;
|
|
124
|
+
jwtExpiresAt = parseJwtExp(jwt);
|
|
125
|
+
return jwt;
|
|
126
|
+
} finally {
|
|
127
|
+
inFlightJwt = null;
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
}
|
|
131
|
+
return inFlightJwt;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Idempotently prepend the MiMo anti-abuse system marker if it is not already present.
|
|
136
|
+
* The marker must appear in a system message; we prepend one if the request has none with it.
|
|
137
|
+
*/
|
|
138
|
+
export function injectMimoSystemMarker(body: unknown): unknown {
|
|
139
|
+
if (!body || typeof body !== "object") return body;
|
|
140
|
+
const parsed = body as Record<string, unknown>;
|
|
141
|
+
const messages = parsed["messages"];
|
|
142
|
+
if (!Array.isArray(messages)) return body;
|
|
143
|
+
const hasMarker = messages.some(
|
|
144
|
+
(m): m is { role: string; content: string } =>
|
|
145
|
+
m !== null &&
|
|
146
|
+
typeof m === "object" &&
|
|
147
|
+
(m as Record<string, unknown>)["role"] === "system" &&
|
|
148
|
+
typeof (m as Record<string, unknown>)["content"] === "string" &&
|
|
149
|
+
((m as Record<string, unknown>)["content"] as string).includes(MIMO_SYSTEM_MARKER),
|
|
150
|
+
);
|
|
151
|
+
if (hasMarker) return body;
|
|
152
|
+
return { ...parsed, messages: [{ role: "system", content: MIMO_SYSTEM_MARKER }, ...messages] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates the MiMo Free adapter. Wraps openai-chat's request builder to inject:
|
|
157
|
+
* 1. JWT from the bootstrap endpoint (cached, auto-refreshed).
|
|
158
|
+
* 2. Anti-abuse system marker in the request body.
|
|
159
|
+
* 3. Required headers (User-Agent, X-Mimo-Source, x-session-affinity).
|
|
160
|
+
* On 401/403, flushes the JWT cache and retries once via fetchResponse.
|
|
161
|
+
*/
|
|
162
|
+
export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
163
|
+
const base = createOpenAIChatAdapter(provider);
|
|
164
|
+
// Per-adapter session-affinity id (random, per process instance).
|
|
165
|
+
const sessionId = `ses_${Math.random().toString(36).slice(2, 26)}`;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
name: "mimo-free",
|
|
170
|
+
|
|
171
|
+
async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
|
|
172
|
+
const jwt = await getMimoJwt();
|
|
173
|
+
|
|
174
|
+
// Let the base adapter build the wire body (handles reasoning, tools, etc.)
|
|
175
|
+
// but override the URL and headers after.
|
|
176
|
+
const baseReq = base.buildRequest(parsed) as AdapterRequest;
|
|
177
|
+
const baseBody = JSON.parse(baseReq.body as string) as unknown;
|
|
178
|
+
const markedBody = injectMimoSystemMarker(baseBody);
|
|
179
|
+
|
|
180
|
+
const headers: Record<string, string> = {
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
"Authorization": `Bearer ${jwt}`,
|
|
183
|
+
"X-Mimo-Source": "mimocode-cli-free",
|
|
184
|
+
"User-Agent": randomUserAgent(),
|
|
185
|
+
"x-session-affinity": sessionId,
|
|
186
|
+
"Accept": parsed.stream ? "text/event-stream" : "application/json",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
url: MIMO_CHAT_URL,
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers,
|
|
193
|
+
body: JSON.stringify(markedBody),
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async fetchResponse(request: AdapterRequest, ctx): Promise<Response> {
|
|
198
|
+
const response = await fetch(request.url, {
|
|
199
|
+
method: request.method,
|
|
200
|
+
headers: request.headers as Record<string, string>,
|
|
201
|
+
body: request.body,
|
|
202
|
+
signal: ctx?.abortSignal,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Retry predicate: 401 (expired/invalid JWT) retries ONCE with a fresh token.
|
|
206
|
+
// 403 is NOT retried — Xiaomi uses it for anti-abuse "Illegal access" and there is
|
|
207
|
+
// no documented token-expiry signature that would mark a 403 as retryable.
|
|
208
|
+
if (response.status === 401) {
|
|
209
|
+
// Drain the first response body before issuing the retry.
|
|
210
|
+
try { await response.body?.cancel(); } catch { /* already consumed */ }
|
|
211
|
+
resetMimoJwtCache();
|
|
212
|
+
const freshJwt = await getMimoJwt(ctx?.abortSignal);
|
|
213
|
+
const retryHeaders = {
|
|
214
|
+
...(request.headers as Record<string, string>),
|
|
215
|
+
"Authorization": `Bearer ${freshJwt}`,
|
|
216
|
+
};
|
|
217
|
+
return fetch(request.url, {
|
|
218
|
+
method: request.method,
|
|
219
|
+
headers: retryHeaders,
|
|
220
|
+
body: request.body,
|
|
221
|
+
signal: ctx?.abortSignal,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return response;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -172,6 +172,88 @@ function safeToolName(name: string | undefined): string {
|
|
|
172
172
|
return sanitized;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
|
|
176
|
+
const ZEN_DROPPED_SCHEMA_KEYS = new Set(["encrypted"]);
|
|
177
|
+
|
|
178
|
+
function sanitizeZenSchemaMap(value: unknown): unknown {
|
|
179
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeZenToolParameters(value);
|
|
180
|
+
const out: Record<string, unknown> = {};
|
|
181
|
+
for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
|
|
182
|
+
out[name] = sanitizeZenToolParameters(child);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sanitizeZenToolParameters(value: unknown): unknown {
|
|
188
|
+
if (Array.isArray(value)) return value.map(sanitizeZenToolParameters);
|
|
189
|
+
if (!value || typeof value !== "object") return value;
|
|
190
|
+
const input = value as Record<string, unknown>;
|
|
191
|
+
const out: Record<string, unknown> = {};
|
|
192
|
+
for (const [key, child] of Object.entries(input)) {
|
|
193
|
+
if (ZEN_DROPPED_SCHEMA_KEYS.has(key)) continue;
|
|
194
|
+
if (key === "required" && Array.isArray(child) && child.length === 0) continue;
|
|
195
|
+
if (key === "type" && Array.isArray(child)) {
|
|
196
|
+
const nonNull = child.filter(entry => entry !== "null");
|
|
197
|
+
if (child.includes("null")) out.nullable = true;
|
|
198
|
+
if (nonNull.length > 0) out.type = nonNull[0];
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
out[key] = ZEN_SCHEMA_MAP_KEYS.has(key) ? sanitizeZenSchemaMap(child) : sanitizeZenToolParameters(child);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ensureZenRootObjectSchema(schema: unknown): Record<string, unknown> {
|
|
207
|
+
const obj = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
208
|
+
? schema as Record<string, unknown>
|
|
209
|
+
: {};
|
|
210
|
+
const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
|
|
211
|
+
const hasComposition = compositionKeys.some(key => Array.isArray(obj[key]));
|
|
212
|
+
const rootType = obj.type;
|
|
213
|
+
const rootObjectType = rootType === "object" || (Array.isArray(rootType) && rootType.includes("object"));
|
|
214
|
+
if (!hasComposition) {
|
|
215
|
+
const base = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
216
|
+
return rootObjectType && base.type === "object" ? base : { ...base, type: "object" };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const props: Record<string, unknown> = {};
|
|
220
|
+
const required = new Set<string>();
|
|
221
|
+
if (obj.properties && typeof obj.properties === "object") {
|
|
222
|
+
Object.assign(props, sanitizeZenSchemaMap(obj.properties) as Record<string, unknown>);
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(obj.required)) {
|
|
225
|
+
for (const entry of obj.required) if (typeof entry === "string") required.add(entry);
|
|
226
|
+
}
|
|
227
|
+
for (const key of compositionKeys) {
|
|
228
|
+
const variants = obj[key];
|
|
229
|
+
if (!Array.isArray(variants)) continue;
|
|
230
|
+
const mergeRequired = key === "allOf";
|
|
231
|
+
for (const variant of variants) {
|
|
232
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
233
|
+
const rec = variant as Record<string, unknown>;
|
|
234
|
+
if (rec.properties && typeof rec.properties === "object") {
|
|
235
|
+
Object.assign(props, sanitizeZenSchemaMap(rec.properties) as Record<string, unknown>);
|
|
236
|
+
}
|
|
237
|
+
if (mergeRequired && Array.isArray(rec.required)) {
|
|
238
|
+
for (const entry of rec.required) if (typeof entry === "string") required.add(entry);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const merged = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
244
|
+
delete merged.oneOf;
|
|
245
|
+
delete merged.anyOf;
|
|
246
|
+
delete merged.allOf;
|
|
247
|
+
merged.type = "object";
|
|
248
|
+
if (Object.keys(props).length > 0) merged.properties = props;
|
|
249
|
+
if (required.size > 0) merged.required = [...required];
|
|
250
|
+
return merged;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
254
|
+
return provider.baseUrl.replace(/\/+$/, "") === "https://opencode.ai/zen/v1";
|
|
255
|
+
}
|
|
256
|
+
|
|
175
257
|
const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
|
|
176
258
|
|
|
177
259
|
function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
@@ -240,6 +322,23 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
|
|
|
240
322
|
return formatted.length > 0 ? formatted : undefined;
|
|
241
323
|
}
|
|
242
324
|
|
|
325
|
+
function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
326
|
+
const base = toolsToChatFormat(parsed, provider);
|
|
327
|
+
if (!base || !shouldSanitizeZenToolParameters(provider)) return base;
|
|
328
|
+
return base.map(tool => {
|
|
329
|
+
if (!tool || typeof tool !== "object") return tool;
|
|
330
|
+
const functionDef = (tool as { function?: Record<string, unknown> }).function;
|
|
331
|
+
if (!functionDef || typeof functionDef !== "object") return tool;
|
|
332
|
+
return {
|
|
333
|
+
...tool,
|
|
334
|
+
function: {
|
|
335
|
+
...functionDef,
|
|
336
|
+
parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
243
342
|
function toolChoiceToChatFormat(tc: OcxParsedRequest["options"]["toolChoice"], tools: OcxParsedRequest["context"]["tools"]): unknown {
|
|
244
343
|
if (!tc) return undefined;
|
|
245
344
|
if (isAllowedToolChoice(tc)) return tc.mode === "required" ? "required" : "auto";
|
|
@@ -287,7 +386,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
287
386
|
}
|
|
288
387
|
|
|
289
388
|
const messages = messagesToChatFormat(parsed, provider);
|
|
290
|
-
const tools =
|
|
389
|
+
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
291
390
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
|
|
292
391
|
|
|
293
392
|
const body: Record<string, unknown> = {
|
|
@@ -346,6 +445,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
346
445
|
|
|
347
446
|
const url = `${provider.baseUrl}/chat/completions`;
|
|
348
447
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
448
|
+
// Precedence preserved from pre-#128 behavior: apiKey Authorization first, then
|
|
449
|
+
// provider.headers may override (user/registry-configured headers win). Registry
|
|
450
|
+
// staticHeaders (e.g. opencode-free x-opencode-client) flow in via derive.ts and
|
|
451
|
+
// never carry Authorization, so keyless providers are unaffected.
|
|
349
452
|
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
350
453
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
351
454
|
|
package/src/claude/inbound.ts
CHANGED
|
@@ -180,7 +180,9 @@ function maybeElideSkillText(text: string, names: readonly string[]): string {
|
|
|
180
180
|
if (!text.startsWith(SKILL_TEXT_MARKER)) return text;
|
|
181
181
|
const firstLineEnd = text.indexOf("\n");
|
|
182
182
|
const dir = text.slice(SKILL_TEXT_MARKER.length, firstLineEnd === -1 ? text.length : firstLineEnd).trim();
|
|
183
|
-
|
|
183
|
+
// Windows clients send `C:\Users\...\claude-api`; normalize separators before
|
|
184
|
+
// basenaming (repo precedent: src/codex/inject.ts isOpencodexCatalogPath).
|
|
185
|
+
const base = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
|
|
184
186
|
if (!names.includes(base)) return text;
|
|
185
187
|
return `[opencodex] '${base}' skill document bundle (${text.length} chars) elided for routed models `
|
|
186
188
|
+ "(claudeCode.blockedSkills). The skill is loaded; answer from general knowledge instead of citing the bundle.";
|
package/src/cli/claude.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { loadConfig } from "../config";
|
|
|
11
11
|
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
12
12
|
import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
13
13
|
import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
|
|
14
|
+
import { commandInvocation } from "../lib/win-exec";
|
|
14
15
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
15
16
|
import type { OcxConfig } from "../types";
|
|
16
17
|
|
|
@@ -141,6 +142,21 @@ async function ensureProxyForClaude(): Promise<number | null> {
|
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
const CLAUDE_INSTALL_HINT = "❌ `claude` CLI not found. Install it first: npm install -g @anthropic-ai/claude-code";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* cmd.exe reports command-not-found as exit 9009 (the win32 launcher routes `.cmd`
|
|
149
|
+
* shims through cmd.exe, so ENOENT never fires there). Signal exits are not hints.
|
|
150
|
+
* Devlog 260715_cross_platform_audit/020.
|
|
151
|
+
*/
|
|
152
|
+
export function claudeNotFoundHint(
|
|
153
|
+
code: number | null,
|
|
154
|
+
signal: NodeJS.Signals | null,
|
|
155
|
+
platform: NodeJS.Platform = process.platform,
|
|
156
|
+
): string | null {
|
|
157
|
+
return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
export async function cmdClaude(args: string[]): Promise<number> {
|
|
145
161
|
const config = loadConfig();
|
|
146
162
|
if (config.claudeCode?.enabled === false) {
|
|
@@ -176,16 +192,19 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
176
192
|
console.error(`⚠ Claude agent definitions could not be synced: ${message}`);
|
|
177
193
|
}
|
|
178
194
|
return await new Promise<number>(resolve => {
|
|
179
|
-
const
|
|
195
|
+
const inv = commandInvocation("claude", args);
|
|
196
|
+
const child = spawn(inv.file, inv.args, { stdio: "inherit", env: env as NodeJS.ProcessEnv, ...inv.options });
|
|
180
197
|
child.on("error", (err: NodeJS.ErrnoException) => {
|
|
181
198
|
if (err.code === "ENOENT") {
|
|
182
|
-
console.error(
|
|
199
|
+
console.error(CLAUDE_INSTALL_HINT);
|
|
183
200
|
} else {
|
|
184
201
|
console.error(`❌ Failed to launch claude: ${err.message}`);
|
|
185
202
|
}
|
|
186
203
|
resolve(1);
|
|
187
204
|
});
|
|
188
205
|
child.on("exit", (code, signal) => {
|
|
206
|
+
const hint = claudeNotFoundHint(code, signal);
|
|
207
|
+
if (hint) console.error(hint);
|
|
189
208
|
resolve(signal ? 1 : code ?? 0);
|
|
190
209
|
});
|
|
191
210
|
});
|
package/src/cli/v2.ts
CHANGED
|
@@ -13,22 +13,38 @@
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { getLogicalMaxThreads, hasAgentsMaxThreads, isMultiAgentV2Enabled, transitionMultiAgentV2 } from "../codex/features";
|
|
15
15
|
|
|
16
|
+
import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
|
|
16
17
|
import { loadConfig, saveConfig } from "../config";
|
|
17
18
|
|
|
18
19
|
export interface V2CliDeps {
|
|
19
|
-
execFile?: (file: string, args: string[]) => void;
|
|
20
|
+
execFile?: (file: string, args: string[], options?: SpawnInvocation["options"]) => void;
|
|
20
21
|
isEnabled?: typeof isMultiAgentV2Enabled;
|
|
21
22
|
hasMaxThreads?: typeof hasAgentsMaxThreads;
|
|
22
23
|
sync?: (port?: number) => Promise<unknown>;
|
|
23
24
|
log?: Pick<Console, "log" | "error">;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Shared invocation for `codex features enable|disable multi_agent_v2` — the single
|
|
29
|
+
* source of truth for the CLI and the management API fallback. Windows npm installs
|
|
30
|
+
* expose `codex` as a `.cmd` shim, which needs the win-exec launcher
|
|
31
|
+
* (devlog 260715_cross_platform_audit/020).
|
|
32
|
+
*/
|
|
33
|
+
export function codexFeaturesInvocation(
|
|
34
|
+
action: "enable" | "disable",
|
|
35
|
+
platform: NodeJS.Platform = process.platform,
|
|
36
|
+
deps: Parameters<typeof commandInvocation>[3] = {},
|
|
37
|
+
): SpawnInvocation {
|
|
38
|
+
const command = (deps.env ?? process.env).CODEX_CLI_PATH?.trim() || "codex";
|
|
39
|
+
return commandInvocation(command, ["features", action, "multi_agent_v2"], platform, deps);
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
|
|
27
|
-
const exec = deps.execFile ?? ((file: string, args: string[]) => {
|
|
28
|
-
execFileSync(file, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true });
|
|
43
|
+
const exec = deps.execFile ?? ((file: string, args: string[], options?: SpawnInvocation["options"]) => {
|
|
44
|
+
execFileSync(file, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true, ...options });
|
|
29
45
|
});
|
|
30
|
-
const
|
|
31
|
-
exec(
|
|
46
|
+
const inv = codexFeaturesInvocation(action);
|
|
47
|
+
exec(inv.file, inv.args, inv.options);
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
export function v2StatusLine(enabled: boolean): string {
|