@bitkyc08/opencodex 2.7.24 → 2.7.25-preview.20260718
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 +5 -0
- package/README.zh-CN.md +13 -12
- package/gui/dist/assets/{index-BzhyTAco.js → index-Ce5n9e3j.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/openai-chat.ts +94 -21
- package/src/claude/agents-inject.ts +12 -5
- package/src/codex/catalog.ts +71 -18
- package/src/providers/registry.ts +8 -2
- package/src/providers/slug-codec.ts +67 -0
- package/src/router.ts +37 -5
- package/src/server/management-api.ts +20 -11
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
|
|
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
|
-
-
|
|
188
|
-
-
|
|
189
|
-
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
|
|
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-preview.20260718`,[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,{})})}));
|
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-Ce5n9e3j.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.
|
|
3
|
+
"version": "2.7.25-preview.20260718",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
chatMsg = { role: "user", content: chatParts };
|
|
99
143
|
}
|
|
100
144
|
}
|
|
101
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/codex/catalog.ts
CHANGED
|
@@ -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"
|
|
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(
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
249
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
865
|
-
.filter(m => !disabled.
|
|
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
|
-
...
|
|
944
|
-
]
|
|
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
|
-
|
|
970
|
-
|
|
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 (
|
|
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);
|