@bitkyc08/opencodex 2.7.24 → 2.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -137,6 +137,11 @@ You can also add providers through `ocx init` (interactive CLI) or by editing `~
137
137
 
138
138
  Target any configured provider and model using the `provider/model` syntax:
139
139
 
140
+ Providers whose own model ids contain `/` (zenmux, openrouter, nvidia, …) are exposed to
141
+ Codex with inner slashes aliased to `-` (e.g. `zenmux/moonshotai-kimi-k3-free`); the
142
+ proxy transparently routes them back to the native id, and the raw full-slash form keeps
143
+ working too.
144
+
140
145
  ```bash
141
146
  # Use Claude Opus through Anthropic
142
147
  codex -m "anthropic/claude-opus-4-8" "Explain this stack trace"
package/README.zh-CN.md CHANGED
@@ -176,26 +176,27 @@ preview gate 限制;opencodex 只是准备好你的账户/provider 可访问
176
176
  <img src="assets/codex-app-picker.png" alt="Codex App 展示 opencodex 路由模型及 reasoning effort 选择器" width="480">
177
177
  </p>
178
178
 
179
- ## OpenAI provider 层级
179
+ ## OpenAI provider 账户模式
180
180
 
181
- | Provider ID | 层级 | 凭证 | 行为 |
181
+ | Provider ID | 路径 | 凭证 | 行为 |
182
182
  |---|---|---|---|
183
- | `openai` | Codex Direct | 当前 Codex 主登录 | 单账户,不读取或轮换池 |
184
- | `openai-multi` | Codex 多账户 | 主账户 + 添加的账户 | affinity、配额、冷却、failover |
183
+ | `openai` | Codex 登录 | 主账户 + 添加的 Codex 账户 | 默认 Pool,可选 Direct 模式 |
185
184
  | `openai-apikey` | OpenAI API | API key/key pool | 不进行 Codex 账户路由 |
186
185
 
187
- - Codex 主账户始终是 Multi 池的普通成员,不是池外 fallback
188
- - 新安装默认使用 Direct(`openai`)。`gpt-5.6-sol` 选择 Direct,
189
- `openai-multi/gpt-5.6-sol` 选择 Multi,`openai-apikey/gpt-5.6-sol` 选择 API。
190
- - 三个层级之间不会因凭证缺失而相互 fallback。
191
- - 升级时,带池账户的旧配置迁移到 Multi;旧的公开 `chatgpt` provider id 会被隐藏。
192
- 原配置只备份一次到 `~/.opencodex/config.json.pre-openai-tiers-v1.bak`。恢复命令:
193
- `cp ~/.opencodex/config.json.pre-openai-tiers-v1.bak ~/.opencodex/config.json`
186
+ - Pool 包含主登录和添加的账户,并应用 affinity、配额、冷却和 failover
187
+ - Direct 绕过池状态,只使用当前 caller/主登录 bearer。
188
+ - 新安装和未保存模式的配置默认使用 Pool。在仪表盘 **Providers** 中切换模式时,
189
+ `gpt-5.6-sol` bare 模型 id 保持不变。
190
+ - `openai-apikey/gpt-5.6-sol` 选择 API;Codex 登录与 API 凭证之间不会 fallback。
191
+ - 当前 marker 为 `openaiProviderTierVersion: 2`,原配置备份到
192
+ `~/.opencodex/config.json.pre-openai-tiers-v2.bak`。恢复命令:
193
+ `cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`
194
+ - 旧的 v1 三 provider 配置会自动迁移为单一 `openai` 行。
194
195
  - API 层 GPT-5.6 元数据为 1,050,000 context / 922,000 max input。
195
196
  `gpt-5.6-sol-pro`、`terra-pro`、`luna-pro` 保留公开 virtual id,线上请求改写为 base id 加
196
197
  `reasoning.mode: "pro"`。
197
198
 
198
- ### 多账户行为
199
+ ### Pool 账户行为
199
200
 
200
201
  打开仪表盘中的 **Codex Auth** 来添加池账户,并选择由哪个账户处理下一个 Codex 会话。
201
202
  opencodex 保持两种独立行为:
@@ -37,4 +37,4 @@ v2: 多线程代理(spawn_agent)。所有模型使用 v2 协作界面。
37
37
  "model": "gpt-5.4",
38
38
  "input": "Hello, world!"
39
39
  }'`})]})]})}var Ai=[`ANTHROPIC_MODEL`,`ANTHROPIC_DEFAULT_OPUS_MODEL`,`ANTHROPIC_DEFAULT_SONNET_MODEL`,`ANTHROPIC_DEFAULT_HAIKU_MODEL`,`ANTHROPIC_DEFAULT_FABLE_MODEL`];function ji(e){return e>=1e6?`1M`:`${Math.round(e/1e3)}k`}function Mi(e){let t=`http://127.0.0.1:${e.port}`,n=e.autoContext&&e.maxContextTokens===null,r=Ai.filter(t=>e.effectiveModelEnv[t]).map(t=>`export ${t}=${e.effectiveModelEnv[t]}`);return[`export ANTHROPIC_BASE_URL=${t}`,...e.authMode===`proxy`?[`export ANTHROPIC_AUTH_TOKEN=opencodex-proxy`]:[`# no ANTHROPIC_AUTH_TOKEN: your claude.ai login (and connectors) stay active`],`export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,...n?[`export CLAUDE_CODE_AUTO_COMPACT_WINDOW=${e.autoCompactWindow??35e4}`]:[],...r,`claude`].join(`
