@bitkyc08/opencodex 2.7.6 → 2.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);--radius:12px;--radius-sm:8px;--radius-xs:6px;--radius-pill:999px;--font:"OpenAI Sans", "Söhne", -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}: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);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background:0 0;margin:0;font-size:14px;line-height:1.5;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,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:0;margin:0;font-weight:600}::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(--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{letter-spacing:0;font-size:15px;font-weight:600;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,.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item.active{font-weight:600}.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}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;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);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;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;min-height:auto;box-shadow:none;-webkit-backdrop-filter:none;background:0 0;border:none;justify-content:space-between;padding:0;font-size:13px}.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;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(--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: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}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px;font-size:.92em}.btn{border-radius:var(--radius-pill);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:8px 16px;font-size:13px;font-weight:500;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(--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{border-radius:var(--radius-pill);padding:4px 12px;font-size:12px}.btn-icon{appearance:none;font:inherit;cursor:pointer;width:28px;height:28px;color:var(--muted);border-radius:var(--radius-sm);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:background .12s,color .12s;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(--mono);white-space:pre;margin:0;padding:9px 11px;font-size:12.5px;line-height:1.55;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{flex-wrap:wrap;align-items:flex-start;gap:6px;font-size:12px;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;color:var(--muted);flex:200px;font-size:12px}.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:650}@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);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--faint)}.stat .label{color:var(--muted);align-items:center;gap:6px;margin-bottom:6px;font-size:12px;font-weight:500;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:0;font-size:20px;font-weight:600;line-height:1.15}.stat .value.mono{font-family:var(--mono);font-size:17px}.model-group-head{color:var(--text);align-items:baseline;gap:8px;margin:0 0 8px;font-size:13px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);color:var(--faint);font-size:12px;font-weight:500}.group-head{cursor:pointer;background:var(--surface);padding:10px 12px;transition:background .12s}.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);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--faint);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:0;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:0;border:1px solid #0000;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--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;appearance:none;transition:filter .12s}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{background:var(--amber-soft);width:44px;height:44px;color:var(--amber);border-radius:50%;justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);padding:8px 10px;transition:border-color .12s}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);align-items:center;gap:6px;margin-bottom:3px;font-size:12px;font-weight:600;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:11px;font-family:var(--mono);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:600}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);border-bottom:1px solid var(--border);padding:9px 12px;font-size:12px;font-weight:500}.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);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;padding:8px 11px;font-size:13px;transition:border-color .12s}.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(--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{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;cursor:pointer;border-radius:999px;padding:6px 12px;font-size:13px;transition:border-color .12s,box-shadow .12s;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{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;cursor:pointer;white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:6px 12px;font-size:13px;transition:border-color .12s,box-shadow .12s;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:12px;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;width:100%;color:var(--text);font:inherit;cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;padding:7px 12px;font-size:13px;transition:background .1s;display:block}.select-option:hover{background:var(--hover)}.select-option.active{background:var(--accent-soft);color:var(--accent);font-weight:600}.switch{border:1px solid var(--border);cursor:pointer;background:var(--raised);vertical-align:middle;appearance:none;border-radius:99px;flex-shrink:0;align-items:center;width:34px;height:20px;padding:2px;line-height:0;transition:background .15s,border-color .15s;display:inline-flex}.switch.on{background:var(--accent);border-color:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);border-radius:50%;width:14px;height:14px;transition:transform .15s,background .15s;transform:translate(0);box-shadow:0 0 0 1px #10182814,0 1px 1px #1018282e}.switch.on .knob{background:var(--accent-ink);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{margin-top:3px;font-size:13px}.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}}@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:16px;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:16px}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:14px 16px 6px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px;font-size:12px}.card-active{border-color:var(--accent-ring)}.card-right{color:var(--faint);align-items:center;gap:4px;margin-left:auto;font-size:11px;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{color:var(--muted);white-space:nowrap;font-size:12.5px;font-weight:500}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:2px 16px 12px;display:grid}.quota-row{grid-template-columns:minmax(34px,max-content) 34px minmax(34px,44px) minmax(38px,42px) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{color:var(--muted);font-size:11px;font-weight:600}.quota-val{font-size:11px;font-family:var(--mono);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;overflow:hidden}.quota-reset-day,.quota-reset-time{color:var(--muted);white-space:nowrap;font-size:11px}.quota-reset-time{font-family:var(--mono)}.bar{background:var(--raised);border-radius:3px;min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:3px;height:100%;transition:width .3s}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{background:var(--raised);border:1px solid var(--border);cursor:pointer;border-radius:10px;flex-shrink:0;width:36px;height:20px;transition:background .15s;position:relative}.toggle.on{background:var(--accent);border-color:var(--accent)}.toggle-knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:left .15s;position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{left:17px}.notice-warn{border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;font-size:12px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{color:var(--muted);margin-bottom:14px;font-size:13px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.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;white-space:nowrap;background:0 0;border:none;padding:0;font-size:11px;text-decoration:underline}.log-detail-grid{grid-template-columns:max-content minmax(0,1fr);gap:6px 14px;font-size:13px;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(--mono);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;font-size:11.5px;line-height:1.5;overflow:auto}.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{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{background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);border-radius:7px;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;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;font-size:12px;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform .15s;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;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;min-height:32px;padding:6px 8px;font-size:13px;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:12.5px}.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{flex:auto;min-width:0;height:28px;padding:4px 8px;font-size:12.5px}.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{align-items:center;gap:8px;min-width:0;font-size:13px;font-weight:600;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:6px;width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{align-items:center;gap:7px;min-width:0;font-size:13px;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{flex-wrap:wrap;grid-column:1/-1;align-items:center;gap:8px;font-size:12px;display:inline-flex}@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{border-top:1px solid var(--border-soft);flex:100%;order:3;grid-template-columns:repeat(4,minmax(0,1fr));gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:grid}.nav-item{text-align:center;text-overflow:ellipsis;white-space:nowrap;flex-direction:column;justify-content:center;gap:3px;width:auto;min-height:44px;padding:6px 4px;font-size:12px;overflow:hidden}.nav-item svg{width:16px;height:16px}.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}.lang-toggle .select-dropdown-beside{top:calc(100% + 6px);left:auto;right:0}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{margin-top:4px;font-size:20px;font-weight:600}.usage-range{border:1px solid var(--border);background:var(--surface);border-radius:999px;gap:2px;padding:2px;display:inline-flex}.usage-range-btn{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:none;border-radius:999px;padding:4px 12px}.usage-range-btn.active{background:var(--raised);color:var(--text);font-weight:600}.panel-title{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{--hm-cell:11px;--hm-gap:3px;flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{color:var(--muted);width:max-content;margin-bottom:-2px;font-size:11px;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);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;font-size:10px;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);background:var(--border);border-radius:2px}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{align-self:flex-end;align-items:center;gap:4px;font-size:12px;display:inline-flex;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);white-space:nowrap;border-radius:8px;padding:6px 10px;font-size:12px;position:fixed;transform:translate(-50%,-100%)translateY(-8px);box-shadow:0 6px 20px #00000059}.heatmap-tip-date{color:var(--text);margin-bottom:2px;font-weight:600}.heatmap-tip-val{color:var(--text);font-variant-numeric:tabular-nums}.heatmap-tip-req{font-size:11px}.usage-bar{background:var(--border);border-radius:999px;width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:999px;height:100%}.daybars{grid-template-columns:repeat(7,1fr);align-items:end;gap:10px;height:180px;padding-top:8px;display:grid}.daybar{flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%;display:flex;position:relative}.daybar-track{background:var(--border);border-radius:6px;flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:6px 6px 0 0;flex-direction:column-reverse;width:100%;min-height:2px;transition:height .2s;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{color:var(--text);font-size:12px;font-weight:600}.daybar-label{white-space:nowrap;font-size:11px}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);pointer-events:none;border-radius:8px;min-width:160px;padding:8px 10px;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);box-shadow:0 6px 20px #00000059}.daybar-tip-date{color:var(--text);white-space:nowrap;margin-bottom:6px;font-size:12px;font-weight:600}.daybar-tip-row{align-items:center;gap:8px;font-size:12px;line-height:1.7;display:flex}.daybar-tip-swatch{border-radius:2px;flex-shrink:0;width:10px;height:10px}.daybar-tip-name{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:1;max-width:160px;overflow:hidden}.daybar-tip-val{color:var(--muted);font-variant-numeric:tabular-nums}.usage-scroll{max-height:360px;overflow-y:auto}.usage-scroll thead th{z-index:1;background:var(--surface);position:sticky;top:0}
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-mEjIne-M.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-D7o1qwy-.css">
19
+ <script type="module" crossorigin src="/assets/index-Bp8dDrs5.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-C0xVu72_.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.7.6",
3
+ "version": "2.7.8",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -21,6 +21,7 @@ export const FORWARD_HEADERS = [
21
21
  "x-codex-turn-state",
22
22
  "x-codex-window-id",
23
23
  "x-oai-attestation",
24
+ "x-openai-subagent",
24
25
  "x-responsesapi-include-timing-metrics",
25
26
  ];
