@bitkyc08/opencodex 2.1.1 → 2.1.5

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.
@@ -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{padding:5px}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.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-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)}.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}.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}.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{justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;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}}
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{padding:5px}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.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-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)}.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}.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{justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;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}}
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DgCnBxqJ.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-cEIM1XWY.css">
19
+ <script type="module" crossorigin src="/assets/index-DB2i6w5f.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-dCS-lwCM.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.1.1",
3
+ "version": "2.1.5",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -12,6 +12,7 @@ import type {
12
12
  OcxToolCall,
13
13
  OcxUsage,
14
14
  } from "../types";
15
+ import { namespacedToolName } from "../types";
15
16
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
16
17
  import { parseDataUrl } from "./image";
17
18
 
@@ -85,7 +86,8 @@ function messagesToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean):
85
86
  content.push({ type: "thinking", thinking: t.thinking, ...(t.signature ? { signature: t.signature } : {}) });
86
87
  } else if (part.type === "toolCall") {
87
88
  const tc = part as OcxToolCall;
88
- content.push({ type: "tool_use", id: tc.id, name: isOAuth ? applyClaudeToolPrefix(tc.name) : tc.name, input: tc.arguments });
89
+ const flatName = namespacedToolName(tc.namespace, tc.name);
90
+ content.push({ type: "tool_use", id: tc.id, name: isOAuth ? applyClaudeToolPrefix(flatName) : flatName, input: tc.arguments });
89
91
  }
90
92
  }
91
93
  messages.push({ role: "assistant", content });
@@ -116,7 +118,7 @@ function messagesToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean):
116
118
  function toolsToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean): unknown[] | undefined {
117
119
  if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
118
120
  return parsed.context.tools.map(t => ({
119
- name: isOAuth ? applyClaudeToolPrefix(t.name) : t.name,
121
+ name: isOAuth ? applyClaudeToolPrefix(namespacedToolName(t.namespace, t.name)) : namespacedToolName(t.namespace, t.name),
120
122
  description: t.description,
121
123
  input_schema: t.parameters,
122
124
  }));
@@ -175,7 +177,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
175
177
  else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name: isOAuth ? applyClaudeToolPrefix(tc.name) : tc.name };
176
178
  }
177
179
 
178
- const url = `${provider.baseUrl}/v1/messages`;
180
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
181
+ const url = `${base}/v1/messages`;
179
182
  const headers: Record<string, string> = {
180
183
  "Content-Type": "application/json",
181
184
  "anthropic-version": "2023-06-01",
@@ -19,13 +19,13 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
19
19
  headers["api-key"] = provider.apiKey;
20
20
  delete headers["Authorization"];
21
21
  }
22
- const apiVersion = (provider.headers?.["api-version"]) ?? "2025-04-01-preview";
23
- const separator = request.url.includes("?") ? "&" : "?";
24
- return {
25
- ...request,
26
- url: `${request.url}${separator}api-version=${apiVersion}`,
27
- headers,
28
- };
22
+ let url = request.url;
23
+ if (!url.includes("/v1/")) {
24
+ const apiVersion = (provider.headers?.["api-version"]) ?? "2025-04-01-preview";
25
+ const separator = url.includes("?") ? "&" : "?";
26
+ url = `${url}${separator}api-version=${apiVersion}`;
27
+ }
28
+ return { ...request, url, headers };
29
29
  },
30
30
  };
31
31
  }
@@ -10,6 +10,7 @@ import type {
10
10
  OcxToolCall,
11
11
  OcxUsage,
12
12
  } from "../types";
13
+ import { namespacedToolName } from "../types";
13
14
  import { contentPartsToText, parseDataUrl } from "./image";
14
15
 
15
16
  function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
@@ -46,7 +47,7 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
46
47
  if (p.type === "text") parts.push({ text: (p as OcxTextContent).text });