40
- `)}function Ni({label:e,checked:t,onChange:n}){return(0,S.jsxs)(`label`,{className:`toggle`,children:[(0,S.jsx)(`input`,{type:`checkbox`,checked:t,onChange:e=>n(e.target.checked),"aria-label":e}),(0,S.jsx)(`span`,{className:`slider`,"aria-hidden":`true`})]})}function Pi({apiBase:e}){let t=U(),[n,r]=(0,_.useState)(null),[i,a]=(0,_.useState)([]),[o,s]=(0,_.useState)(``),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!0),f=(0,_.useCallback)(async()=>{try{let t=await fetch(`${e}/api/claude-code`).then(e=>e.json());r({...t,authMode:t.authMode===`proxy`?`proxy`:`subscription`,systemEnv:t.systemEnv!==!1,fastMode:t.fastMode??null,maxContextTokens:t.maxContextTokens??null,autoContext:t.autoContext!==!1,autoCompactWindow:t.autoCompactWindow??null,injectAgents:t.injectAgents!==!1,effectiveModelEnv:t.effectiveModelEnv??{}}),a(Object.entries(t.modelMap??{}).map(([e,t])=>({from:e,to:String(t)})))}catch{l(!1),s(t(`claude.loadFail`))}finally{d(!1)}},[e,t]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{f()},0);return()=>window.clearTimeout(e)},[f]);let p=(0,_.useMemo)(()=>{let e=(n?.available??[]).map(e=>({value:e,label:String(tr(e))}));return[{value:``,label:t(`claude.slotUnset`)},...e]},[n?.available,t]),m=(0,_.useMemo)(()=>{let e=[1e5,2e5,25e4,3e5,35e4,4e5,5e5,6e5,75e4,9e5,1e6],r=n?.autoCompactWindow??null,i=r!==null&&!e.includes(r)?[...e,r].sort((e,t)=>e-t):e;return[{value:``,label:t(`claude.autoCompactDefault`)},...i.map(e=>({value:String(e),label:ji(e)}))]},[n?.autoCompactWindow,t]),h=async()=>{if(!n)return;s(``);let r={};for(let e of i)e.from.trim()&&e.to.trim()&&(r[e.from.trim()]=e.to.trim());try{let i=await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:n.enabled,authMode:n.authMode,systemEnv:n.systemEnv,fastMode:n.fastMode,autoContext:n.autoContext,autoCompactWindow:n.autoCompactWindow,injectAgents:n.injectAgents,smallFastModel:n.smallFastModel,modelMap:r,webSearchSidecar:n.webSearchSidecar?{backend:n.webSearchSidecar.backend??null,model:n.webSearchSidecar.model??``}:null,visionSidecar:n.visionSidecar?{backend:n.visionSidecar.backend??null,model:n.visionSidecar.model??``}:null})}),a=await i.json();l(i.ok),s(i.ok?t(`claude.saved`):a.error||t(`claude.saveFailed`)),i.ok&&await f()}catch{l(!1),s(t(`claude.networkError`))}};if(u)return(0,S.jsx)(`div`,{className:`muted`,style:{padding:8},children:t(`claude.loading`)});if(!n)return(0,S.jsx)(W,{tone:`err`,children:o||t(`claude.loadFail`)});let g=Mi(n);return(0,S.jsxs)(S.Fragment,{children:[(0,S.jsx)(`div`,{className:`page-head`,children:(0,S.jsx)(`h2`,{children:t(`claude.pageTitle`)})}),(0,S.jsx)(`p`,{className:`page-sub`,children:t(`claude.subtitle`)}),o&&(0,S.jsx)(W,{tone:c?`ok`:`err`,children:o}),(0,S.jsxs)(`div`,{className:`card`,style:{overflow:`hidden`},children:[(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.enabledLabel`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.enabledHint`)})]}),(0,S.jsx)(Ni,{label:t(`claude.enabledLabel`),checked:n.enabled,onChange:e=>r({...n,enabled:e})})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.authMode`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.authModeHint`)})]}),(0,S.jsx)(G,{value:n.authMode,options:[{value:`subscription`,label:t(`claude.authModeSubscription`)},{value:`proxy`,label:t(`claude.authModeProxy`)}],onChange:e=>r({...n,authMode:e}),label:t(`claude.authMode`),style:{minWidth:220}})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.systemEnv`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.systemEnvDesc`)}),n.systemEnv&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:t(`claude.systemEnvWarn`)})]}),(0,S.jsx)(Ni,{label:t(`claude.systemEnv`),checked:n.systemEnv,onChange:e=>r({...n,systemEnv:e})})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.fastMode`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.fastModeDesc`)})]}),(0,S.jsxs)(`select`,{value:n.fastMode===null?`auto`:n.fastMode?`on`:`off`,onChange:e=>{let t=e.target.value;r({...n,fastMode:t===`auto`?null:t===`on`})},className:`text-label font-medium`,"aria-label":t(`claude.fastMode`),style:{padding:`5px 10px`,borderRadius:`var(--radius-xs)`,border:`1px solid var(--border)`,background:`var(--surface)`,color:`var(--text)`},children:[(0,S.jsx)(`option`,{value:`auto`,children:t(`claude.fastAuto`)}),(0,S.jsx)(`option`,{value:`on`,children:t(`claude.fastOn`)}),(0,S.jsx)(`option`,{value:`off`,children:t(`claude.fastOff`)})]})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.autoContext`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.autoContextDesc`)}),n.maxContextTokens!==null&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--muted)`},children:t(`claude.autoContextInert`)})]}),(0,S.jsx)(Ni,{label:t(`claude.autoContext`),checked:n.autoContext,onChange:e=>r({...n,autoContext:e})})]}),n.autoContext&&(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.autoCompactWindow`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.autoCompactWindowDesc`)}),n.autoCompactWindow!==null&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:t(`claude.autoCompactWindowWarn`)})]}),(0,S.jsx)(G,{value:n.autoCompactWindow===null?``:String(n.autoCompactWindow),options:m,onChange:e=>r({...n,autoCompactWindow:e===``?null:Number(e)}),label:t(`claude.autoCompactWindow`),style:{minWidth:130}})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.injectAgents`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.injectAgentsDesc`)})]}),(0,S.jsx)(Ni,{label:t(`claude.injectAgents`),checked:n.injectAgents,onChange:e=>r({...n,injectAgents:e})})]}),[`webSearchSidecar`,`visionSidecar`].map(e=>{let i=n[e],a=e===`webSearchSidecar`?`claude.webSearchSidecar`:`claude.visionSidecar`,o=e===`webSearchSidecar`?`claude.webSearchSidecarHint`:`claude.visionSidecarHint`;return(0,S.jsxs)(`div`,{className:`setting-row`,style:{alignItems:`flex-start`},children:[(0,S.jsxs)(`div`,{className:`setting-label setting-copy`,style:{flex:1},children:[(0,S.jsx)(`span`,{className:`title`,children:t(a)}),(0,S.jsx)(`span`,{className:`desc`,children:t(o)})]}),(0,S.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8},children:[(0,S.jsx)(G,{value:i?i.backend??`auto`:`inherit`,options:[{value:`inherit`,label:t(`claude.useMainSetting`)},{value:`auto`,label:t(`dash.backendAuto`)},{value:`openai`,label:t(`dash.backendOpenAI`)},{value:`anthropic`,label:t(`dash.backendAnthropic`)}],onChange:t=>r({...n,[e]:t===`inherit`?void 0:{...i,backend:t===`auto`?void 0:t}}),label:t(`dash.sidecarBackend`)}),(0,S.jsx)(`input`,{className:`input mono`,value:i?.model??``,onChange:t=>r({...n,[e]:{...i,model:t.target.value}}),placeholder:t(`claude.sidecarModelPlaceholder`),disabled:!i,"aria-label":t(`dash.sidecarModel`),style:{minWidth:210}})]})]},e)})]}),(0,S.jsx)(`div`,{className:`h-section`,children:t(`claude.quickstart`)}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:(0,S.jsx)(Te,{k:`claude.quickstartHint`,cmd:`ocx claude`})}),(0,S.jsx)(`pre`,{className:`mono card`,style:{padding:`10px 14px`,overflowX:`auto`,margin:0},children:`ocx claude`}),(0,S.jsxs)(`details`,{style:{margin:`10px 0 0`},children:[(0,S.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:t(`claude.manualEnv`)}),(0,S.jsx)(`pre`,{className:`mono card text-label`,style:{padding:`10px 14px`,overflowX:`auto`,margin:`6px 0 0`},children:g})]}),(0,S.jsx)(`div`,{className:`h-section`,children:t(`claude.smallFastModel`)}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.smallFastModelHint`)}),(0,S.jsx)(G,{value:n.smallFastModel,options:p,onChange:e=>r({...n,smallFastModel:e}),label:t(`claude.smallFastModel`),style:{maxWidth:420}}),(0,S.jsxs)(`div`,{className:`h-section`,children:[t(`claude.modelMap`),` `,(0,S.jsx)(`span`,{className:`count`,children:i.length})]}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.modelMapHint`)}),(0,S.jsx)(`div`,{className:`stack`,style:{gap:8},children:i.map((e,n)=>(0,S.jsxs)(`div`,{className:`row`,style:{gap:8},children:[(0,S.jsx)(`input`,{className:`input mono`,value:e.from,placeholder:t(`claude.mapFrom`),"aria-label":t(`claude.mapFrom`),onChange:e=>a(t=>t.map((t,r)=>r===n?{...t,from:e.target.value}:t)),style:{flex:1}}),(0,S.jsx)(`span`,{className:`muted`,"aria-hidden":!0,children:`→`}),(0,S.jsx)(`input`,{className:`input mono`,value:e.to,placeholder:t(`claude.mapTo`),"aria-label":t(`claude.mapTo`),onChange:e=>a(t=>t.map((t,r)=>r===n?{...t,to:e.target.value}:t)),style:{flex:1}}),(0,S.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>a(e=>e.filter((e,t)=>t!==n)),"aria-label":t(`claude.removeMapping`),style:{color:`var(--red)`},children:(0,S.jsx)(N,{})})]},n))}),(0,S.jsx)(`div`,{style:{marginTop:8},children:(0,S.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>a(e=>[...e,{from:``,to:``}]),children:[(0,S.jsx)(P,{}),` `,t(`claude.addMapping`)]})}),(0,S.jsx)(`div`,{style:{marginTop:14},children:(0,S.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:h,children:t(`common.save`)})}),(0,S.jsxs)(`div`,{className:`h-section`,children:[t(`claude.aliases`),` `,(0,S.jsx)(`span`,{className:`count`,children:n.aliases.length})]}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.aliasesHint`)}),n.aliases.length===0?(0,S.jsx)(`div`,{className:`muted text-label`,children:t(`claude.none`)}):(0,S.jsx)(`div`,{className:`stack`,style:{gap:6,maxHeight:320,overflowY:`auto`},children:Array.from(n.aliases.reduce((e,t)=>{let n=/\(([^)]+)\)\s*$/.exec(t.display_name),r=n?n[1]:`etc`;return(e.get(r)??e.set(r,[]).get(r)).push(t),e},new Map)).map(([e,t])=>(0,S.jsxs)(`div`,{children:[(0,S.jsxs)(`div`,{className:`muted text-caption font-semibold`,style:{textTransform:`uppercase`,letterSpacing:`var(--tracking-wide)`,margin:`6px 2px 4px`},children:[e,` · `,t.length]}),(0,S.jsx)(`div`,{className:`stack`,style:{gap:4},children:t.map(e=>(0,S.jsxs)(`div`,{className:`card row`,style:{padding:`6px 12px`,gap:10},children:[(0,S.jsx)(`code`,{className:`mono text-label`,style:{flex:1},children:e.id}),(0,S.jsx)(`span`,{className:`muted text-label`,children:e.display_name})]},e.id))})]},e))})]})}var Fi=`opencodex-api-token`,Ii=!1,Li=null;function Ri(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function zi(e){let t=Ri(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Bi(){try{return sessionStorage.getItem(Fi)?.trim()||null}catch{return null}}function Vi(e){try{sessionStorage.setItem(Fi,e)}catch{}}function Hi(){try{sessionStorage.removeItem(Fi)}catch{}}function Ui(e,t,n){let r=new Headers(t?.headers??(e instanceof Request?e.headers:void 0));return r.set(`X-OpenCodex-API-Key`,n),e instanceof Request?[new Request(e,{headers:r}),t?{...t,headers:r}:void 0]:[e,{...t,headers:r}]}async function Wi(){return Li||(Li=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Li=null}),Li)}function Gi(){if(Ii)return;Ii=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!zi(t))return e(t,n);let r=Bi(),[i,a]=r?Ui(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Hi();let s=await Wi();if(!s)return o;Vi(s);let[c,l]=Ui(t,n,s),u=await e(c,l);return u.status===401&&Hi(),u}}Gi();var Ki=new Set([`dashboard`,`providers`,`models`,`combos`,`subagents`,`logs`,`debug`,`usage`,`codex-auth`,`api`,`claude`]);function qi(){let e=location.hash.replace(/^#\/?/,``).split(`/`)[0];return Ki.has(e)?e:`dashboard`}function Ji(e,t){return e===t||t===`providers`&&e===`providers/workspace`}var Yi=``,Xi=`ocx-theme`,Zi=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`combos`,tkey:`nav.combos`,Icon:pe},{id:`subagents`,tkey:`nav.subagents`,Icon:D},{id:`logs`,tkey:`nav.logs`,Icon:O},{id:`debug`,tkey:`nav.debug`,Icon:A},{id:`usage`,tkey:`nav.usage`,Icon:j},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:ae},{id:`api`,tkey:`nav.api`,Icon:de},{id:`claude`,tkey:`nav.claude`,Icon:fe}],Qi={light:le,dark:V,system:ue},$i={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function ea(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function ta(){let e=localStorage.getItem(Xi);return e===`light`||e===`dark`?e:`system`}function na(){let[e,t]=(0,_.useState)(qi),[n,r]=(0,_.useState)(ta),[i,a]=(0,_.useState)(null),{locale:o,setLocale:s}=H(),c=U(),[l,u]=(0,_.useState)(!1),d=(0,_.useRef)(null),f=(0,_.useRef)(null),p=(0,_.useRef)(!1);(0,_.useEffect)(()=>{let e=()=>{let e=qi(),n=window.location.hash.replace(/^#\/?/,``);if(u(!1),!Ji(n,e)){window.location.hash=e;return}t(e)};return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{Ji(window.location.hash.replace(/^#\/?/,``),e)||(window.location.hash=e)},[e]),(0,_.useEffect)(()=>{let e=document.documentElement;n===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Xi)):(e.setAttribute(`data-theme`,n),localStorage.setItem(Xi,n))},[n]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Yi}/healthz`);if(!t.ok)return;let n=ea(await t.json());!e&&n&&a(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let m=()=>r(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),h=Qi[n],g=i??`2.7.24`,[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null);(0,_.useEffect)(()=>{if(!l)return;let e=e=>{e.key===`Escape`&&u(!1)};window.addEventListener(`keydown`,e);let t=document.body.style.overflow;return document.body.style.overflow=`hidden`,()=>{window.removeEventListener(`keydown`,e),document.body.style.overflow=t}},[l]),(0,_.useEffect)(()=>{if(l){p.current=!0;let e=setTimeout(()=>f.current?.focus(),200);return()=>clearTimeout(e)}p.current&&(p.current=!1,d.current?.focus())},[l]),(0,_.useEffect)(()=>{let e=window.matchMedia(`(min-width: 761px)`),t=()=>{e.matches&&u(!1)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]),(0,_.useEffect)(()=>{let e=!1;return fetch(`${Yi}/api/claude-code`).then(e=>e.json()).then(t=>{!e&&typeof t.enabled==`boolean`&&x(t.enabled)}).catch(()=>{}),()=>{e=!0}},[]);let C=async()=>{if(b===null)return;let e=!b;x(e);try{(await fetch(`${Yi}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:e})})).ok||x(!e)}catch{x(!e)}},w=async()=>{if(confirm(c(`dash.stopConfirm`))){y(!0);try{await fetch(`${Yi}/api/stop`,{method:`POST`})}catch{}}},T=(0,S.jsxs)(`div`,{className:`brand`,children:[(0,S.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":c(`app.logoAria`)}),(0,S.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,S.jsxs)(`span`,{className:`ver`,children:[`v`,g]})]});return(0,S.jsxs)(`div`,{className:`app`,children:[(0,S.jsxs)(`header`,{className:`mobile-topbar`,inert:l,children:[(0,S.jsx)(`button`,{ref:d,type:`button`,className:`menu-toggle`,onClick:()=>u(e=>!e),"aria-expanded":l,"aria-controls":`app-sidebar`,"aria-label":c(l?`nav.closeMenu`:`nav.openMenu`),title:c(l?`nav.closeMenu`:`nav.openMenu`),children:(0,S.jsx)(k,{})}),T,(0,S.jsx)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:w,disabled:v,"aria-label":c(`dash.stop`),title:c(`dash.stop`),children:(0,S.jsx)(re,{})})]}),l&&(0,S.jsx)(`div`,{className:`drawer-scrim`,onClick:()=>u(!1),"aria-hidden":`true`}),(0,S.jsxs)(`aside`,{id:`app-sidebar`,className:`sidebar${l?` open`:``}`,ref:f,tabIndex:-1,children:[(0,S.jsxs)(`div`,{className:`drawer-head`,children:[T,(0,S.jsx)(`button`,{type:`button`,className:`menu-toggle drawer-close`,onClick:()=>u(!1),"aria-label":c(`nav.closeMenu`),title:c(`nav.closeMenu`),children:(0,S.jsx)(N,{})})]}),(0,S.jsx)(`nav`,{children:Zi.map(({id:n,tkey:r,Icon:i})=>(0,S.jsxs)(`button`,{className:`nav-item${e===n?` active`:``}`,"data-page":n,onClick:()=>{t(n),u(!1)},"aria-current":e===n?`page`:void 0,children:[(0,S.jsx)(i,{}),` `,c(r)]},n))}),(0,S.jsxs)(`div`,{className:`sidebar-foot`,children:[b!==null&&(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:C,"aria-pressed":b,"aria-label":c(`claude.toggleAria`),title:c(`claude.toggleAria`),style:b?{color:`var(--accent)`}:void 0,children:[(0,S.jsx)(fe,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c(b?`app.claudeOn`:`app.claudeOff`)})]}),(0,S.jsxs)(`div`,{className:`lang-toggle`,children:[(0,S.jsx)(de,{"aria-hidden":!0}),(0,S.jsx)(G,{value:o,options:ye.map(e=>({value:e.code,label:e.name})),onChange:e=>s(e),label:c(`lang.label`),placement:`right`,style:{flex:1,minWidth:0,width:`100%`}})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:m,"aria-label":`${c(`theme.label`)}: ${c($i[n])}`,title:`${c(`theme.label`)}: ${c($i[n])}`,children:[(0,S.jsx)(h,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c($i[n])})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:w,disabled:v,"aria-label":c(`dash.stop`),title:c(`dash.stop`),children:[(0,S.jsx)(re,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c(v?`dash.stopping`:`dash.stop`)})]}),(0,S.jsxs)(`a`,{className:`sidebar-link`,href:`https://github.com/lidge-jun/opencodex`,target:`_blank`,rel:`noreferrer`,children:[(0,S.jsx)(B,{}),` `,c(`common.github`)]})]})]}),(0,S.jsx)(`main`,{className:`main`,inert:l,children:(0,S.jsxs)(`div`,{className:`main-inner${e===`combos`?` main-inner--combos`:``}`,children:[e===`dashboard`&&(0,S.jsx)(Ve,{apiBase:Yi}),e===`providers`&&(0,S.jsx)(Xn,{apiBase:Yi}),e===`models`&&(0,S.jsx)(dr,{apiBase:Yi}),e===`combos`&&(0,S.jsx)(Wr,{apiBase:Yi}),e===`subagents`&&(0,S.jsx)(Gr,{apiBase:Yi}),e===`logs`&&(0,S.jsx)(ii,{apiBase:Yi}),e===`debug`&&(0,S.jsx)(fi,{apiBase:Yi}),e===`usage`&&(0,S.jsx)(Ei,{apiBase:Yi}),e===`codex-auth`&&(0,S.jsx)(K,{apiBase:Yi}),e===`api`&&(0,S.jsx)(ki,{apiBase:Yi}),e===`claude`&&(0,S.jsx)(Pi,{apiBase:Yi})]})})]})}v.createRoot(document.getElementById(`root`)).render((0,S.jsx)(_.StrictMode,{children:(0,S.jsx)(we,{children:(0,S.jsx)(na,{})})}));
40
+ `)}function Ni({label:e,checked:t,onChange:n}){return(0,S.jsxs)(`label`,{className:`toggle`,children:[(0,S.jsx)(`input`,{type:`checkbox`,checked:t,onChange:e=>n(e.target.checked),"aria-label":e}),(0,S.jsx)(`span`,{className:`slider`,"aria-hidden":`true`})]})}function Pi({apiBase:e}){let t=U(),[n,r]=(0,_.useState)(null),[i,a]=(0,_.useState)([]),[o,s]=(0,_.useState)(``),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!0),f=(0,_.useCallback)(async()=>{try{let t=await fetch(`${e}/api/claude-code`).then(e=>e.json());r({...t,authMode:t.authMode===`proxy`?`proxy`:`subscription`,systemEnv:t.systemEnv!==!1,fastMode:t.fastMode??null,maxContextTokens:t.maxContextTokens??null,autoContext:t.autoContext!==!1,autoCompactWindow:t.autoCompactWindow??null,injectAgents:t.injectAgents!==!1,effectiveModelEnv:t.effectiveModelEnv??{}}),a(Object.entries(t.modelMap??{}).map(([e,t])=>({from:e,to:String(t)})))}catch{l(!1),s(t(`claude.loadFail`))}finally{d(!1)}},[e,t]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{f()},0);return()=>window.clearTimeout(e)},[f]);let p=(0,_.useMemo)(()=>{let e=(n?.available??[]).map(e=>({value:e,label:String(tr(e))}));return[{value:``,label:t(`claude.slotUnset`)},...e]},[n?.available,t]),m=(0,_.useMemo)(()=>{let e=[1e5,2e5,25e4,3e5,35e4,4e5,5e5,6e5,75e4,9e5,1e6],r=n?.autoCompactWindow??null,i=r!==null&&!e.includes(r)?[...e,r].sort((e,t)=>e-t):e;return[{value:``,label:t(`claude.autoCompactDefault`)},...i.map(e=>({value:String(e),label:ji(e)}))]},[n?.autoCompactWindow,t]),h=async()=>{if(!n)return;s(``);let r={};for(let e of i)e.from.trim()&&e.to.trim()&&(r[e.from.trim()]=e.to.trim());try{let i=await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:n.enabled,authMode:n.authMode,systemEnv:n.systemEnv,fastMode:n.fastMode,autoContext:n.autoContext,autoCompactWindow:n.autoCompactWindow,injectAgents:n.injectAgents,smallFastModel:n.smallFastModel,modelMap:r,webSearchSidecar:n.webSearchSidecar?{backend:n.webSearchSidecar.backend??null,model:n.webSearchSidecar.model??``}:null,visionSidecar:n.visionSidecar?{backend:n.visionSidecar.backend??null,model:n.visionSidecar.model??``}:null})}),a=await i.json();l(i.ok),s(i.ok?t(`claude.saved`):a.error||t(`claude.saveFailed`)),i.ok&&await f()}catch{l(!1),s(t(`claude.networkError`))}};if(u)return(0,S.jsx)(`div`,{className:`muted`,style:{padding:8},children:t(`claude.loading`)});if(!n)return(0,S.jsx)(W,{tone:`err`,children:o||t(`claude.loadFail`)});let g=Mi(n);return(0,S.jsxs)(S.Fragment,{children:[(0,S.jsx)(`div`,{className:`page-head`,children:(0,S.jsx)(`h2`,{children:t(`claude.pageTitle`)})}),(0,S.jsx)(`p`,{className:`page-sub`,children:t(`claude.subtitle`)}),o&&(0,S.jsx)(W,{tone:c?`ok`:`err`,children:o}),(0,S.jsxs)(`div`,{className:`card`,style:{overflow:`hidden`},children:[(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.enabledLabel`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.enabledHint`)})]}),(0,S.jsx)(Ni,{label:t(`claude.enabledLabel`),checked:n.enabled,onChange:e=>r({...n,enabled:e})})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.authMode`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.authModeHint`)})]}),(0,S.jsx)(G,{value:n.authMode,options:[{value:`subscription`,label:t(`claude.authModeSubscription`)},{value:`proxy`,label:t(`claude.authModeProxy`)}],onChange:e=>r({...n,authMode:e}),label:t(`claude.authMode`),style:{minWidth:220}})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.systemEnv`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.systemEnvDesc`)}),n.systemEnv&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:t(`claude.systemEnvWarn`)})]}),(0,S.jsx)(Ni,{label:t(`claude.systemEnv`),checked:n.systemEnv,onChange:e=>r({...n,systemEnv:e})})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.fastMode`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.fastModeDesc`)})]}),(0,S.jsxs)(`select`,{value:n.fastMode===null?`auto`:n.fastMode?`on`:`off`,onChange:e=>{let t=e.target.value;r({...n,fastMode:t===`auto`?null:t===`on`})},className:`text-label font-medium`,"aria-label":t(`claude.fastMode`),style:{padding:`5px 10px`,borderRadius:`var(--radius-xs)`,border:`1px solid var(--border)`,background:`var(--surface)`,color:`var(--text)`},children:[(0,S.jsx)(`option`,{value:`auto`,children:t(`claude.fastAuto`)}),(0,S.jsx)(`option`,{value:`on`,children:t(`claude.fastOn`)}),(0,S.jsx)(`option`,{value:`off`,children:t(`claude.fastOff`)})]})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.autoContext`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.autoContextDesc`)}),n.maxContextTokens!==null&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--muted)`},children:t(`claude.autoContextInert`)})]}),(0,S.jsx)(Ni,{label:t(`claude.autoContext`),checked:n.autoContext,onChange:e=>r({...n,autoContext:e})})]}),n.autoContext&&(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.autoCompactWindow`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.autoCompactWindowDesc`)}),n.autoCompactWindow!==null&&(0,S.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:t(`claude.autoCompactWindowWarn`)})]}),(0,S.jsx)(G,{value:n.autoCompactWindow===null?``:String(n.autoCompactWindow),options:m,onChange:e=>r({...n,autoCompactWindow:e===``?null:Number(e)}),label:t(`claude.autoCompactWindow`),style:{minWidth:130}})]}),(0,S.jsxs)(`div`,{className:`setting-row`,children:[(0,S.jsxs)(`div`,{className:`setting-label`,children:[(0,S.jsx)(`span`,{className:`title`,children:t(`claude.injectAgents`)}),(0,S.jsx)(`span`,{className:`desc`,children:t(`claude.injectAgentsDesc`)})]}),(0,S.jsx)(Ni,{label:t(`claude.injectAgents`),checked:n.injectAgents,onChange:e=>r({...n,injectAgents:e})})]}),[`webSearchSidecar`,`visionSidecar`].map(e=>{let i=n[e],a=e===`webSearchSidecar`?`claude.webSearchSidecar`:`claude.visionSidecar`,o=e===`webSearchSidecar`?`claude.webSearchSidecarHint`:`claude.visionSidecarHint`;return(0,S.jsxs)(`div`,{className:`setting-row`,style:{alignItems:`flex-start`},children:[(0,S.jsxs)(`div`,{className:`setting-label setting-copy`,style:{flex:1},children:[(0,S.jsx)(`span`,{className:`title`,children:t(a)}),(0,S.jsx)(`span`,{className:`desc`,children:t(o)})]}),(0,S.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8},children:[(0,S.jsx)(G,{value:i?i.backend??`auto`:`inherit`,options:[{value:`inherit`,label:t(`claude.useMainSetting`)},{value:`auto`,label:t(`dash.backendAuto`)},{value:`openai`,label:t(`dash.backendOpenAI`)},{value:`anthropic`,label:t(`dash.backendAnthropic`)}],onChange:t=>r({...n,[e]:t===`inherit`?void 0:{...i,backend:t===`auto`?void 0:t}}),label:t(`dash.sidecarBackend`)}),(0,S.jsx)(`input`,{className:`input mono`,value:i?.model??``,onChange:t=>r({...n,[e]:{...i,model:t.target.value}}),placeholder:t(`claude.sidecarModelPlaceholder`),disabled:!i,"aria-label":t(`dash.sidecarModel`),style:{minWidth:210}})]})]},e)})]}),(0,S.jsx)(`div`,{className:`h-section`,children:t(`claude.quickstart`)}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:(0,S.jsx)(Te,{k:`claude.quickstartHint`,cmd:`ocx claude`})}),(0,S.jsx)(`pre`,{className:`mono card`,style:{padding:`10px 14px`,overflowX:`auto`,margin:0},children:`ocx claude`}),(0,S.jsxs)(`details`,{style:{margin:`10px 0 0`},children:[(0,S.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:t(`claude.manualEnv`)}),(0,S.jsx)(`pre`,{className:`mono card text-label`,style:{padding:`10px 14px`,overflowX:`auto`,margin:`6px 0 0`},children:g})]}),(0,S.jsx)(`div`,{className:`h-section`,children:t(`claude.smallFastModel`)}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.smallFastModelHint`)}),(0,S.jsx)(G,{value:n.smallFastModel,options:p,onChange:e=>r({...n,smallFastModel:e}),label:t(`claude.smallFastModel`),style:{maxWidth:420}}),(0,S.jsxs)(`div`,{className:`h-section`,children:[t(`claude.modelMap`),` `,(0,S.jsx)(`span`,{className:`count`,children:i.length})]}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.modelMapHint`)}),(0,S.jsx)(`div`,{className:`stack`,style:{gap:8},children:i.map((e,n)=>(0,S.jsxs)(`div`,{className:`row`,style:{gap:8},children:[(0,S.jsx)(`input`,{className:`input mono`,value:e.from,placeholder:t(`claude.mapFrom`),"aria-label":t(`claude.mapFrom`),onChange:e=>a(t=>t.map((t,r)=>r===n?{...t,from:e.target.value}:t)),style:{flex:1}}),(0,S.jsx)(`span`,{className:`muted`,"aria-hidden":!0,children:`→`}),(0,S.jsx)(`input`,{className:`input mono`,value:e.to,placeholder:t(`claude.mapTo`),"aria-label":t(`claude.mapTo`),onChange:e=>a(t=>t.map((t,r)=>r===n?{...t,to:e.target.value}:t)),style:{flex:1}}),(0,S.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>a(e=>e.filter((e,t)=>t!==n)),"aria-label":t(`claude.removeMapping`),style:{color:`var(--red)`},children:(0,S.jsx)(N,{})})]},n))}),(0,S.jsx)(`div`,{style:{marginTop:8},children:(0,S.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>a(e=>[...e,{from:``,to:``}]),children:[(0,S.jsx)(P,{}),` `,t(`claude.addMapping`)]})}),(0,S.jsx)(`div`,{style:{marginTop:14},children:(0,S.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:h,children:t(`common.save`)})}),(0,S.jsxs)(`div`,{className:`h-section`,children:[t(`claude.aliases`),` `,(0,S.jsx)(`span`,{className:`count`,children:n.aliases.length})]}),(0,S.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:t(`claude.aliasesHint`)}),n.aliases.length===0?(0,S.jsx)(`div`,{className:`muted text-label`,children:t(`claude.none`)}):(0,S.jsx)(`div`,{className:`stack`,style:{gap:6,maxHeight:320,overflowY:`auto`},children:Array.from(n.aliases.reduce((e,t)=>{let n=/\(([^)]+)\)\s*$/.exec(t.display_name),r=n?n[1]:`etc`;return(e.get(r)??e.set(r,[]).get(r)).push(t),e},new Map)).map(([e,t])=>(0,S.jsxs)(`div`,{children:[(0,S.jsxs)(`div`,{className:`muted text-caption font-semibold`,style:{textTransform:`uppercase`,letterSpacing:`var(--tracking-wide)`,margin:`6px 2px 4px`},children:[e,` · `,t.length]}),(0,S.jsx)(`div`,{className:`stack`,style:{gap:4},children:t.map(e=>(0,S.jsxs)(`div`,{className:`card row`,style:{padding:`6px 12px`,gap:10},children:[(0,S.jsx)(`code`,{className:`mono text-label`,style:{flex:1},children:e.id}),(0,S.jsx)(`span`,{className:`muted text-label`,children:e.display_name})]},e.id))})]},e))})]})}var Fi=`opencodex-api-token`,Ii=!1,Li=null;function Ri(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function zi(e){let t=Ri(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Bi(){try{return sessionStorage.getItem(Fi)?.trim()||null}catch{return null}}function Vi(e){try{sessionStorage.setItem(Fi,e)}catch{}}function Hi(){try{sessionStorage.removeItem(Fi)}catch{}}function Ui(e,t,n){let r=new Headers(t?.headers??(e instanceof Request?e.headers:void 0));return r.set(`X-OpenCodex-API-Key`,n),e instanceof Request?[new Request(e,{headers:r}),t?{...t,headers:r}:void 0]:[e,{...t,headers:r}]}async function Wi(){return Li||(Li=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Li=null}),Li)}function Gi(){if(Ii)return;Ii=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!zi(t))return e(t,n);let r=Bi(),[i,a]=r?Ui(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Hi();let s=await Wi();if(!s)return o;Vi(s);let[c,l]=Ui(t,n,s),u=await e(c,l);return u.status===401&&Hi(),u}}Gi();var Ki=new Set([`dashboard`,`providers`,`models`,`combos`,`subagents`,`logs`,`debug`,`usage`,`codex-auth`,`api`,`claude`]);function qi(){let e=location.hash.replace(/^#\/?/,``).split(`/`)[0];return Ki.has(e)?e:`dashboard`}function Ji(e,t){return e===t||t===`providers`&&e===`providers/workspace`}var Yi=``,Xi=`ocx-theme`,Zi=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`combos`,tkey:`nav.combos`,Icon:pe},{id:`subagents`,tkey:`nav.subagents`,Icon:D},{id:`logs`,tkey:`nav.logs`,Icon:O},{id:`debug`,tkey:`nav.debug`,Icon:A},{id:`usage`,tkey:`nav.usage`,Icon:j},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:ae},{id:`api`,tkey:`nav.api`,Icon:de},{id:`claude`,tkey:`nav.claude`,Icon:fe}],Qi={light:le,dark:V,system:ue},$i={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function ea(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function ta(){let e=localStorage.getItem(Xi);return e===`light`||e===`dark`?e:`system`}function na(){let[e,t]=(0,_.useState)(qi),[n,r]=(0,_.useState)(ta),[i,a]=(0,_.useState)(null),{locale:o,setLocale:s}=H(),c=U(),[l,u]=(0,_.useState)(!1),d=(0,_.useRef)(null),f=(0,_.useRef)(null),p=(0,_.useRef)(!1);(0,_.useEffect)(()=>{let e=()=>{let e=qi(),n=window.location.hash.replace(/^#\/?/,``);if(u(!1),!Ji(n,e)){window.location.hash=e;return}t(e)};return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{Ji(window.location.hash.replace(/^#\/?/,``),e)||(window.location.hash=e)},[e]),(0,_.useEffect)(()=>{let e=document.documentElement;n===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Xi)):(e.setAttribute(`data-theme`,n),localStorage.setItem(Xi,n))},[n]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Yi}/healthz`);if(!t.ok)return;let n=ea(await t.json());!e&&n&&a(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let m=()=>r(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),h=Qi[n],g=i??`2.7.25`,[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null);(0,_.useEffect)(()=>{if(!l)return;let e=e=>{e.key===`Escape`&&u(!1)};window.addEventListener(`keydown`,e);let t=document.body.style.overflow;return document.body.style.overflow=`hidden`,()=>{window.removeEventListener(`keydown`,e),document.body.style.overflow=t}},[l]),(0,_.useEffect)(()=>{if(l){p.current=!0;let e=setTimeout(()=>f.current?.focus(),200);return()=>clearTimeout(e)}p.current&&(p.current=!1,d.current?.focus())},[l]),(0,_.useEffect)(()=>{let e=window.matchMedia(`(min-width: 761px)`),t=()=>{e.matches&&u(!1)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]),(0,_.useEffect)(()=>{let e=!1;return fetch(`${Yi}/api/claude-code`).then(e=>e.json()).then(t=>{!e&&typeof t.enabled==`boolean`&&x(t.enabled)}).catch(()=>{}),()=>{e=!0}},[]);let C=async()=>{if(b===null)return;let e=!b;x(e);try{(await fetch(`${Yi}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:e})})).ok||x(!e)}catch{x(!e)}},w=async()=>{if(confirm(c(`dash.stopConfirm`))){y(!0);try{await fetch(`${Yi}/api/stop`,{method:`POST`})}catch{}}},T=(0,S.jsxs)(`div`,{className:`brand`,children:[(0,S.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":c(`app.logoAria`)}),(0,S.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,S.jsxs)(`span`,{className:`ver`,children:[`v`,g]})]});return(0,S.jsxs)(`div`,{className:`app`,children:[(0,S.jsxs)(`header`,{className:`mobile-topbar`,inert:l,children:[(0,S.jsx)(`button`,{ref:d,type:`button`,className:`menu-toggle`,onClick:()=>u(e=>!e),"aria-expanded":l,"aria-controls":`app-sidebar`,"aria-label":c(l?`nav.closeMenu`:`nav.openMenu`),title:c(l?`nav.closeMenu`:`nav.openMenu`),children:(0,S.jsx)(k,{})}),T,(0,S.jsx)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:w,disabled:v,"aria-label":c(`dash.stop`),title:c(`dash.stop`),children:(0,S.jsx)(re,{})})]}),l&&(0,S.jsx)(`div`,{className:`drawer-scrim`,onClick:()=>u(!1),"aria-hidden":`true`}),(0,S.jsxs)(`aside`,{id:`app-sidebar`,className:`sidebar${l?` open`:``}`,ref:f,tabIndex:-1,children:[(0,S.jsxs)(`div`,{className:`drawer-head`,children:[T,(0,S.jsx)(`button`,{type:`button`,className:`menu-toggle drawer-close`,onClick:()=>u(!1),"aria-label":c(`nav.closeMenu`),title:c(`nav.closeMenu`),children:(0,S.jsx)(N,{})})]}),(0,S.jsx)(`nav`,{children:Zi.map(({id:n,tkey:r,Icon:i})=>(0,S.jsxs)(`button`,{className:`nav-item${e===n?` active`:``}`,"data-page":n,onClick:()=>{t(n),u(!1)},"aria-current":e===n?`page`:void 0,children:[(0,S.jsx)(i,{}),` `,c(r)]},n))}),(0,S.jsxs)(`div`,{className:`sidebar-foot`,children:[b!==null&&(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:C,"aria-pressed":b,"aria-label":c(`claude.toggleAria`),title:c(`claude.toggleAria`),style:b?{color:`var(--accent)`}:void 0,children:[(0,S.jsx)(fe,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c(b?`app.claudeOn`:`app.claudeOff`)})]}),(0,S.jsxs)(`div`,{className:`lang-toggle`,children:[(0,S.jsx)(de,{"aria-hidden":!0}),(0,S.jsx)(G,{value:o,options:ye.map(e=>({value:e.code,label:e.name})),onChange:e=>s(e),label:c(`lang.label`),placement:`right`,style:{flex:1,minWidth:0,width:`100%`}})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:m,"aria-label":`${c(`theme.label`)}: ${c($i[n])}`,title:`${c(`theme.label`)}: ${c($i[n])}`,children:[(0,S.jsx)(h,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c($i[n])})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:w,disabled:v,"aria-label":c(`dash.stop`),title:c(`dash.stop`),children:[(0,S.jsx)(re,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:c(v?`dash.stopping`:`dash.stop`)})]}),(0,S.jsxs)(`a`,{className:`sidebar-link`,href:`https://github.com/lidge-jun/opencodex`,target:`_blank`,rel:`noreferrer`,children:[(0,S.jsx)(B,{}),` `,c(`common.github`)]})]})]}),(0,S.jsx)(`main`,{className:`main`,inert:l,children:(0,S.jsxs)(`div`,{className:`main-inner${e===`combos`?` main-inner--combos`:``}`,children:[e===`dashboard`&&(0,S.jsx)(Ve,{apiBase:Yi}),e===`providers`&&(0,S.jsx)(Xn,{apiBase:Yi}),e===`models`&&(0,S.jsx)(dr,{apiBase:Yi}),e===`combos`&&(0,S.jsx)(Wr,{apiBase:Yi}),e===`subagents`&&(0,S.jsx)(Gr,{apiBase:Yi}),e===`logs`&&(0,S.jsx)(ii,{apiBase:Yi}),e===`debug`&&(0,S.jsx)(fi,{apiBase:Yi}),e===`usage`&&(0,S.jsx)(Ei,{apiBase:Yi}),e===`codex-auth`&&(0,S.jsx)(K,{apiBase:Yi}),e===`api`&&(0,S.jsx)(ki,{apiBase:Yi}),e===`claude`&&(0,S.jsx)(Pi,{apiBase:Yi})]})})]})}v.createRoot(document.getElementById(`root`)).render((0,S.jsx)(_.StrictMode,{children:(0,S.jsx)(we,{children:(0,S.jsx)(na,{})})}));
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BzhyTAco.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-QyUBi3W_.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-Dq3eZ1cU.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.7.24",
3
+ "version": "2.7.25",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -63,7 +63,50 @@ function extractErrorDetail(parsed: unknown): string | undefined {
63
63
  function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
64
64
  const out: unknown[] = [];
65
65
  const { context, options } = parsed;
66
- let pendingToolCallIds = new Set<string>();
66
+
67
+ // 260718 dangling tool_calls hardening (devlog/_plan/260718_dangling_toolcall_hardening):
68
+ // strict chat providers (Kimi/Moonshot) 400 when an assistant tool_call is not answered
69
+ // immediately by role:"tool" messages. Repair order: (1) reattach a real result to its
70
+ // original call (barrier messages are DEFERRED until the open tool round closes),
71
+ // (2) synthesize an explicit unavailable-result only when no real result exists,
72
+ // (3) manufacture an orphan assistant call only when no call occurrence matches at all.
73
+ // Occurrences are kept as an ordered list (never a Map) so duplicated ids survive.
74
+ interface PendingToolCall { id: string; name: string }
75
+ let pendingToolCalls: PendingToolCall[] = [];
76
+ let deferredBarrierMessages: unknown[] = [];
77
+ let mintedIdSeq = 0;
78
+ const seenWireCallIds = new Set<string>();
79
+
80
+ const mintCallId = (): string => {
81
+ let id = "";
82
+ do {
83
+ id = `call_ocx_minted_${++mintedIdSeq}`;
84
+ } while (seenWireCallIds.has(id));
85
+ seenWireCallIds.add(id);
86
+ return id;
87
+ };
88
+
89
+ const releaseDeferredBarriers = (): void => {
90
+ if (deferredBarrierMessages.length === 0) return;
91
+ out.push(...deferredBarrierMessages);
92
+ deferredBarrierMessages = [];
93
+ };
94
+
95
+ // Close an unresolved tool round with explicit unavailable-result messages. The wording
96
+ // must not claim interruption, success, failure, or user intent: execution status is
97
+ // UNKNOWN, and for user-input tools this must not read as an answer.
98
+ const flushPendingToolCalls = (): void => {
99
+ if (pendingToolCalls.length === 0) return;
100
+ for (const call of pendingToolCalls) {
101
+ out.push({
102
+ role: "tool",
103
+ tool_call_id: call.id,
104
+ content: `[ocx] no tool result was recorded for "${call.name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
105
+ });
106
+ }
107
+ pendingToolCalls = [];
108
+ releaseDeferredBarriers();
109
+ };
67
110
 
68
111
  const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
69
112
  ? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
@@ -83,22 +126,26 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
83
126
  case "user":
84
127
  case "developer": {
85
128
  const role = msg.role === "developer" ? "system" : "user";
129
+ let chatMsg: Record<string, unknown>;
86
130
  if (typeof msg.content === "string") {
87
- out.push({ role, content: msg.content });
131
+ chatMsg = { role, content: msg.content };
88
132
  } else {
89
133
  const parts = msg.content as OcxContentPart[];
90
134
  if (!parts.some(p => p.type === "image")) {
91
- out.push({ role, content: parts.map(p => (p as OcxTextContent).text).join("") });
135
+ chatMsg = { role, content: parts.map(p => (p as OcxTextContent).text).join("") };
92
136
  } else {
93
137
  // Vision: chat-completions content-parts array. Images are only valid on the user role,
94
138
  // and the data URL goes straight into image_url.url (never the token-exploding text path).
95
139
  const chatParts = parts.map(p => p.type === "image"
96
140
  ? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
97
141
  : { type: "text", text: (p as OcxTextContent).text });
98
- out.push({ role: "user", content: chatParts });
142
+ chatMsg = { role: "user", content: chatParts };
99
143
  }
100
144
  }
101
- pendingToolCallIds = new Set();
145
+ // A barrier must not split an open tool round: defer it until the round closes
146
+ // (real result arrives) or the round is synthesized shut.
147
+ if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
148
+ else out.push(chatMsg);
102
149
  break;
103
150
  }
104
151
  case "assistant": {
@@ -114,9 +161,21 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
114
161
  if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
115
162
  chatMsg.reasoning_content = reasoningContent;
116
163
  }
117
- if (toolCalls.length > 0) {
118
- chatMsg.tool_calls = toolCalls.map(tc => ({
119
- id: tc.id,
164
+ // Skip empty assistant messages: chat APIs like DeepSeek reject an assistant message
165
+ // with neither content, tool calls, nor a provider-supported reasoning_content field.
166
+ if (chatMsg.content === undefined && toolCalls.length === 0 && chatMsg.reasoning_content === undefined) break;
167
+ // A new assistant starts while a previous round is still open: close the previous
168
+ // round synthetically first so its tool_calls are never left dangling.
169
+ flushPendingToolCalls();
170
+ const wireToolCalls = toolCalls.map(tc => {
171
+ let id = tc.id;
172
+ if (!id) id = mintCallId();
173
+ else seenWireCallIds.add(id);
174
+ return { tc, id };
175
+ });
176
+ if (wireToolCalls.length > 0) {
177
+ chatMsg.tool_calls = wireToolCalls.map(({ tc, id }) => ({
178
+ id,
120
179
  type: "function",
121
180
  function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
122
181
  }));
@@ -127,19 +186,30 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
127
186
  if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
128
187
  chatMsg.content = "";
129
188
  }
130
- // Skip empty assistant messages: chat APIs like DeepSeek reject an assistant message
131
- // with neither content, tool calls, nor a provider-supported reasoning_content field.
132
- if (chatMsg.content === undefined && chatMsg.tool_calls === undefined && chatMsg.reasoning_content === undefined) break;
133
189
  out.push(chatMsg);
134
- pendingToolCallIds = new Set(toolCalls.map(tc => tc.id).filter(Boolean));
190
+ pendingToolCalls = wireToolCalls.map(({ tc, id }) => ({ id, name: namespacedToolName(tc.namespace, tc.name) }));
135
191
  break;
136
192
  }
137
193
  case "toolResult": {
138
194
  let toolCallId = msg.toolCallId;
139
- if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
140
- if (!pendingToolCallIds.has(toolCallId)) {
195
+ const matchIdx = toolCallId ? pendingToolCalls.findIndex(c => c.id === toolCallId) : -1;
196
+ if (matchIdx >= 0 && toolCallId) {
197
+ // Real result reattached to its original call. Barriers were deferred, so the
198
+ // tool message lands immediately inside the open round.
199
+ out.push({
200
+ role: "tool",
201
+ tool_call_id: toolCallId,
202
+ content: contentPartsToText(msg.content),
203
+ });
204
+ pendingToolCalls.splice(matchIdx, 1);
205
+ if (pendingToolCalls.length === 0) releaseDeferredBarriers();
206
+ } else {
207
+ if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
208
+ // No matching call in the open round. Close any unresolved round first so the
209
+ // synthesized orphan pair never splits it, then keep the historical repair:
141
210
  // WS turns can arrive with only tool outputs; chat-completions providers reject a bare
142
211
  // role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
212
+ flushPendingToolCalls();
143
213
  const name = safeToolName(msg.toolName);
144
214
  out.push({
145
215
  role: "assistant",
@@ -150,19 +220,22 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
150
220
  function: { name, arguments: "{}" },
151
221
  }],
152
222
  });
153
- pendingToolCallIds = new Set([toolCallId]);
223
+ seenWireCallIds.add(toolCallId);
224
+ out.push({
225
+ role: "tool",
226
+ tool_call_id: toolCallId,
227
+ content: contentPartsToText(msg.content),
228
+ });
154
229
  }
155
- out.push({
156
- role: "tool",
157
- tool_call_id: toolCallId,
158
- content: contentPartsToText(msg.content),
159
- });
160
- pendingToolCallIds.delete(toolCallId);
161
230
  break;
162
231
  }
163
232
  }
164
233
  }
165
234
 
235
+ // Trailing dangle: a turn interrupted after the assistant requested tools leaves the
236
+ // round open; close it synthetically (then release any deferred barriers in order).
237
+ flushPendingToolCalls();
238
+ releaseDeferredBarriers();
166
239
  return out;
167
240
  }
168
241
 
@@ -17,8 +17,10 @@ import type { OcxConfig } from "../types";
17
17
  import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
18
18
  import { resolveAutoContext, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
19
19
  import { claudeConfigDir } from "./gateway-cache";
20
- import { DEFAULT_SUBAGENT_MODELS } from "../config";
20
+ import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
21
21
  import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
22
+ import { knownModelIdsForProvider } from "../router";
23
+ import { decodeRoutedModelId } from "../providers/slug-codec";
22
24
 
23
25
  export interface ClaudeAgentDef {
24
26
  file: string;
@@ -52,12 +54,17 @@ function pickerDefaultModel(configDir: string): string | null {
52
54
  }
53
55
  }
54
56
 
55
- /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id". */
56
- function entryParts(entry: string): { alias: string; id: string; provider: string } {
57
+ /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
58
+ * Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
59
+ * so the alias joins the raw-native context-window map (context-windows.ts). */
60
+ function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
57
61
  const slash = entry.indexOf("/");
58
62
  if (slash > 0) {
59
63
  const provider = entry.slice(0, slash);
60
- const id = entry.slice(slash + 1);
64
+ const prov = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
65
+ const id = prov
66
+ ? decodeRoutedModelId(entry.slice(slash + 1), knownModelIdsForProvider(provider, prov))
67
+ : entry.slice(slash + 1);
61
68
  return { alias: claudeCodeAlias(provider, id), id, provider };
62
69
  }
63
70
  return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
@@ -101,7 +108,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
101
108
  const roster = config.subagentModels === undefined ? DEFAULT_SUBAGENT_MODELS : config.subagentModels;
102
109
  for (const entry of roster.slice(0, 5)) {
103
110
  if (typeof entry !== "string" || entry.trim() === "") continue;
104
- const { alias, id, provider } = entryParts(entry.trim());
111
+ const { alias, id, provider } = entryParts(entry.trim(), config);
105
112
  push(sanitizeName(id), alias, `Delegate work to ${id} (${provider}) via opencodex routing. General-purpose worker/explorer on that model. ${NO_MODEL_ARG}`);
106
113
  }
107
114
 
@@ -13,6 +13,7 @@ import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJa
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
16
+ import { encodeRoutedModelId, routedSlug, slugEquals, slugsEquivalent } from "../providers/slug-codec";
16
17
  import { CODEX_GPT5_IDENTITY_LINE } from "../adapters/identity";
17
18
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../adapters/cursor/discovery";
18
19
  import { fetchCursorUsableModels } from "../adapters/cursor/live-models";
@@ -276,14 +277,17 @@ export function catalogModelEfforts(slugs: readonly string[]): Map<string, strin
276
277
  if (slugs.length === 0) return out;
277
278
  const catalog = readCatalog(readCodexCatalogPath());
278
279
  if (!catalog) return out;
279
- const wanted = new Set(slugs);
280
280
  for (const entry of catalog.models ?? []) {
281
- if (typeof entry.slug !== "string" || !wanted.has(entry.slug)) continue;
281
+ if (typeof entry.slug !== "string") continue;
282
+ // Tolerate raw legacy config slugs (`provider/vendor/model`) against the
283
+ // Codex-facing encoded catalog slug (`provider/vendor-model`).
284
+ const callerSlug = slugs.find(s => slugsEquivalent(s, entry.slug as string));
285
+ if (callerSlug === undefined) continue;
282
286
  const levels = Array.isArray(entry.supported_reasoning_levels)
283
287
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
284
288
  : [];
285
289
  const efforts = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
286
- if (efforts.length > 0) out.set(entry.slug, efforts);
290
+ if (efforts.length > 0) out.set(callerSlug, efforts);
287
291
  }
288
292
  return out;
289
293
  }
@@ -533,11 +537,10 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
533
537
  return ensureStrictCatalogFields(entry);
534
538
  }
535
539
 
536
- function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string, contextCap?: number): void {
537
- const slash = slug.indexOf("/");
538
- if (slash < 0) return;
539
- const provider = slug.slice(0, slash);
540
- const modelId = slug.slice(slash + 1);
540
+ // provider + NATIVE model id are passed separately: the Codex-facing slug may carry an
541
+ // encoded alias (`provider/vendor-model`) that must never reach the metadata lookup,
542
+ // whose keys are native ids (openrouter `anthropic/...`, nvidia `moonshotai/...`).
543
+ function applyJawcodeCatalogMetadata(entry: RawEntry, provider: string, modelId: string, contextCap?: number): void {
541
544
  const jawcodeProvider = resolveJawcodeProvider(provider);
542
545
  if (!jawcodeProvider) return;
543
546
  const meta = getJawcodeModelMetadata(jawcodeProvider, modelId)
@@ -878,7 +881,9 @@ function deriveEntry(
878
881
  // Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
879
882
  // and advertise the reasoning ladder Codex accepts.
880
883
  if (slug.includes("/")) {
881
- const modelName = slug.slice(slug.indexOf("/") + 1);
884
+ // Native id for identity text + metadata lookups — the slug may be an encoded
885
+ // alias (`provider/vendor-model`); the model object carries the native id.
886
+ const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
882
887
  if (typeof e.base_instructions === "string") {
883
888
  // Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
884
889
  // (leaking that into base_instructions is a non-first-party signature → ToS risk).
@@ -889,7 +894,7 @@ function deriveEntry(
889
894
  }
890
895
  applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
891
896
  normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
892
- applyJawcodeCatalogMetadata(e, slug, model?.contextCap);
897
+ if (model) applyJawcodeCatalogMetadata(e, model.provider, model.id, model.contextCap);
893
898
  applyCatalogModelMetadata(e, model);
894
899
  } else {
895
900
  applyNativeOpenAiContextOverride(e);
@@ -925,7 +930,7 @@ function deriveEntry(
925
930
  applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
926
931
  if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
927
932
  }
928
- applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
933
+ if (model && slug.includes("/")) applyJawcodeCatalogMetadata(entry, model.provider, model.id, model.contextCap);
929
934
  applyCatalogModelMetadata(entry, model);
930
935
  applyNativeOpenAiContextOverride(entry);
931
936
  return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
@@ -953,13 +958,17 @@ export function buildCatalogEntries(
953
958
  // it sorts to the front. This works for native gpt slugs AND routed slugs alike.
954
959
  const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
955
960
  const out: RawEntry[] = [];
961
+ const collisionSkipped = resolveSlugAliasCollisions(goModels);
956
962
  for (const slug of gptSlugs) {
957
963
  const e = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
958
964
  if (rank.has(slug)) e.priority = rank.get(slug)!;
959
965
  out.push(e);
960
966
  }
961
967
  for (const m of goModels) {
962
- const slug = `${m.provider}/${m.id}`;
968
+ if (collisionSkipped.has(m)) continue;
969
+ // Codex-facing slug: exactly one "/" (slug-codec). Codex's models-manager metadata
970
+ // lookup rejects remainders containing "/", so native slash ids are aliased with "_".
971
+ const slug = routedSlug(m.provider, m.id);
963
972
  const e = deriveEntry(
964
973
  template,
965
974
  slug,
@@ -968,7 +977,9 @@ export function buildCatalogEntries(
968
977
  m,
969
978
  exactComboSlugs,
970
979
  );
971
- if (rank.has(slug)) e.priority = rank.get(slug)!;
980
+ // Featured picks may be stored raw (legacy) or encoded — honor both.
981
+ const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
982
+ if (rankHit !== undefined) e.priority = rankHit;
972
983
  out.push(e);
973
984
  }
974
985
  // Central capability override (phase 120.4): the advertised flag must match the implemented WS
@@ -1361,7 +1372,10 @@ export function filterCatalogVisibleModels(
1361
1372
  if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
1362
1373
  }
1363
1374
  return models.filter(m => {
1364
- if (disabled.has(`${m.provider}/${m.id}`)) return false;
1375
+ // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
1376
+ for (const stored of disabled) {
1377
+ if (slugEquals(stored, m.provider, m.id)) return false;
1378
+ }
1365
1379
  const allow = allowByProvider.get(m.provider);
1366
1380
  return !allow || allow.has(m.id);
1367
1381
  });
@@ -1546,12 +1560,51 @@ export function resetOpenAiApiCatalogWarningStateForTests(): void {
1546
1560
  openAiApiCollisionWarnings.clear();
1547
1561
  }
1548
1562
 
1563
+ /**
1564
+ * Encode-collision guard (slug-codec): two DISTINCT native ids of one provider mapping
1565
+ * to the same Codex-facing alias (`a/b` vs `a-b`) cannot be decoded bijectively and must
1566
+ * not emit duplicate catalog slugs. The plain-hyphen native id wins (matching decode
1567
+ * precedence: exact native match first); the loser is dropped from the catalog — it stays
1568
+ * callable via its raw full-slash selector — and we warn once per provider+alias.
1569
+ */
1570
+ const slugAliasCollisionWarnings = new Set<string>();
1571
+
1572
+ function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<CatalogModel> {
1573
+ const skipped = new Set<CatalogModel>();
1574
+ const winnerByAlias = new Map<string, CatalogModel>();
1575
+ for (const m of goModels) {
1576
+ const key = `${m.provider}/${encodeRoutedModelId(m.id)}`;
1577
+ const winner = winnerByAlias.get(key);
1578
+ if (!winner) {
1579
+ winnerByAlias.set(key, m);
1580
+ continue;
1581
+ }
1582
+ const winnerIsPlainAlias = !winner.id.includes("/");
1583
+ const currentIsPlainAlias = !m.id.includes("/");
1584
+ if (currentIsPlainAlias && !winnerIsPlainAlias) {
1585
+ skipped.add(winner);
1586
+ winnerByAlias.set(key, m);
1587
+ } else {
1588
+ skipped.add(m);
1589
+ }
1590
+ if (!slugAliasCollisionWarnings.has(key)) {
1591
+ slugAliasCollisionWarnings.add(key);
1592
+ console.warn(
1593
+ `[opencodex] slug alias collision on "${key}": multiple native ids encode to the same Codex-facing slug; `
1594
+ + "the plain-hyphen native id is cataloged, the slash id remains callable via its raw selector.",
1595
+ );
1596
+ }
1597
+ }
1598
+ return skipped;
1599
+ }
1600
+
1549
1601
  /** Test-only reset for every process-global catalog cache/warning owner. */
1550
1602
  export function resetCatalogRuntimeStateForTests(): void {
1551
1603
  bundledCatalogCache = null;
1552
1604
  lastDropWarnSignature.clear();
1553
1605
  openAiApiCollisionWarnings.clear();
1554
1606
  comboCatalogWarningSignatures.clear();
1607
+ slugAliasCollisionWarnings.clear();
1555
1608
  clearModelCache();
1556
1609
  }
1557
1610
 
@@ -1651,11 +1704,11 @@ export function augmentRoutedModelsWithJawcodeMetadata(
1651
1704
  export function orderForSubagents(goModels: CatalogModel[], featured?: string[]): CatalogModel[] {
1652
1705
  if (!featured || featured.length === 0) return goModels;
1653
1706
  const rank = new Map(featured.map((id, i) => [id, i]));
1654
- const keyOf = (m: CatalogModel) => `${m.provider}/${m.id}`;
1707
+ // Featured picks may be stored raw (legacy) or encoded — match both forms.
1708
+ const rankOf = (m: CatalogModel) =>
1709
+ rank.get(`${m.provider}/${m.id}`) ?? rank.get(routedSlug(m.provider, m.id)) ?? Number.MAX_SAFE_INTEGER;
1655
1710
  return [...goModels].sort((a, b) => {
1656
- const ra = rank.has(keyOf(a)) ? rank.get(keyOf(a))! : Number.MAX_SAFE_INTEGER;
1657
- const rb = rank.has(keyOf(b)) ? rank.get(keyOf(b))! : Number.MAX_SAFE_INTEGER;
1658
- return ra - rb;
1711
+ return rankOf(a) - rankOf(b);
1659
1712
  });
1660
1713
  }
1661
1714
 
@@ -653,8 +653,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
653
653
  // no /models endpoint is documented, and tools are silently ignored upstream per docs.parallel.ai.
654
654
  // Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
655
655
  { id: "parallel", label: "Parallel", baseUrl: "https://platform.parallel.ai", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.parallel.ai" },
656
- // FREEZE 2026-07-10: model ids remain unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
657
- { id: "zenmux", label: "ZenMux", baseUrl: "https://zenmux.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://zenmux.ai" },
656
+ // ZenMux native ids are vendor-namespaced (`<vendor>/<model>`), verified live against
657
+ // https://zenmux.ai/api/v1/models on 2026-07-18. The static seed doubles as the
658
+ // cold-cache decode source for the Codex slug codec (src/providers/slug-codec.ts);
659
+ // live discovery still owns the full catalog.
660
+ {
661
+ id: "zenmux", label: "ZenMux", baseUrl: "https://zenmux.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://zenmux.ai",
662
+ models: ["moonshotai/kimi-k3-free", "moonshotai/kimi-k3"],
663
+ },
658
664
  {
659
665
  id: "litellm", label: "LiteLLM (self-hosted)", baseUrl: "http://localhost:4000/v1", adapter: "openai-chat", authKind: "key",
660
666
  dashboardUrl: "https://docs.litellm.ai/docs/proxy/quick_start",
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Codex-facing slug codec for routed models whose NATIVE ids contain "/".
3
+ *
4
+ * Codex's models-manager resolves per-model metadata (effort ladder, context window,
5
+ * capabilities — "tagging") with an exact one-slash rule: the namespaced-suffix lookup
6
+ * (codex-rs models-manager/src/manager.rs, `find_model_by_namespaced_suffix`) splits
7
+ * once on "/" and rejects the lookup when the remainder still contains "/". Providers
8
+ * whose native ids are themselves namespaced (zenmux `moonshotai/kimi-k3-free`,
9
+ * openrouter `anthropic/...`, nvidia `moonshotai/...`, together, fireworks, …) would
10
+ * otherwise produce two-slash Codex slugs that silently fall back to default metadata.
11
+ *
12
+ * Contract:
13
+ * - Codex-facing surfaces (catalog entries, picker lists, Codex-bound config picks)
14
+ * use `routedSlug(provider, id)` — inner slashes become "-".
15
+ * - Internal state (upstream requests, logs, usage, jawcode metadata, combo keys)
16
+ * keeps the native id. Decoding is an EXACT bijective lookup against the provider's
17
+ * known native ids — never a blind "-" → "/" replace — with three ordered rules:
18
+ * native exact match (back-compat with raw full-slash selectors) > unique alias
19
+ * match > pass-through unchanged (honest upstream error).
20
+ * - Config comparisons are tolerant via `slugEquals`/`slugsEquivalent` so legacy raw
21
+ * values keep working regardless of which form was stored.
22
+ */
23
+
24
+ /** Separator standing in for "/" inside the model-id portion of a Codex-facing slug. */
25
+ export const SLUG_ALIAS_SEPARATOR = "-";
26
+
27
+ /** Native model id -> Codex-facing alias id. No-op for ids without "/". */
28
+ export function encodeRoutedModelId(id: string): string {
29
+ return id.includes("/") ? id.replaceAll("/", SLUG_ALIAS_SEPARATOR) : id;
30
+ }
31
+
32
+ /** Codex-facing routed slug: exactly one "/" — `<provider>/<encoded id>`. */
33
+ export function routedSlug(provider: string, id: string): string {
34
+ return `${provider}/${encodeRoutedModelId(id)}`;
35
+ }
36
+
37
+ /**
38
+ * Map a Codex-supplied model id back to the provider's native id.
39
+ * `knownIds` is the provider's known native ids (config ∪ registry ∪ live cache).
40
+ */
41
+ export function decodeRoutedModelId(requested: string, knownIds: Iterable<string>): string {
42
+ let aliasMatch: string | undefined;
43
+ for (const id of knownIds) {
44
+ if (id === requested) return requested; // native exact (raw selector back-compat)
45
+ if (id.includes("/") && encodeRoutedModelId(id) === requested) {
46
+ // Ambiguous alias (e.g. both `a/b` and `a-b` exist): refuse to guess.
47
+ if (aliasMatch !== undefined && aliasMatch !== id) return requested;
48
+ aliasMatch = id;
49
+ }
50
+ }
51
+ return aliasMatch ?? requested;
52
+ }
53
+
54
+ /** Does a stored config slug name this routed model, in either raw or encoded form? */
55
+ export function slugEquals(stored: string, provider: string, id: string): boolean {
56
+ return stored === `${provider}/${id}` || stored === routedSlug(provider, id);
57
+ }
58
+
59
+ /** Equivalence between two routed slugs regardless of raw/encoded mix. */
60
+ export function slugsEquivalent(a: string, b: string): boolean {
61
+ if (a === b) return true;
62
+ const pa = a.indexOf("/");
63
+ const pb = b.indexOf("/");
64
+ if (pa <= 0 || pb <= 0) return false;
65
+ if (a.slice(0, pa) !== b.slice(0, pb)) return false;
66
+ return encodeRoutedModelId(a.slice(pa + 1)) === encodeRoutedModelId(b.slice(pb + 1));
67
+ }
package/src/router.ts CHANGED
@@ -4,6 +4,8 @@ import { hasOwnProvider, resolveEnvValue } from "./config";
4
4
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
5
5
  import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
6
6
  import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
7
+ import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
8
+ import { getStaleCached } from "./codex/model-cache";
7
9
 
8
10
  export interface RouteResult {
9
11
  providerName: string;
@@ -28,6 +30,32 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
28
30
  },
29
31
  ];
30
32
 
33
+ /**
34
+ * Known native model ids for a provider — the decode source for the Codex slug codec
35
+ * (src/providers/slug-codec.ts). Union of static config ids, registry seeds, and the
36
+ * last-known-good live /models cache (may be empty on a cold start; decode then passes
37
+ * unknown ids through unchanged for an honest upstream error).
38
+ */
39
+ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
40
+ const ids = new Set<string>();
41
+ for (const id of prov.models ?? []) ids.add(id);
42
+ const registry = PROVIDER_REGISTRY.find(entry => entry.id === provName);
43
+ for (const id of registry?.models ?? []) ids.add(id);
44
+ // Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
45
+ // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
46
+ for (const map of [
47
+ registry?.modelContextWindows,
48
+ registry?.modelInputModalities,
49
+ registry?.modelReasoningEfforts,
50
+ registry?.modelDefaultReasoningEfforts,
51
+ registry?.modelReasoningEffortMap,
52
+ ]) {
53
+ for (const id of Object.keys(map ?? {})) ids.add(id);
54
+ }
55
+ for (const cached of getStaleCached(provName) ?? []) ids.add(cached.id);
56
+ return [...ids];
57
+ }
58
+
31
59
  // Merge registry-default effort maps under user values so built-in provider configs can
32
60
  // carry real upstream aliases without a disk migration. User overrides win per-key.
33
61
  function mergeRecord(
@@ -225,7 +253,9 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
225
253
  if (hasOwnProvider(config.providers, provName)) {
226
254
  const prov = config.providers[provName];
227
255
  if (prov.disabled === true) throw new Error(`Provider is disabled: ${provName}`);
228
- return routeResult(provName, prov, modelId.slice(slash + 1));
256
+ // Codex-facing alias ids (`provider/vendor-model`) decode back to the native
257
+ // slash id via an exact known-id lookup; raw full-slash selectors keep working.
258
+ return routeResult(provName, prov, decodeRoutedModelId(modelId.slice(slash + 1), knownModelIdsForProvider(provName, prov)));
229
259
  }
230
260
  }
231
261
 
@@ -236,8 +266,9 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
236
266
  }
237
267
 
238
268
  for (const [provName, prov] of activeProviderEntries(config)) {
239
- if (prov.defaultModel === modelId) {
240
- return routeResult(provName, prov, modelId);
269
+ if (prov.defaultModel === modelId
270
+ || (typeof prov.defaultModel === "string" && encodeRoutedModelId(prov.defaultModel) === modelId)) {
271
+ return routeResult(provName, prov, prov.defaultModel as string);
241
272
  }
242
273
  }
243
274
 
@@ -245,8 +276,9 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
245
276
  if (patternRoute) return patternRoute;
246
277
 
247
278
  for (const [provName, prov] of activeProviderEntries(config)) {
248
- if (prov.models && Array.isArray(prov.models) && (prov.models as string[]).includes(modelId)) {
249
- return routeResult(provName, prov, modelId);
279
+ if (prov.models && Array.isArray(prov.models)) {
280
+ const hit = (prov.models as string[]).find(id => id === modelId || encodeRoutedModelId(id) === modelId);
281
+ if (hit !== undefined) return routeResult(provName, prov, hit);
250
282
  }
251
283
  }
252
284
 
@@ -24,6 +24,7 @@ import { providerDestinationResolvedError } from "../lib/destination-policy";
24
24
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../oauth/key-providers";
25
25
  import { deriveProviderPresets } from "../providers/derive";
26
26
  import { providerCodexAccountMode } from "../providers/registry";
27
+ import { routedSlug, slugEquals } from "../providers/slug-codec";
27
28
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../providers/quota";
28
29
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
29
30
  import { clearThreadAccountMap } from "../codex/routing";
@@ -678,12 +679,13 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
678
679
  ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
679
680
  }));
680
681
  return jsonResponse([...native, ...models.map(m => {
681
- const namespaced = `${m.provider}/${m.id}`;
682
+ // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
683
+ const namespaced = routedSlug(m.provider, m.id);
682
684
  const contextCap = providerContextCap(config, m.provider);
683
685
  return {
684
686
  ...m,
685
687
  namespaced,
686
- disabled: disabled.has(namespaced),
688
+ disabled: [...disabled].some(stored => slugEquals(stored, m.provider, m.id)),
687
689
  ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
688
690
  };
689
691
  })]);
@@ -861,8 +863,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
861
863
  .filter(slug => !disabled.has(slug))
862
864
  .map(slug => ({ provider: "openai", model: slug, namespaced: slug }));
863
865
  const routedModels = models
864
- .map(m => ({ provider: m.provider, model: m.id, namespaced: `${m.provider}/${m.id}` }))
865
- .filter(m => !disabled.has(m.namespaced));
866
+ .map(m => ({ provider: m.provider, model: m.id, namespaced: routedSlug(m.provider, m.id) }))
867
+ .filter(m => ![...disabled].some(stored => slugEquals(stored, m.provider, m.model)));
866
868
  return jsonResponse({
867
869
  model: config.injectionModel ?? null,
868
870
  effort: config.injectionEffort ?? null,
@@ -938,10 +940,13 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
938
940
  // Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
939
941
  // catalog, just buried by priority. List them first so the user can feature them over routed.
940
942
  const { listCatalogNativeSlugs } = await import("../codex/catalog");
943
+ const visibleRouted = models
944
+ .filter(m => ![...disabled].some(stored => slugEquals(stored, m.provider, m.id)))
945
+ .map(m => routedSlug(m.provider, m.id));
941
946
  const available = [
942
- ...listCatalogNativeSlugs(),
943
- ...models.map(m => `${m.provider}/${m.id}`),
944
- ].filter(ns => !disabled.has(ns));
947
+ ...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
948
+ ...visibleRouted,
949
+ ];
945
950
  return jsonResponse({ chosen: config.subagentModels ?? [], available });
946
951
  }
947
952
  if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
@@ -964,10 +969,14 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
964
969
  const { buildClaudeContextWindows, effectiveModelEnv } = await import("../claude/context-windows");
965
970
  const { visibleNativeSlugs } = await import("../codex/catalog");
966
971
  const disabled = new Set(config.disabledModels ?? []);
972
+ const isDisabled = (provider: string, id: string) =>
973
+ [...disabled].some(stored => slugEquals(stored, provider, id));
967
974
  const available = [
968
- ...listCatalogNativeSlugs(),
969
- ...models.map(m => `${m.provider}/${m.id}`),
970
- ].filter(ns => !disabled.has(ns));
975
+ ...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
976
+ // Claude-facing values stay RAW native selectors (resolved inbound via routeModel,
977
+ // which accepts the raw full-slash form); only the disabled check goes tolerant.
978
+ ...models.filter(m => !isDisabled(m.provider, m.id)).map(m => `${m.provider}/${m.id}`),
979
+ ];
971
980
  const aliases: { id: string; display_name: string }[] = [];
972
981
  for (const slug of listCatalogNativeSlugs()) {
973
982
  // Readable CLI-surface alias with hash fallback (devlog 050 / audit 051 #2) —
@@ -975,7 +984,7 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
975
984
  if (!disabled.has(slug)) aliases.push({ id: claudeCodeNativeAlias(slug), display_name: `${slug} (native)` });
976
985
  }
977
986
  for (const m of models) {
978
- if (disabled.has(`${m.provider}/${m.id}`)) continue;
987
+ if (isDisabled(m.provider, m.id)) continue;
979
988
  aliases.push({ id: claudeCodeAlias(m.provider, m.id), display_name: `${m.id} (${m.provider})` });
980
989
  }
981
990
  const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models);