26
27
 
@@ -174,6 +175,46 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
174
175
  return rest;
175
176
  }
176
177
 
178
+ /**
179
+ * Hosted tool types whose server-side function names collide with the client tools Codex
180
+ * declares for the matching app skill. Codex sends BOTH (e.g. hosted `image_generation` plus a
181
+ * declared `image_gen.imagegen` function/namespace tool for the imagegen skill). The ChatGPT
182
+ * backend tolerates the pair, but the platform `/v1/responses` rejects it:
183
+ * `Invalid Value: 'tools'. Function 'image_gen.imagegen' conflicts with a hosted tool in the
184
+ * same request.` Keyed hosted-type → conflicting client tool-name prefix; the hosted entry is
185
+ * dropped (the declared tool wins — Codex executes the skill client-side either way).
186
+ */
187
+ const HOSTED_TOOL_NAME_CONFLICTS: ReadonlyArray<{ hostedType: string; namePrefix: string }> = [
188
+ { hostedType: "image_generation", namePrefix: "image_gen" },
189
+ ];
190
+
191
+ /**
192
+ * Drop hosted tools whose names collide with declared function/namespace tools (see
193
+ * HOSTED_TOOL_NAME_CONFLICTS). Only applies on the API-key platform path: the ChatGPT backend
194
+ * ("forward" mode) accepts the pair, and stripping there would disable native imagegen. No-op
195
+ * (returns the original reference) when nothing matches.
196
+ */
197
+ function stripConflictingHostedTools(body: unknown): unknown {
198
+ if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
199
+ const allTools = body.tools;
200
+
201
+ const conflicting = HOSTED_TOOL_NAME_CONFLICTS.filter(c =>
202
+ allTools.some(t => {
203
+ if (!isPlainObject(t) || typeof t.name !== "string") return false;
204
+ if (t.type === "namespace") return t.name === c.namePrefix;
205
+ return t.name === c.namePrefix || t.name.startsWith(`${c.namePrefix}.`);
206
+ }),
207
+ );
208
+ if (conflicting.length === 0) return body;
209
+
210
+ const tools = allTools.filter(t => {
211
+ const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
212
+ if (!type) return true;
213
+ return !conflicting.some(c => c.hostedType === type);
214
+ });
215
+ return tools.length === allTools.length ? body : { ...body, tools };
216
+ }
217
+
177
218
  /**
178
219
  * Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
179
220
  * carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
@@ -236,6 +277,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
236
277
  forward || parsed._previousResponseInputExpanded === true,
237
278
  );
238
279
  if (forward) outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
280
+ else outBody = stripConflictingHostedTools(outBody);
239
281
  return {
240
282
  url,
241
283
  method: "POST",
@@ -569,6 +569,18 @@ function codexCommandCandidates(): string[] {
569
569
  return unique(candidates);
570
570
  }
571
571
 
572
+ /**
573
+ * Windows probe guard: only PE/batch launchers can be spawned as processes. Anything
574
+ * else pulled from the shim state (the extensionless Git-Bash sh backup
575
+ * `codex.opencodex-real`, `.ps1` scripts) risks falling through to the cmd/ShellExecute
576
+ * document-association path — Windows then OPENS the file in the user's editor
577
+ * (e.g. VS Code) on every `codex` launch instead of executing it.
578
+ */
579
+ export function isSpawnableCodexCandidate(path: string, platform: NodeJS.Platform = process.platform): boolean {
580
+ if (platform !== "win32") return true;
581
+ return /\.(cmd|bat|exe|com)$/i.test(path);
582
+ }
583
+
572
584
  function codexShimCommandCandidates(): string[] {
573
585
  try {
574
586
  const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
@@ -582,7 +594,7 @@ function codexShimCommandCandidates(): string[] {
582
594
  for (const file of files) {
583
595
  for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
584
596
  if (typeof value !== "string" || value.length === 0) continue;
585
- if (process.platform === "win32" && value.toLowerCase().endsWith(".ps1")) continue;
597
+ if (!isSpawnableCodexCandidate(value)) continue;
586
598
  out.push(value);
587
599
  }
588
600
  }
@@ -459,6 +459,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
459
459
  modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
460
460
  modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
461
461
  preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
462
+ // Issue #88: every DeepSeek API model is text-only input (no image support upstream) — the
463
+ // vision sidecar describes attached images for them, and the catalog advertises image input
464
+ // on their behalf (same treatment as opencode-go's DeepSeek V4 entries above).
465
+ noVisionModels: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS],
462
466
  },