47
48
  else if (p.type === "toolCall") {
48
49
  const tc = p as OcxToolCall;
49
- parts.push({ functionCall: { name: tc.name, args: tc.arguments } });
50
+ parts.push({ functionCall: { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments } });
50
51
  }
51
52
  }
52
53
  contents.push({ role: "model", parts });
@@ -55,7 +56,7 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
55
56
  case "toolResult": {
56
57
  contents.push({
57
58
  role: "user",
58
- parts: [{ functionResponse: { name: msg.toolName, response: { result: contentPartsToText(msg.content) } } }],
59
+ parts: [{ functionResponse: { name: namespacedToolName(msg.toolNamespace, msg.toolName), response: { result: contentPartsToText(msg.content) } } }],
59
60
  });
60
61
  break;
61
62
  }
@@ -69,7 +70,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
69
70
  if (!parsed.context.tools?.length) return undefined;
70
71
  return [{
71
72
  functionDeclarations: parsed.context.tools.map(t => ({
72
- name: t.name,
73
+ name: namespacedToolName(t.namespace, t.name),
73
74
  description: t.description,
74
75
  parameters: t.parameters,
75
76
  })),
@@ -160,7 +160,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
160
160
  stream: parsed.stream,
161
161
  };
162
162
  if (tools) body.tools = tools;
163
- if (toolChoice !== undefined) {
163
+ if (tools && toolChoice !== undefined) {
164
164
  body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
165
165
  ? (toolChoice === "none" ? "none" : "auto")
166
166
  : toolChoice;
@@ -182,6 +182,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
182
182
  body.frequency_penalty = parsed.options.frequencyPenalty;
183
183
  }
184
184
 
185
+ if (tools) body.parallel_tool_calls = false;
185
186
  if (parsed.stream) {
186
187
  body.stream_options = { include_usage: true };
187
188
  }
@@ -60,7 +60,8 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
60
60
  if (v) headers[h] = v; // …so forwarded auth always wins.
61
61
  }
62
62
  } else {
63
- url = `${provider.baseUrl}/v1/responses`;
63
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
64
+ url = `${base}/v1/responses`;
64
65
  if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
65
66
  if (provider.headers) Object.assign(headers, provider.headers);
66
67
  }
package/src/bridge.ts CHANGED
@@ -43,7 +43,7 @@ export function bridgeToResponsesSSE(
43
43
  toolSearchToolNames?: Set<string>,
44
44
  onCancel?: () => void,
45
45
  heartbeatMs = 2_000,
46
- options?: { responseId?: string },
46
+ options?: { responseId?: string; stallTimeoutSec?: number; hideThinkingSummary?: boolean },
47
47
  ): ReadableStream<Uint8Array> {
48
48
  // Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
49
49
  // function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
@@ -104,7 +104,8 @@ export function bridgeToResponsesSSE(
104
104
  // whenever a real event was emitted since the last tick, so it only fires on a genuine stall.
105
105
  const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
106
106
  let stallTicks = 0;
107
- const maxStallTicks = 150; // 5 min at default 2 s interval
107
+ const stallSec = Math.max(1, options?.stallTimeoutSec ?? 90);
108
+ const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
108
109
  beat = setInterval(() => {
109
110
  if (closed) return;
110
111
  if (activity) { activity = false; stallTicks = 0; return; }
@@ -230,6 +231,8 @@ export function bridgeToResponsesSSE(
230
231
 
231
232
  try {
232
233
  for await (const event of events) {
234
+ activity = true;
235
+ stallTicks = 0;
233
236
  switch (event.type) {
234
237
  case "text_delta": {
235
238
  if (currentReasoning) closeCurrentReasoning();
@@ -256,6 +259,7 @@ export function bridgeToResponsesSSE(
256
259
  break;
257
260
  }
258
261
  case "thinking_delta": {
262
+ if (options?.hideThinkingSummary) break;
259
263
  if (currentMsg) closeCurrentMessage();
260
264
  if (currentRawReasoning) closeCurrentRawReasoning();
261
265
  if (currentToolCall) closeCurrentToolCall();
@@ -406,46 +410,143 @@ export function bridgeToResponsesSSE(
406
410
  export function buildResponseJSON(
407
411
  events: AdapterEvent[],
408
412
  modelId: string,
413
+ options?: {
414
+ hideThinkingSummary?: boolean;
415
+ toolNsMap?: Map<string, { namespace: string; name: string }>;
416
+ freeformToolNames?: Set<string>;
417
+ toolSearchToolNames?: Set<string>;
418
+ },
409
419
  ): Record<string, unknown> {
410
420
  const responseId = `resp_${uuid()}`;
411
421
  const output: OutputItem[] = [];
412
- let text = "";
413
- let summaryReasoning = "";
414
- let rawReasoning = "";
415
422
  let usage: OcxUsage | undefined;
423
+ let errorMessage: string | undefined;
416
424
 
417
- for (const e of events) {
418
- if (e.type === "text_delta") text += e.text;
419
- if (e.type === "thinking_delta") summaryReasoning += e.thinking;
420
- if (e.type === "reasoning_raw_delta") rawReasoning += e.text;
421
- if (e.type === "done") usage = e.usage;
422
- }
425
+ let currentText = "";
426
+ let currentSummaryReasoning = "";
427
+ let currentRawReasoning = "";
428
+ let currentToolCallId = "";
429
+ let currentToolCallName = "";
430
+ let currentToolCallArgs = "";
431
+
432
+ const freeformInput = (args: string): string => {
433
+ try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
434
+ return args;
435
+ };
436
+ const parseArgsObj = (args: string): Record<string, unknown> => {
437
+ try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
438
+ };
423
439
 
424
- if (rawReasoning) {
440
+ const flushText = () => {
441
+ if (!currentText) return;
425
442
  output.push({
426
- type: "reasoning", id: `rs_${uuid()}`, summary: [],
427
- content: [{ type: "reasoning_text", text: rawReasoning }],
443
+ type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
444
+ content: [{ type: "output_text", text: currentText, annotations: [] }],
428
445
  });
429
- }
430
-
431
- if (summaryReasoning) {
446
+ currentText = "";
447
+ };
448
+ const flushSummaryReasoning = () => {
449
+ if (!currentSummaryReasoning || options?.hideThinkingSummary) { currentSummaryReasoning = ""; return; }
432
450
  output.push({
433
451
  type: "reasoning", id: `rs_${uuid()}`,
434
- summary: [{ type: "summary_text", text: summaryReasoning }],
452
+ summary: [{ type: "summary_text", text: currentSummaryReasoning }],
435
453
  });
436
- }
437
-
438
- if (text) {
454
+ currentSummaryReasoning = "";
455
+ };
456
+ const flushRawReasoning = () => {
457
+ if (!currentRawReasoning) return;
439
458
  output.push({
440
- type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
441
- content: [{ type: "output_text", text, annotations: [] }],
459
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
460
+ content: [{ type: "reasoning_text", text: currentRawReasoning }],
442
461
  });
462
+ currentRawReasoning = "";
463
+ };
464
+ const flushToolCall = () => {
465
+ if (!currentToolCallId) return;
466
+ const mapped = options?.toolNsMap?.get(currentToolCallName);
467
+ const realName = mapped?.name ?? currentToolCallName;
468
+ const ns = mapped?.namespace;
469
+ const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
470
+ const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
471
+ if (toolSearch) {
472
+ output.push({
473
+ type: "tool_search_call", id: `fc_${uuid()}`,
474
+ call_id: currentToolCallId, execution: "client",
475
+ arguments: parseArgsObj(currentToolCallArgs), status: "completed",
476
+ });
477
+ } else if (freeform) {
478
+ output.push({
479
+ type: "custom_tool_call", id: `fc_${uuid()}`,
480
+ call_id: currentToolCallId, name: realName,
481
+ input: freeformInput(currentToolCallArgs), status: "completed",
482
+ });
483
+ } else {
484
+ output.push({
485
+ type: "function_call", id: `fc_${uuid()}`,
486
+ call_id: currentToolCallId, name: realName,
487
+ arguments: currentToolCallArgs || "{}", status: "completed",
488
+ ...(ns ? { namespace: ns } : {}),
489
+ });
490
+ }
491
+ currentToolCallId = "";
492
+ currentToolCallName = "";
493
+ currentToolCallArgs = "";
494
+ };
495
+
496
+ for (const e of events) {
497
+ switch (e.type) {
498
+ case "text_delta":
499
+ if (currentSummaryReasoning) flushSummaryReasoning();
500
+ if (currentRawReasoning) flushRawReasoning();
501
+ if (currentToolCallId) flushToolCall();
502
+ currentText += e.text;
503
+ break;
504
+ case "thinking_delta":
505
+ if (currentText) flushText();
506
+ if (currentRawReasoning) flushRawReasoning();
507
+ if (currentToolCallId) flushToolCall();
508
+ currentSummaryReasoning += e.thinking;
509
+ break;
510
+ case "reasoning_raw_delta":
511
+ if (currentText) flushText();
512
+ if (currentSummaryReasoning) flushSummaryReasoning();
513
+ if (currentToolCallId) flushToolCall();
514
+ currentRawReasoning += e.text;
515
+ break;
516
+ case "tool_call_start":
517
+ if (currentText) flushText();
518
+ if (currentSummaryReasoning) flushSummaryReasoning();
519
+ if (currentRawReasoning) flushRawReasoning();
520
+ flushToolCall();
521
+ currentToolCallId = e.id;
522
+ currentToolCallName = e.name;
523
+ currentToolCallArgs = "";
524
+ break;
525
+ case "tool_call_delta":
526
+ currentToolCallArgs += e.arguments;
527
+ break;
528
+ case "tool_call_end":
529
+ flushToolCall();
530
+ break;
531
+ case "error":
532
+ errorMessage = e.message;
533
+ break;
534
+ case "done":
535
+ usage = e.usage;
536
+ break;
537
+ }
443
538
  }
539
+ flushText();
540
+ flushSummaryReasoning();
541
+ flushRawReasoning();
542
+ flushToolCall();
444
543
 
445
544
  return {
446
545
  id: responseId, object: "response",
447
546
  created_at: Math.floor(Date.now() / 1000),
448
- status: "completed", model: modelId, output,
547
+ status: errorMessage ? "failed" : "completed",
548
+ model: modelId, output,
549
+ ...(errorMessage ? { error: { message: errorMessage } } : {}),
449
550
  usage: responsesUsage(usage),
450
551
  };
451
552
  }
package/src/cli.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import { execFileSync, spawn } from "node:child_process";
3
+ import { rmSync } from "node:fs";
3
4
  import { restoreNativeCodex } from "./codex-inject";
4
- import { loadConfig, readPid, removePid, writePid } from "./config";
5
- import { serviceCommand, stopServiceIfInstalled } from "./service";
5
+ import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
6
+ import { findAvailablePort } from "./ports";
7
+ import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
6
8
  import { startServer } from "./server";
7
9
  import { maybeShowStarPrompt } from "./star-prompt";
8
10
 
@@ -17,9 +19,12 @@ Usage:
17
19
  ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
18
20
  ocx stop Stop the proxy AND restore native Codex (plain codex works again)
19
21
  ocx restore Restore native Codex without stopping (alias: eject)
22
+ ocx uninstall Remove service/shim/config and restore native Codex
20
23
  ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
21
24
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
25
+ ocx ensure Ensure the proxy is running and Codex config/cache are current
22
26
  ocx sync Fetch models from providers and inject into Codex config
27
+ ocx sync-cache Refresh Codex's model cache from the active catalog
23
28
  ocx status Check proxy server status
24
29
  ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
25
30
  ocx logout <provider> Remove a stored OAuth login
@@ -55,23 +60,74 @@ async function syncModelsToCodex(port?: number) {
55
60
  return result;
56
61
  }
57
62
 
58
- async function handleStart(options: { block?: boolean } = {}) {
59
- const existingPid = readPid();
60
- if (existingPid) {
61
- console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
63
+ function parsePortOption(): number | undefined {
64
+ const portIdx = args.indexOf("--port");
65
+ if (portIdx === -1) return undefined;
66
+ const value = args[portIdx + 1];
67
+ const port = value ? parseInt(value, 10) : NaN;
68
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
69
+ console.error("Invalid port number");
62
70
  process.exit(1);
63
71
  }
72
+ return port;
73
+ }
64
74
 
65
- let port: number | undefined;
66
- const portIdx = args.indexOf("--port");
67
- if (portIdx !== -1 && args[portIdx + 1]) {
68
- port = parseInt(args[portIdx + 1], 10);
69
- if (isNaN(port)) {
70
- console.error("Invalid port number");
75
+ function healthHost(hostname?: string): string {
76
+ return !hostname || hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname;
77
+ }
78
+
79
+ async function proxyHealthy(port?: number): Promise<boolean> {
80
+ const config = loadConfig();
81
+ const p = port ?? config.port ?? 10100;
82
+ try {
83
+ const res = await fetch(`http://${healthHost(config.hostname)}:${p}/healthz`, {
84
+ signal: AbortSignal.timeout(750),
85
+ });
86
+ return res.ok;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ async function waitForProxy(timeoutMs = 8_000): Promise<number | null> {
93
+ const deadline = Date.now() + timeoutMs;
94
+ while (Date.now() < deadline) {
95
+ const config = loadConfig();
96
+ const port = config.port ?? 10100;
97
+ if (await proxyHealthy(port)) return port;
98
+ await new Promise(resolve => setTimeout(resolve, 150));
99
+ }
100
+ return null;
101
+ }
102
+
103
+ async function chooseListenPort(requestedPort?: number): Promise<number> {
104
+ const config = loadConfig();
105
+ const preferred = requestedPort ?? config.port ?? 10100;
106
+ const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1");
107
+ if (selected !== preferred) {
108
+ console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
109
+ }
110
+ if (config.port !== selected) {
111
+ config.port = selected;
112
+ saveConfig(config);
113
+ }
114
+ return selected;
115
+ }
116
+
117
+ async function handleStart(options: { block?: boolean } = {}) {
118
+ const existingPid = readPid();
119
+ if (existingPid) {
120
+ const config = loadConfig();
121
+ if (await proxyHealthy(config.port)) {
122
+ console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
71
123
  process.exit(1);
72
124
  }
125
+ removePid();
73
126
  }
74
127
 
128
+ const requestedPort = parsePortOption();
129
+ const port = await chooseListenPort(requestedPort);
130
+
75
131
  const server = startServer(port);
76
132
  writePid(process.pid);
77
133
 
@@ -96,6 +152,39 @@ async function handleStart(options: { block?: boolean } = {}) {
96
152
  }
97
153
  }
98
154
 
155
+ async function handleEnsure() {
156
+ let config = loadConfig();
157
+ if (!codexAutoStartEnabled(config)) {
158
+ console.log("Codex autostart is disabled.");
159
+ return;
160
+ }
161
+ if (await proxyHealthy(config.port)) {
162
+ await syncModelsToCodex(config.port).catch(e => {
163
+ console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
164
+ });
165
+ console.log(`✅ Proxy running on port ${config.port}`);
166
+ return;
167
+ }
168
+
169
+ const child = spawn(process.execPath, [process.argv[1], "start"], {
170
+ detached: true,
171
+ stdio: "ignore",
172
+ env: { ...process.env, OCX_SERVICE: "1" },
173
+ });
174
+ child.unref();
175
+
176
+ const port = await waitForProxy();
177
+ if (!port) {
178
+ console.error("❌ Proxy did not become healthy after starting.");
179
+ process.exit(1);
180
+ }
181
+ config = loadConfig();
182
+ await syncModelsToCodex(config.port ?? port).catch(e => {
183
+ console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
184
+ });
185
+ console.log(`✅ Proxy running on port ${config.port ?? port}`);
186
+ }
187
+
99
188
  function killProxy(pid: number): void {
100
189
  if (!isProcessAlive(pid)) return;
101
190
  if (process.platform === "win32") {
@@ -154,6 +243,58 @@ function handleStop() {
154
243
  if (stopFailed) process.exit(1);
155
244
  }
156
245
 
246
+ async function handleUninstall() {
247
+ const failures: string[] = [];
248
+
249
+ const runStep = (label: string, step: () => void | boolean) => {
250
+ try {
251
+ const changed = step();
252
+ if (changed === false) console.log(`- ${label}: not installed`);
253
+ else console.log(`✅ ${label}`);
254
+ } catch (err) {
255
+ failures.push(label);
256
+ console.error(`⚠️ ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
257
+ }
258
+ };
259
+
260
+ runStep("service removed", () => {
261
+ stopServiceIfInstalled();
262
+ return uninstallServiceIfInstalled();
263
+ });
264
+
265
+ runStep("proxy stopped", () => {
266
+ const pid = readPid();
267
+ if (!pid) return false;
268
+ killProxy(pid);
269
+ removePid();
270
+ return true;
271
+ });
272
+
273
+ runStep("native Codex restored", () => {
274
+ const r = restoreNativeCodex();
275
+ if (!r.success) throw new Error(r.message);
276
+ });
277
+
278
+ try {
279
+ const { uninstallCodexShim } = await import("./codex-shim");
280
+ const r = uninstallCodexShim();
281
+ console.log(r.removed ? "✅ Codex autostart shim removed" : "- Codex autostart shim removed: not installed");
282
+ } catch (err) {
283
+ failures.push("Codex autostart shim removed");
284
+ console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
285
+ }
286
+
287
+ runStep("opencodex config removed", () => {
288
+ rmSync(getConfigDir(), { recursive: true, force: true });
289
+ });
290
+
291
+ if (failures.length > 0) {
292
+ console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
293
+ process.exit(1);
294
+ }
295
+ console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
296
+ }
297
+
157
298
  function handleStatus() {
158
299
  const pid = readPid();
159
300
  if (pid) {
@@ -182,9 +323,16 @@ switch (command) {
182
323
  console.log("Plain `codex` now runs natively (no proxy).");
183
324
  break;
184
325
  }
326
+ case "uninstall":
327
+ case "remove":
328
+ await handleUninstall();
329
+ break;
185
330
  case "status":
186
331
  handleStatus();
187
332
  break;
333
+ case "ensure":
334
+ await handleEnsure();
335
+ break;
188
336
  case "login": {
189
337
  const { handleLogin } = await import("./oauth/login-cli");
190
338
  await handleLogin(args[1]);
@@ -201,6 +349,11 @@ switch (command) {
201
349
  await syncModelsToCodex();
202
350
  break;
203
351
  }
352
+ case "sync-cache": {
353
+ const { invalidateCodexModelsCache } = await import("./codex-catalog");
354
+ invalidateCodexModelsCache();
355
+ break;
356
+ }
204
357
  case "gui": {
205
358
  const cfg = await import("./config");
206
359
  const config = cfg.loadConfig();
@@ -248,7 +401,7 @@ switch (command) {
248
401
  }
249
402
  case "update": {
250
403
  const { runUpdate } = await import("./update");
251
- runUpdate();
404
+ await runUpdate();
252
405
  break;
253
406
  }
254
407
  case "help":