463
467
  // llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
464
468
  { id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" },
@@ -19,6 +19,11 @@ export function isCodexReasoningEffort(effort: string): boolean {
19
19
  return CODEX_REASONING_SET.has(effort);
20
20
  }
21
21
 
22
+ /** Position of `effort` in the Codex ladder (low=0 .. ultra=5), or -1 when not a ladder member. */
23
+ export function codexEffortRank(effort: string): number {
24
+ return CODEX_REASONING_ORDER.indexOf(effort);
25
+ }
26
+
22
27
  export function modelRecordValue<T>(record: Record<string, T> | undefined, modelId: string): T | undefined {
23
28
  if (!record) return undefined;
24
29
  if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
@@ -278,6 +278,34 @@ export function parseRequest(body: unknown): OcxParsedRequest {
278
278
  continue;
279
279
  }
280
280
 
281
+ if (effectiveType === "agent_message") {
282
+ const agentMessage = item as {
283
+ author?: string;
284
+ recipient?: string;
285
+ content?: unknown;
286
+ };
287
+
288
+ const content = inputContentParts(
289
+ agentMessage.content as unknown[] | string | undefined,
290
+ );
291
+
292
+ const hasContent =
293
+ typeof content === "string"
294
+ ? content.trim().length > 0
295
+ : content.length > 0;
296
+
297
+ // An agent_message is external input delivered to the parent agent.
298
+ // Preserve it as a user-role turn so signed Anthropic thinking blocks
299
+ // on either side are never merged into one modified assistant response.
300
+ messages.push({
301
+ role: "user",
302
+ content: hasContent ? content : "(sub-agent message received)",
303
+ timestamp: now,
304
+ });
305
+
306
+ continue;
307
+ }
308
+
281
309
  if (effectiveType === "message") {
282
310
  const msg = item as { role?: string; content?: unknown };
283
311
  switch (msg.role) {
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Hard reasoning-effort caps (devlog/260710_subagent_effort_intercept).
3
+ *
4
+ * Prompt-side effort designation (injectionEffort) is advisory only: codex-rs inherits the
5
+ * parent's effective effort when spawn_agent carries no model/effort args
6
+ * (multi_agents_common.rs resolve defaults), rejects overrides on full-history forks, and a
7
+ * non-empty agent-role file rebuilds the child Config and silently drops spawn-time
8
+ * model/effort. So a session whose config default is ultra leaks max-tier children whenever
9
+ * the parent model spawns bare. This module is the enforcement path: it rewrites the effort
10
+ * of proxied turns at the single choke point every HTTP/WS turn passes through
11
+ * (handleResponses), using the same dual-shape rewrite contract as nativeEffortClamp —
12
+ * parsed.options.reasoning feeds routed adapters, _rawBody.reasoning.effort feeds the
13
+ * ChatGPT passthrough serializer.
14
+ */
15
+ import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
16
+ import { modelInList } from "../types";
17
+ import { codexEffortRank, configuredReasoningEfforts, isCodexReasoningEffort, modelRecordValue } from "../reasoning-effort";
18
+ import { catalogModelEfforts } from "../codex/catalog";
19
+
20
+ /**
21
+ * True when the request carries codex-rs's spawned-child markers, matched EXACTLY.
22
+ * Source of truth (openai/codex @ 6138909d): every collab-spawned child turn sends
23
+ * `x-openai-subagent: collab_spawn` (core/src/responses_metadata.rs) and embeds
24
+ * `"subagent_kind":"thread_spawn"` in the JSON `x-codex-turn-metadata` compatibility
25
+ * header. Both are checked: the WS bridge rebuilds internal requests from the
26
+ * FORWARD_HEADERS allowlist, so either header alone is sufficient evidence.
27
+ *
28
+ * Exact matching matters: upstream emits `x-openai-subagent` for OTHER internal
29
+ * turn categories too (review, compact, memory_consolidation, arbitrary "other"
30
+ * sources — responses_metadata.rs subagent_source). Those are maintenance turns,
31
+ * not spawned children, and must never trip subagentEffortCap.
32
+ */
33
+ export function isThreadSpawnRequest(headers: Headers): boolean {
34
+ if (headers.get("x-openai-subagent") === "collab_spawn") return true;
35
+ const turnMeta = headers.get("x-codex-turn-metadata");
36
+ if (!turnMeta) return false;
37
+ try {
38
+ const parsed = JSON.parse(turnMeta) as { subagent_kind?: unknown };
39
+ return parsed.subagent_kind === "thread_spawn";
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /** The effective ceiling for this turn, or undefined when no configured cap applies. */
46
+ export function effortCapFor(config: OcxConfig, subagent: boolean): string | undefined {
47
+ const caps: string[] = [];
48
+ if (config.effortCap && isCodexReasoningEffort(config.effortCap)) caps.push(config.effortCap);
49
+ if (subagent && config.subagentEffortCap && isCodexReasoningEffort(config.subagentEffortCap)) {
50
+ caps.push(config.subagentEffortCap);
51
+ }
52
+ if (caps.length === 0) return undefined;
53
+ return caps.reduce((low, cap) => (codexEffortRank(cap) < codexEffortRank(low) ? cap : low));
54
+ }
55
+
56
+ /**
57
+ * Whether the effort caps apply to this turn at all. Caps are a V2-surface feature
58
+ * (v1 sub-agents are pinned via explicit spawn args + injectionEffort prompting, so
59
+ * the ultra-default leak this module intercepts is v2-specific):
60
+ * - compaction turns are maintenance, not agent turns: they bypass caps entirely so
61
+ * native /v1/responses/compact (forwarded, never enters handleResponses) and routed
62
+ * compaction (synthesized internal request) get identical cap semantics.
63
+ * - multiAgentMode "v1" disables caps entirely (mirrors the GUI hiding the panel).
64
+ * - a main turn qualifies when its own tool list carries the v2 collab surface.
65
+ * - a CHILD turn is admitted by its spawned-child markers (isThreadSpawnRequest)
66
+ * REGARDLESS of tool surface: depth-limited leaves carry no collab tools (surface
67
+ * null) while children below the spawn-depth limit retain collab tools (spec_plan.rs
68
+ * leaf guard), so tool sniffing alone would cap siblings inconsistently.
69
+ * - a v1-surface MAIN turn (no child markers) never qualifies.
70
+ */
71
+ export function effortCapAppliesTo(
72
+ surface: "v1" | "v2" | null,
73
+ headers: Headers,
74
+ config: OcxConfig,
75
+ compaction = false,
76
+ ): boolean {
77
+ if (compaction) return false;
78
+ if (config.multiAgentMode === "v1") return false;
79
+ return surface === "v2" || isThreadSpawnRequest(headers);
80
+ }
81
+
82
+ /**
83
+ * The routed model's supported effort ladder for cap resolution, from the ROUTE's
84
+ * registry-merged provider (router.ts routedProviderConfig) — the persisted
85
+ * config.providers entry misses registry seeds, and bare ids can route via
86
+ * defaultModel/model-list/default-provider, so no "/" heuristic anywhere.
87
+ *
88
+ * - `[]` -> the model intentionally exposes no effort control (noReasoningModels or
89
+ * an explicitly empty configured ladder): cap resolution strips.
90
+ * - list -> sanitized + healed ladder (configuredReasoningEfforts).
91
+ * - undefined -> unknown. Includes the raw-nonempty-but-non-rankable case (e.g. a
92
+ * thinking-toggle ladder of ["enabled"]): sanitizing would flatten it to []
93
+ * and mis-classify it as "no effort control", so it stays unknown.
94
+ *
95
+ * Catalog fallback fires only for the ChatGPT-backend native passthrough IDENTITY
96
+ * (adapter "openai-responses" + authMode "forward", the fresh-install `openai`
97
+ * provider shape): the injected catalog is authoritative exactly for models Codex
98
+ * validates against that backend. A custom responses provider (key mode) serving a
99
+ * native-looking bare id must NOT inherit the unrelated native ladder.
100
+ */
101
+ export function supportedLadderFor(route: { provider: OcxProviderConfig; modelId: string }): string[] | undefined {
102
+ const { provider, modelId } = route;
103
+ if (modelInList(provider.noReasoningModels, modelId)) return [];
104
+ const raw = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
105
+ if (raw !== undefined) {
106
+ const sanitized = configuredReasoningEfforts(provider, modelId) ?? [];
107
+ if (sanitized.length === 0 && raw.length > 0) return undefined;
108
+ return sanitized;
109
+ }
110
+ if (provider.adapter === "openai-responses" && provider.authMode === "forward") {
111
+ const efforts = catalogModelEfforts([modelId]).get(modelId);
112
+ if (efforts && efforts.length > 0) return efforts;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ /**
118
+ * Resolve the configured cap against the model's supported ladder. Returns the effective
119
+ * ceiling rung, or null when the turn must be STRIPPED of its effort entirely. The cap
120
+ * NEVER raises: when rankable rungs exist but none sits at or below the cap, the model
121
+ * cannot run within the ceiling, so the effort is stripped and the provider default
122
+ * applies (never a rung above the cap).
123
+ */
124
+ export function resolveCappedEffort(cap: string, supported: readonly string[] | undefined): string | null {
125
+ if (supported === undefined) return cap;
126
+ const rankable = supported.filter(isCodexReasoningEffort);
127
+ if (rankable.length === 0) {
128
+ // Nonempty but non-rankable (e.g. ["enabled"]) -> unknown ladder, cap as-is.
129
+ // Genuinely empty -> no effort control at all -> strip.
130
+ return supported.length > 0 ? cap : null;
131
+ }
132
+ const capRank = codexEffortRank(cap);
133
+ let best: string | null = null;
134
+ for (const rung of rankable) {
135
+ const rank = codexEffortRank(rung);
136
+ if (rank <= capRank && (best === null || rank > codexEffortRank(best))) best = rung;
137
+ }
138
+ return best;
139
+ }
140
+
141
+ /**
142
+ * Cap the turn's reasoning effort in BOTH request shapes. Non-strip resolution only
143
+ * lowers: efforts at or below the resolved ceiling (and non-ladder/absent efforts) pass
144
+ * untouched. Strip resolution (model exposes no effort control, or no supported rung
145
+ * fits under the cap) removes whatever effort is present — regardless of its rank —
146
+ * from both shapes while preserving `reasoning.summary`. Returns the applied rewrite
147
+ * for request-log annotation (`to: "none"` on strip), or null when nothing changed.
148
+ */
149
+ export function applyEffortCap(
150
+ parsed: OcxParsedRequest,
151
+ headers: Headers,
152
+ config: OcxConfig,
153
+ supported?: readonly string[] | undefined,
154
+ ): { from: string; to: string; subagent: boolean } | null {
155
+ const subagent = isThreadSpawnRequest(headers);
156
+ const cap = effortCapFor(config, subagent);
157
+ if (!cap) return null;
158
+ const resolved = resolveCappedEffort(cap, supported);
159
+ const requested = parsed.options.reasoning;
160
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
161
+ if (resolved === null) {
162
+ if (!requested) return null;
163
+ parsed.options.reasoning = undefined;
164
+ if (raw?.reasoning && typeof raw.reasoning === "object") delete raw.reasoning.effort;
165
+ return { from: requested, to: "none", subagent };
166
+ }
167
+ if (!requested || !isCodexReasoningEffort(requested)) return null;
168
+ if (codexEffortRank(requested) <= codexEffortRank(resolved)) return null;
169
+ parsed.options.reasoning = resolved;
170
+ if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = resolved;
171
+ return { from: requested, to: resolved, subagent };
172
+ }
@@ -118,6 +118,7 @@ export {
118
118
  import { disableResponsesRequestTimeout, handleResponses, handleResponsesCompact } from "./responses";
119
119
  export { disableResponsesRequestTimeout, linkAbortSignal } from "./responses";
120
120
  import { handleImages } from "./images";
121
+ import { handleSearch } from "./search";
121
122
  import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
122
123
 
123
124
  const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
@@ -331,6 +332,33 @@ export function startServer(port?: number) {
331
332
  return withCors(response, req, config);
332
333
  }
333
334
 
335
+ if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
336
+ disableResponsesRequestTimeout(req, requestServer);
337
+ if (isDraining()) {
338
+ return new Response("Service shutting down", {
339
+ status: 503,
340
+ headers: { ...corsHeaders(req, config), "Retry-After": "5" },
341
+ });
342
+ }
343
+ const apiAuthError = requireApiAuth(req, config, "data-plane");
344
+ if (apiAuthError) return withCors(apiAuthError, req, config);
345
+ if (!isAllowedRequestOrigin(req, config)) {
346
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
347
+ }
348
+ const start = Date.now();
349
+ const requestId = nextRequestLogId(start);
350
+ const logCtx: RequestLogContext = { model: "web_search", provider: "unknown" };
351
+ const response = await handleSearch(req, config, logCtx);
352
+ addFinalRequestLog(
353
+ requestId,
354
+ start,
355
+ logCtx,
356
+ response.status,
357
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined,
358
+ );
359
+ return withCors(response, req, config);
360
+ }
361
+
334
362
  if (url.pathname === "/v1/responses" && req.method === "POST") {
335
363
  disableResponsesRequestTimeout(req, requestServer);
336
364
  if (isDraining()) {
@@ -370,7 +398,7 @@ export function startServer(port?: number) {
370
398
 
371
399
  // Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
372
400
  // GUI static handler (extensionless paths would get index.html with HTTP 200 and codex-rs
373
- // endpoint clients — alpha/search, memories/*, realtime/* — would surface confusing
401
+ // endpoint clients — memories/*, realtime/* — would surface confusing
374
402
  // serde decode errors instead of a clean not-found).
375
403
  if (url.pathname.startsWith("/v1/")) {
376
404
  return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
@@ -572,6 +572,34 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
572
572
  return jsonResponse({ ok: true, model: config.injectionModel ?? null, effort: config.injectionEffort ?? null, prompt: config.injectionPrompt ?? null });
573
573
  }
574
574
 
575
+ // Hard reasoning-effort caps (devlog/260710_subagent_effort_intercept): a global ceiling and a
576
+ // sub-agent-only ceiling, enforced per-request in handleResponses (src/server/effort-policy.ts).
577
+ // Key semantics per field: absent -> unchanged; null/"" -> clear; ladder value -> set; else 400.
578
+ if (url.pathname === "/api/effort-caps" && req.method === "GET") {
579
+ const { CODEX_REASONING_LEVELS } = await import("../reasoning-effort");
580
+ return jsonResponse({
581
+ effortCap: config.effortCap ?? null,
582
+ subagentEffortCap: config.subagentEffortCap ?? null,
583
+ efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
584
+ });
585
+ }
586
+ if (url.pathname === "/api/effort-caps" && req.method === "PUT") {
587
+ let body: { effortCap?: unknown; subagentEffortCap?: unknown };
588
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
589
+ const { isCodexReasoningEffort } = await import("../reasoning-effort");
590
+ for (const key of ["effortCap", "subagentEffortCap"] as const) {
591
+ if (!(key in body)) continue;
592
+ const value = body[key];
593
+ if (value === null || value === "") { delete config[key]; continue; }
594
+ if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
595
+ return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
596
+ }
597
+ config[key] = value;
598
+ }
599
+ saveConfig(config);
600
+ return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
601
+ }
602
+
575
603
  // Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
576
604
  // first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
577
605
  if (url.pathname === "/api/subagent-models" && req.method === "GET") {
@@ -298,10 +298,19 @@ function encryptedSlotParts(payload: string): Array<Record<string, string>> {
298
298
  return parts.length > 0 ? parts : [{ type: "input_text", text: payload }];
299
299
  }
300
300
 
301
+ function hasEncryptedContentPart(content: unknown): boolean {
302
+ return Array.isArray(content) && content.some(part => (
303
+ part && typeof part === "object"
304
+ && (part as { type?: unknown }).type === "encrypted_content"
305
+ ));
306
+ }
307
+
301
308
  /**
302
309
  * Rewrite non-ciphertext `{type:"encrypted_content"}` parts into `{type:"input_text"}`
303
- * throughout a native-bound request's input items (message content and
304
- * function_call_output content arrays share the part shape, codex-rs protocol/models.rs).
310
+ * throughout a request's input items (message content and function_call_output content
311
+ * arrays share the part shape, codex-rs protocol/models.rs). An `agent_message` whose
312
+ * payload becomes entirely plaintext is normalized to a user message so routed parsers
313
+ * that do not understand the internal item type still deliver the spawn task.
305
314
  * Genuine backend blobs are left byte-identical so replay/cache semantics survive, and
306
315
  * MIXED slots (plaintext preamble + embedded Fernet task body) are split so the backend
307
316
  * decrypts the blob while the prose passes as text. Returns the number of parts rewritten.
@@ -309,7 +318,8 @@ function encryptedSlotParts(payload: string): Array<Record<string, string>> {
309
318
  export function sanitizeEncryptedContentInPlace(input: unknown): number {
310
319
  if (!Array.isArray(input)) return 0;
311
320
  let rewritten = 0;
312
- const visit = (node: unknown): void => {
321
+ const visit = (node: unknown): number => {
322
+ const before = rewritten;
313
323
  if (Array.isArray(node)) {
314
324
  for (let i = 0; i < node.length; i += 1) {
315
325
  const child = node[i] as unknown;
@@ -327,13 +337,26 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
327
337
  continue;
328
338
  }
329
339
  }
330
- visit(child);
340
+ const childRewrites = visit(child);
341
+ if (
342
+ childRewrites > 0
343
+ && child && typeof child === "object"
344
+ && (child as { type?: unknown }).type === "agent_message"
345
+ && !hasEncryptedContentPart((child as { content?: unknown }).content)
346
+ ) {
347
+ const message = child as { type: string; role?: string; author?: unknown; recipient?: unknown };
348
+ message.type = "message";
349
+ message.role = "user";
350
+ delete message.author;
351
+ delete message.recipient;
352
+ }
331
353
  }
332
- return;
354
+ return rewritten - before;
333
355
  }
334
356
  if (node && typeof node === "object") {
335
357
  for (const value of Object.values(node)) visit(value);
336
358
  }
359
+ return rewritten - before;
337
360
  };
338
361
  visit(input);
339
362
  return rewritten;
@@ -410,6 +433,21 @@ export async function handleResponses(
410
433
  body = expandPreviousResponseInput(body);
411
434
  const previousResponseInputExpanded = body !== originalBody;
412
435
 
436
+ // Spawn-message compatibility (both directions): agent_message task payloads ride in
437
+ // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
438
+ // parsing so every consumer sees the payload: parseRequest (routed/translated providers read
439
+ // the parsed messages) and the native passthrough (_rawBody is this same object, serialized
440
+ // verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
441
+ {
442
+ const rewritten = sanitizeEncryptedContentInPlace(
443
+ (body as { input?: unknown } | undefined)?.input,
444
+ );
445
+ if (rewritten > 0)
446
+ console.warn(
447
+ `[opencodex] rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (spawn-message compatibility)`,
448
+ );
449
+ }
450
+
413
451
  let parsed;
414
452
  try {
415
453
  parsed = parseRequest(body);
@@ -452,15 +490,6 @@ export async function handleResponses(
452
490
  // Runs BEFORE the mock-max clamp below so the synthetic top tier (ultra arrives
453
491
  // as max on the codex wire) is still visible. Both request shapes are rewritten.
454
492
  {
455
- const requestedModelId = logCtx.requestedModel ?? route.modelId;
456
- // Cross-provider spawn poison fix: native-bound requests may carry plaintext parked in
457
- // encrypted_content slots (spawn messages minted under a routed parent). Rewrite them
458
- // to input_text before the passthrough serializes _rawBody verbatim.
459
- if (!requestedModelId.includes("/")) {
460
- const raw = parsed._rawBody as { input?: unknown } | undefined;
461
- const rewritten = sanitizeEncryptedContentInPlace(raw?.input);
462
- if (rewritten > 0) console.warn(`[opencodex] ${route.modelId}: rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (routed-parent spawn compatibility)`);
463
- }
464
493
  const guidance = await multiAgentGuidanceText(parsed, config.injectionModel, config.injectionEffort, config.subagentModels, config.injectionPrompt);
465
494
  if (guidance) {
466
495
  injectDeveloperMessage(parsed, guidance);
@@ -470,6 +499,33 @@ export async function handleResponses(
470
499
  }
471
500
  }
472
501
 
502
+ // Hard effort caps (effortCap / subagentEffortCap): enforcement companion to the advisory
503
+ // injection above — spawn-arg prompting cannot stop codex-rs from inheriting the parent's
504
+ // ultra-tier default on bare spawns (see src/server/effort-policy.ts). Runs BEFORE the
505
+ // mock-max clamp so a capped effort is what nativeness clamping then validates; rewrites
506
+ // both request shapes (same dual-write contract as the clamp below).
507
+ // GATE: v2 feature only (effortCapAppliesTo) — v2-surface main turns plus header-marked
508
+ // child turns admitted regardless of tool surface (depth-limited leaves carry no collab
509
+ // tools while shallower children do, so tool sniffing alone would cap siblings
510
+ // inconsistently); multiAgentMode "v1" disables caps entirely; compaction turns bypass
511
+ // caps so routed compaction matches native /v1/responses/compact (which never enters
512
+ // handleResponses).
513
+ {
514
+ const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("./effort-policy");
515
+ const surface = collabSurface(parsed);
516
+ if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
517
+ const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
518
+ if (capped) {
519
+ logCtx.requestedEffort = `${capped.from}->${capped.to}`;
520
+ if (isInjectionDebugEnabled()) {
521
+ console.log(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
522
+ }
523
+ }
524
+ } else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
525
+ console.log(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
526
+ }
527
+ }
528
+
473
529
  // Mock-max clamp: native models whose real ladder stops below max (gpt-5.5/5.4/…)
474
530
  // receive `max` when the user picks Ultra (codex converts ultra->max client-side).
475
531
  // Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT