@bitkyc08/opencodex 2.6.30-preview.20260707 → 2.6.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -139,6 +139,15 @@ codex -m "ollama/llama3" "이 함수를 리팩터링해 줘"
139
139
 
140
140
  라우팅된 모델은 **Codex App** 모델 선택기에도 모델별 reasoning effort 컨트롤과 함께 나타납니다:
141
141
 
142
+ 현재 Codex 빌드는 모델이 광고하는 경우 `low`, `medium`, `high`, `xhigh`, `max` reasoning 컨트롤을
143
+ 노출할 수 있습니다. opencodex는 프로바이더 config가 명시적으로 alias를 지정하지 않는 한 `xhigh`와
144
+ `max`를 서로 다른 단계로 유지합니다.
145
+
146
+ GPT-5.6 Sol/Terra/Luna는 OpenAI API key 및 OpenRouter preset에서 rollout-ready catalog 항목으로
147
+ seed됩니다(`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`; OpenRouter는 `openai/...` 사용). 실제
148
+ 사용 가능 여부는 upstream preview gate를 따르며, opencodex는 계정/프로바이더가 제공할 때 쓸
149
+ routing/catalog metadata를 준비해 둡니다.
150
+
142
151
  <p align="center">
143
152
  <img src="assets/codex-app-picker.png" alt="opencodex 라우팅 모델을 reasoning effort 선택기와 함께 보여주는 Codex App" width="480">
144
153
  </p>
@@ -164,8 +173,9 @@ opencodex는 두 가지 동작을 분리해서 유지합니다:
164
173
  - **한 번 로그인하면 API 키는 생략.** xAI, Anthropic, Kimi는 OAuth를 지원하므로 기존 계정으로 인증할 수 있고 토큰은 자동 갱신됩니다. 또는 `codex login`을 forward 하거나, API 키를 붙여넣거나, `${ENV_VAR}` 참조를 쓸 수 있습니다 — 선택은 자유입니다.
165
174
  - **Codex가 동작하는 모든 곳에서.** Codex CLI, TUI, App, SDK에 자동으로 주입됩니다. 라우팅된 모델이 네이티브 모델처럼 Codex 모델 선택기에 나타납니다.
166
175
  - **알맞은 모델에 위임.** 대시보드나 config에서 최대 5개의 라우팅/네이티브 모델을 Codex 서브에이전트 선택기에 노출해, 복잡한 작업은 reasoning 모델로, 빠른 작업은 저렴한 모델로 보낼 수 있습니다.
176
+ - **프리뷰 게이트된 OpenAI rollout에 대비.** GPT-5.6 Sol/Terra/Luna fallback 항목은 OpenAI API key와 OpenRouter route용으로 준비되어 있으며, upstream access가 있을 때 `max` reasoning과 372k usable-context metadata를 사용합니다.
167
177
  - **어떤 모델에도 초능력을.** OpenAI가 아닌 모델도 ChatGPT 로그인 위에서 도는 `gpt-5.4-mini` sidecar로 실제 웹 검색과 이미지 이해를 사용합니다.
168
- - **무슨 일이 일어나는지 보이게.** 웹 대시보드가 프로바이더, OAuth 상태, 모델 선택, 실시간 요청 로그를 보여줍니다 — 왜 요청이 실패했는지 더는 추측하지 않아도 됩니다.
178
+ - **무슨 일이 일어나는지 보이게.** 웹 대시보드가 프로바이더, OAuth 상태, 모델 선택, upstream이 보고한 cached/cache-write 토큰 수를 포함한 실시간 요청 로그를 보여줍니다 — 왜 요청이 실패했는지 더는 추측하지 않아도 됩니다.
169
179
  - **백그라운드 실행.** 시스템 서비스(launchd / systemd / Task Scheduler)로 설치하면 부팅 시 자동 시작되어 신경 쓸 필요가 없습니다.
170
180
  - **깔끔한 종료, 잔여물 제로.** `ocx stop`(또는 대시보드의 Stop 버튼)은 프록시를 종료하고, 설치된 백그라운드 서비스를 멈추며, Codex를 원래 설정으로 복원합니다. 이후 `codex`는 잔여 설정이나 좀비 프로세스 없이 이전과 똑같이 동작합니다.
171
181
 
@@ -266,8 +276,10 @@ opencodex는 `config.json.invalid-<timestamp>`로 백업하고 경고를 출력
266
276
  프로바이더 항목은 라우팅 카탈로그 메타데이터도 함께 지정할 수 있습니다. `contextWindow`는 프로바이더
267
277
  전체에 적용되는 Codex 노출용 컨텍스트 상한, `modelContextWindows`는 모델별 상한,
268
278
  `modelInputModalities`는 `["text"]`나 `["text", "image"]` 같은 모델별 입력 힌트입니다. 이 값들은 라이브
269
- `/models` 메타데이터를 상한으로 제한할 뿐, 더 작은 라이브 컨텍스트를 늘리지는 않습니다. 전체 필드는
270
- 설정 레퍼런스를 참고하세요.
279
+ `/models` 메타데이터를 상한으로 제한할 뿐, 더 작은 라이브 컨텍스트를 늘리지는 않습니다. 번들된 GPT-5.6
280
+ Sol/Terra/Luna fallback metadata는 OpenAI API key와 OpenRouter catalog 항목에 372,000 토큰 usable
281
+ context window를 사용하며, upstream preview access를 우회하지 않습니다. 전체 필드는 설정 레퍼런스를
282
+ 참고하세요.
271
283
 
272
284
  > **Z.AI 경유 GLM-5.2 1M 컨텍스트:** `openai-chat` adapter에서는 `glm-5.2`와 `glm-5.2[1m]`이 모두
273
285
  > 동작합니다 — opencodex가 요청 전에 끝의 `[1m]` 접미사를 제거하기 때문입니다(OpenAI 호환 엔드포인트는
package/README.md CHANGED
@@ -141,6 +141,15 @@ When you omit the `provider/` prefix, opencodex routes to the default provider
141
141
 
142
142
  Routed models also appear in the **Codex App** model picker with per-model reasoning effort controls:
143
143
 
144
+ Current Codex builds can expose `low`, `medium`, `high`, `xhigh`, and `max` reasoning controls when a
145
+ model advertises them. opencodex keeps `xhigh` and `max` distinct unless a provider config explicitly
146
+ maps one to the other.
147
+
148
+ GPT-5.6 Sol/Terra/Luna are seeded as rollout-ready catalog entries for the OpenAI API key and
149
+ OpenRouter presets (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`; OpenRouter uses
150
+ `openai/...`). They remain preview-gated by upstream availability; opencodex only prepares the
151
+ routing and catalog metadata for accounts and providers that can serve them.
152
+
144
153
  <p align="center">
145
154
  <img src="assets/codex-app-picker.png" alt="Codex App showing opencodex routed models with reasoning effort picker" width="480">
146
155
  </p>
@@ -170,8 +179,9 @@ next Codex session. opencodex keeps two separate behaviors:
170
179
  - **Works everywhere Codex does.** Injects into Codex CLI, TUI, App, and SDK automatically. Routed models show up in Codex's model picker just like native ones.
171
180
  - **History-safe injection.** On local installs the proxy points Codex's own built-in `openai` provider at itself via a single `openai_base_url` line — new threads keep their native provider tag, so ongoing chat history is never remapped and an unclean shutdown can't hide it. (Threads re-tagged by older versions are migrated back once on the first start; remote/LAN binds use a dedicated provider entry instead, since they need an API-key header.)
172
181
  - **Delegate to the right model.** Feature up to five routed or native models in Codex's subagent picker from the dashboard or config — route complex tasks to a reasoning model, fast tasks to a cheap one.
182
+ - **Prepare for preview-gated OpenAI rollouts.** GPT-5.6 Sol/Terra/Luna fallback entries are ready for OpenAI API key and OpenRouter routes, with `max` reasoning and 372k usable-context metadata when upstream access is available.
173
183
  - **Give any model superpowers.** Non-OpenAI models get real web search and image understanding via a `gpt-5.4-mini` sidecar over your ChatGPT login.
174
- - **See what's happening.** The web dashboard shows providers, OAuth status, model selection, and a live request log — no more guessing why a request failed.
184
+ - **See what's happening.** The web dashboard shows providers, OAuth status, model selection, and a live request log, including cached/cache-write token counts when upstream reports them — no more guessing why a request failed.
175
185
  - **Runs in the background.** Install as a system service (launchd / systemd / Task Scheduler) and forget about it. The proxy starts on boot and stays out of your way.
176
186
  - **Clean exit, zero residue.** `ocx stop` (or the dashboard's Stop button) shuts down the proxy, stops the background service if one is installed, and restores Codex to its original configuration. Plain `codex` works exactly as it did before — no leftover config, no orphaned processes.
177
187
 
@@ -286,7 +296,9 @@ Provider entries can also annotate routed catalog metadata. Use `contextWindow`
286
296
  Codex-visible context cap, `modelContextWindows` for model-specific caps, and
287
297
  `modelInputModalities` for model-specific catalog input hints such as `["text"]` or
288
298
  `["text", "image"]`. Context values cap live `/models` metadata; they never raise a smaller live
289
- context window. See the configuration reference for the full field list.
299
+ context window. The bundled GPT-5.6 Sol/Terra/Luna fallback metadata uses a 372,000-token usable
300
+ context window for OpenAI API key and OpenRouter catalog entries; it does not bypass upstream preview
301
+ access. See the configuration reference for the full field list.
290
302
 
291
303
  > **GLM-5.2 1M context via Z.AI:** through the `openai-chat` adapter, both `glm-5.2`
292
304
  > and `glm-5.2[1m]` work — opencodex strips the trailing `[1m]` suffix before
package/README.zh-CN.md CHANGED
@@ -100,8 +100,9 @@ npm install -g @bitkyc08/opencodex # 不要加 --ignore-scripts、--omit=optio
100
100
  - **登录一次,免填 API key。** xAI、Anthropic、Kimi 支持 OAuth,可用现有账户认证,token 自动刷新。也可以转发 `codex login`、粘贴 API key,或使用 `${ENV_VAR}` 引用 —— 随你选择。
101
101
  - **Codex 在哪里能用,它就在哪里能用。** 自动注入 Codex CLI、TUI、App 和 SDK。路由模型像原生模型一样出现在 Codex 的模型选择器里。
102
102
  - **委派给合适的模型。** 在仪表盘或 config 中把最多 5 个路由/原生模型放进 Codex 的 subagent 选择器 —— 复杂任务交给 reasoning 模型,快速任务交给便宜模型。
103
+ - **为 preview-gated OpenAI rollout 做好准备。** GPT-5.6 Sol/Terra/Luna fallback 条目已可用于 OpenAI API key 和 OpenRouter 路由;当上游访问可用时,会带有 `max` reasoning 和 372k usable-context 元数据。
103
104
  - **给任意模型超能力。** 非 OpenAI 模型也能通过你的 ChatGPT 登录上运行的 `gpt-5.4-mini` sidecar 获得真正的网页搜索和图片理解。
104
- - **看清正在发生什么。** Web 仪表盘展示 provider、OAuth 状态、模型选择和实时请求日志 —— 不必再猜测请求为何失败。
105
+ - **看清正在发生什么。** Web 仪表盘展示 provider、OAuth 状态、模型选择和实时请求日志;当上游返回时,也会包含 cached/cache-write token 计数 —— 不必再猜测请求为何失败。
105
106
  - **后台运行。** 安装为系统服务(launchd / systemd / Task Scheduler)后开机自启,无需操心。
106
107
  - **干净退出,零残留。** `ocx stop`(或仪表盘的 Stop 按钮)会关闭代理、停止已安装的后台服务,并将 Codex 恢复为原始配置。之后 `codex` 就像从未安装过 opencodex 一样工作 —— 无残留配置,无僵尸进程。
107
108
 
@@ -147,6 +148,13 @@ codex -m "ollama/llama3" "重构这个函数"
147
148
 
148
149
  路由模型也会出现在 **Codex App** 模型选择器中,并带有按模型的 reasoning effort 控制:
149
150
 
151
+ 当前 Codex 构建在模型声明支持时可显示 `low`、`medium`、`high`、`xhigh` 和 `max` reasoning 控制。
152
+ 除非 provider config 明确设置 alias,opencodex 会把 `xhigh` 与 `max` 保持为不同档位。
153
+
154
+ GPT-5.6 Sol/Terra/Luna 已在 OpenAI API key 和 OpenRouter 预设中作为 rollout-ready 目录条目预置
155
+ (`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna`;OpenRouter 使用 `openai/...`)。可用性仍受上游
156
+ preview gate 限制;opencodex 只是准备好你的账户/provider 可访问时所需的路由和目录元数据。
157
+
150
158
  <p align="center">
151
159
  <img src="assets/codex-app-picker.png" alt="Codex App 展示 opencodex 路由模型及 reasoning effort 选择器" width="480">
152
160
  </p>
@@ -256,8 +264,9 @@ npm uninstall -g @bitkyc08/opencodex
256
264
 
257
265
  provider 条目还可以标注路由目录元数据。`contextWindow` 设置 provider 级别、对 Codex 可见的上下文上限,
258
266
  `modelContextWindows` 设置按模型的上限,`modelInputModalities` 设置按模型的目录输入提示,例如 `["text"]`
259
- 或 `["text", "image"]`。这些值只会对实时 `/models` 元数据设上限,绝不会抬高更小的实时上下文窗口。完整字段
260
- 参阅配置参考。
267
+ 或 `["text", "image"]`。这些值只会对实时 `/models` 元数据设上限,绝不会抬高更小的实时上下文窗口。内置
268
+ GPT-5.6 Sol/Terra/Luna fallback 元数据会为 OpenAI API key 和 OpenRouter 目录条目使用 372,000 token 的
269
+ usable context window;它不会绕过上游 preview access。完整字段参阅配置参考。
261
270
 
262
271
  > **通过 Z.AI 使用 GLM-5.2 1M 上下文:** 在 `openai-chat` adapter 下,`glm-5.2` 和 `glm-5.2[1m]` 都可用 ——
263
272
  > opencodex 会在发送请求前剥离末尾的 `[1m]` 后缀,因为 OpenAI 兼容端点会拒绝带方括号的 id(Z.AI 400 code
@@ -12,4 +12,4 @@ Error generating stack: `+e.message+`
12
12
  -d '{
13
13
  "model": "gpt-5.4",
14
14
  "input": "Hello, world!"
15
- }'`})]})]})}var xt=`opencodex-api-token`,St=!1,Ct=null;function wt(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function Tt(e){let t=wt(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Et(){try{return sessionStorage.getItem(xt)?.trim()||null}catch{return null}}function Dt(e){try{sessionStorage.setItem(xt,e)}catch{}}function Ot(){try{sessionStorage.removeItem(xt)}catch{}}function kt(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 At(){return Ct||(Ct=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Ct=null}),Ct)}function jt(){if(St)return;St=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!Tt(t))return e(t,n);let r=Et(),[i,a]=r?kt(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Ot();let s=await At();if(!s)return o;Dt(s);let[c,l]=kt(t,n,s),u=await e(c,l);return u.status===401&&Ot(),u}}jt();var Mt=new Set([`dashboard`,`providers`,`models`,`subagents`,`logs`,`usage`,`codex-auth`,`api`]);function Nt(){let e=location.hash.replace(/^#\/?/,``);return Mt.has(e)?e:`dashboard`}var Pt=``,Ft=`ocx-theme`,It=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`subagents`,tkey:`nav.subagents`,Icon:ee},{id:`logs`,tkey:`nav.logs`,Icon:D},{id:`usage`,tkey:`nav.usage`,Icon:te},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:L},{id:`api`,tkey:`nav.api`,Icon:de}],Lt={light:ce,dark:le,system:ue},Rt={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function zt(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 Bt(){let e=localStorage.getItem(Ft);return e===`light`||e===`dark`?e:`system`}function Vt(){let[e,t]=(0,_.useState)(Nt),n=e=>{location.hash=e,t(e)},[r,i]=(0,_.useState)(Bt),[a,o]=(0,_.useState)(null),{locale:s,setLocale:c}=be(),l=xe();(0,_.useEffect)(()=>{let e=()=>t(Nt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{let e=document.documentElement;r===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ft)):(e.setAttribute(`data-theme`,r),localStorage.setItem(Ft,r))},[r]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Pt}/healthz`);if(!t.ok)return;let n=zt(await t.json());!e&&n&&o(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let u=()=>i(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),d=Lt[r],f=a??`2.6.30-preview.20260707`,p=me.find(e=>e.code===s)?.name??`English`,m=()=>{let e=me.map(e=>e.code);c(e[(e.indexOf(s)+1)%e.length])},[h,g]=(0,_.useState)(!1);return(0,S.jsxs)(`div`,{className:`app`,children:[(0,S.jsxs)(`aside`,{className:`sidebar`,children:[(0,S.jsxs)(`div`,{className:`brand`,children:[(0,S.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":`opencodex logo`}),(0,S.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,S.jsxs)(`span`,{className:`ver`,children:[`v`,f]})]}),(0,S.jsx)(`nav`,{children:It.map(({id:t,tkey:r,Icon:i})=>(0,S.jsxs)(`button`,{className:`nav-item${e===t?` active`:``}`,"data-page":t,onClick:()=>n(t),"aria-current":e===t?`page`:void 0,children:[(0,S.jsx)(i,{}),` `,l(r)]},t))}),(0,S.jsxs)(`div`,{className:`sidebar-foot`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:m,"aria-label":`${l(`lang.label`)}: ${p}`,title:`${l(`lang.label`)}: ${p}`,children:[(0,S.jsx)(de,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:p})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:u,"aria-label":`${l(`theme.label`)}: ${l(Rt[r])}`,title:`${l(`theme.label`)}: ${l(Rt[r])}`,children:[(0,S.jsx)(d,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(Rt[r])})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:async()=>{if(confirm(l(`dash.stopConfirm`))){g(!0);try{await fetch(`${Pt}/api/stop`,{method:`POST`})}catch{}}},disabled:h,"aria-label":l(`dash.stop`),title:l(`dash.stop`),children:[(0,S.jsx)(F,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(h?`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)(P,{}),` `,l(`common.github`)]})]})]}),(0,S.jsx)(`main`,{className:`main`,children:(0,S.jsxs)(`div`,{className:`main-inner`,children:[e===`dashboard`&&(0,S.jsx)(Oe,{apiBase:Pt}),e===`providers`&&(0,S.jsx)(We,{apiBase:Pt}),e===`models`&&(0,S.jsx)(Ye,{apiBase:Pt}),e===`subagents`&&(0,S.jsx)(Xe,{apiBase:Pt}),e===`logs`&&(0,S.jsx)(ot,{apiBase:Pt}),e===`usage`&&(0,S.jsx)(pt,{apiBase:Pt}),e===`codex-auth`&&(0,S.jsx)(ht,{apiBase:Pt}),e===`api`&&(0,S.jsx)(bt,{apiBase:Pt})]})})]})}v.createRoot(document.getElementById(`root`)).render((0,S.jsx)(_.StrictMode,{children:(0,S.jsx)(ye,{children:(0,S.jsx)(Vt,{})})}));
15
+ }'`})]})]})}var xt=`opencodex-api-token`,St=!1,Ct=null;function wt(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function Tt(e){let t=wt(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Et(){try{return sessionStorage.getItem(xt)?.trim()||null}catch{return null}}function Dt(e){try{sessionStorage.setItem(xt,e)}catch{}}function Ot(){try{sessionStorage.removeItem(xt)}catch{}}function kt(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 At(){return Ct||(Ct=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Ct=null}),Ct)}function jt(){if(St)return;St=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!Tt(t))return e(t,n);let r=Et(),[i,a]=r?kt(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Ot();let s=await At();if(!s)return o;Dt(s);let[c,l]=kt(t,n,s),u=await e(c,l);return u.status===401&&Ot(),u}}jt();var Mt=new Set([`dashboard`,`providers`,`models`,`subagents`,`logs`,`usage`,`codex-auth`,`api`]);function Nt(){let e=location.hash.replace(/^#\/?/,``);return Mt.has(e)?e:`dashboard`}var Pt=``,Ft=`ocx-theme`,It=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`subagents`,tkey:`nav.subagents`,Icon:ee},{id:`logs`,tkey:`nav.logs`,Icon:D},{id:`usage`,tkey:`nav.usage`,Icon:te},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:L},{id:`api`,tkey:`nav.api`,Icon:de}],Lt={light:ce,dark:le,system:ue},Rt={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function zt(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 Bt(){let e=localStorage.getItem(Ft);return e===`light`||e===`dark`?e:`system`}function Vt(){let[e,t]=(0,_.useState)(Nt),n=e=>{location.hash=e,t(e)},[r,i]=(0,_.useState)(Bt),[a,o]=(0,_.useState)(null),{locale:s,setLocale:c}=be(),l=xe();(0,_.useEffect)(()=>{let e=()=>t(Nt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{let e=document.documentElement;r===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ft)):(e.setAttribute(`data-theme`,r),localStorage.setItem(Ft,r))},[r]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Pt}/healthz`);if(!t.ok)return;let n=zt(await t.json());!e&&n&&o(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let u=()=>i(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),d=Lt[r],f=a??`2.6.32`,p=me.find(e=>e.code===s)?.name??`English`,m=()=>{let e=me.map(e=>e.code);c(e[(e.indexOf(s)+1)%e.length])},[h,g]=(0,_.useState)(!1);return(0,S.jsxs)(`div`,{className:`app`,children:[(0,S.jsxs)(`aside`,{className:`sidebar`,children:[(0,S.jsxs)(`div`,{className:`brand`,children:[(0,S.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":`opencodex logo`}),(0,S.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,S.jsxs)(`span`,{className:`ver`,children:[`v`,f]})]}),(0,S.jsx)(`nav`,{children:It.map(({id:t,tkey:r,Icon:i})=>(0,S.jsxs)(`button`,{className:`nav-item${e===t?` active`:``}`,"data-page":t,onClick:()=>n(t),"aria-current":e===t?`page`:void 0,children:[(0,S.jsx)(i,{}),` `,l(r)]},t))}),(0,S.jsxs)(`div`,{className:`sidebar-foot`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:m,"aria-label":`${l(`lang.label`)}: ${p}`,title:`${l(`lang.label`)}: ${p}`,children:[(0,S.jsx)(de,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:p})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:u,"aria-label":`${l(`theme.label`)}: ${l(Rt[r])}`,title:`${l(`theme.label`)}: ${l(Rt[r])}`,children:[(0,S.jsx)(d,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(Rt[r])})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:async()=>{if(confirm(l(`dash.stopConfirm`))){g(!0);try{await fetch(`${Pt}/api/stop`,{method:`POST`})}catch{}}},disabled:h,"aria-label":l(`dash.stop`),title:l(`dash.stop`),children:[(0,S.jsx)(F,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(h?`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)(P,{}),` `,l(`common.github`)]})]})]}),(0,S.jsx)(`main`,{className:`main`,children:(0,S.jsxs)(`div`,{className:`main-inner`,children:[e===`dashboard`&&(0,S.jsx)(Oe,{apiBase:Pt}),e===`providers`&&(0,S.jsx)(We,{apiBase:Pt}),e===`models`&&(0,S.jsx)(Ye,{apiBase:Pt}),e===`subagents`&&(0,S.jsx)(Xe,{apiBase:Pt}),e===`logs`&&(0,S.jsx)(ot,{apiBase:Pt}),e===`usage`&&(0,S.jsx)(pt,{apiBase:Pt}),e===`codex-auth`&&(0,S.jsx)(ht,{apiBase:Pt}),e===`api`&&(0,S.jsx)(bt,{apiBase:Pt})]})})]})}v.createRoot(document.getElementById(`root`)).render((0,S.jsx)(_.StrictMode,{children:(0,S.jsx)(ye,{children:(0,S.jsx)(Vt,{})})}));
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-D6YMHYXe.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-D_JZzI0r.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-ByGC8-Bm.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.6.30-preview.20260707",
3
+ "version": "2.6.32",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -331,9 +331,16 @@ function buildToolNameTransforms(provider: OcxProviderConfig): { toWire: (name:
331
331
  function toAnthropicToolResult(msg: OcxToolResultMessage): Record<string, unknown> {
332
332
  // Anthropic tool_result accepts a string OR content blocks — render images natively
333
333
  // (e.g. Codex view_image output) instead of dropping them.
334
- const content = typeof msg.content === "string"
335
- ? msg.content
336
- : (msg.content as OcxContentPart[]).map(toAnthropicContentPart);
334
+ let content: string | unknown[];
335
+ if (typeof msg.content === "string") {
336
+ // Anthropic rejects tool_result with empty text content blocks.
337
+ content = msg.content || "(empty tool output)";
338
+ } else {
339
+ const parts = (msg.content as OcxContentPart[])
340
+ .map(toAnthropicContentPart)
341
+ .filter(p => !((p as { type?: string }).type === "text" && !(p as { text?: string }).text));
342
+ content = parts.length > 0 ? parts : "(empty tool output)";
343
+ }
337
344
  return {
338
345
  type: "tool_result",
339
346
  tool_use_id: msg.toolCallId,
@@ -370,9 +377,16 @@ function messagesToAnthropicFormat(
370
377
  switch (msg.role) {
371
378
  case "user":
372
379
  case "developer": {
373
- const content = typeof msg.content === "string"
374
- ? msg.content
375
- : (msg.content as OcxContentPart[]).map(toAnthropicContentPart);
380
+ let content: string | unknown[];
381
+ if (typeof msg.content === "string") {
382
+ // Anthropic rejects empty string text content blocks.
383
+ content = msg.content || "(empty)";
384
+ } else {
385
+ const parts = (msg.content as OcxContentPart[])
386
+ .map(toAnthropicContentPart)
387
+ .filter(p => !((p as { type?: string }).type === "text" && !(p as { text?: string }).text));
388
+ content = parts.length > 0 ? parts : "(empty)";
389
+ }
376
390
  messages.push({ role: "user", content });
377
391
  break;
378
392
  }
@@ -382,7 +396,8 @@ function messagesToAnthropicFormat(
382
396
  const toolUseIds: string[] = [];
383
397
  for (const part of aMsg.content) {
384
398
  if (part.type === "text") {
385
- content.push({ type: "text", text: (part as OcxTextContent).text });
399
+ const text = (part as OcxTextContent).text;
400
+ if (text) content.push({ type: "text", text });
386
401
  } else if (part.type === "thinking") {
387
402
  const t = part as OcxThinkingContent;
388
403
  // Redacted blocks replay verbatim FIRST (they preceded the visible thinking block
@@ -180,7 +180,7 @@ function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
180
180
  low: 0.20,
181
181
  medium: 0.50,
182
182
  high: 0.80,
183
- xhigh: 0.95,
183
+ xhigh: 0.90,
184
184
  max: 0.95,
185
185
  };
186
186
  const ratio = percent[effort];
package/src/bridge.ts CHANGED
@@ -20,8 +20,15 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
20
20
  output_tokens: usage.outputTokens,
21
21
  total_tokens: usageDisplayTotalTokens(usage) ?? inputTokens + usage.outputTokens,
22
22
  };
23
+ const inputDetails: Record<string, number> = {};
23
24
  if (usage.cachedInputTokens !== undefined) {
24
- out.input_tokens_details = { cached_tokens: usage.cachedInputTokens };
25
+ inputDetails.cached_tokens = usage.cachedInputTokens;
26
+ }
27
+ if (usage.cacheCreationInputTokens !== undefined) {
28
+ inputDetails.cache_write_tokens = usage.cacheCreationInputTokens;
29
+ }
30
+ if (Object.keys(inputDetails).length > 0) {
31
+ out.input_tokens_details = inputDetails;
25
32
  }
26
33
  if (usage.reasoningOutputTokens !== undefined) {
27
34
  out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens };
@@ -44,7 +44,10 @@ function isCredentialRecord(value: unknown): value is CodexAccountCredentialReco
44
44
  && (value.credential === undefined || isCredential(value.credential))
45
45
  && (value.refreshGrantFingerprint === undefined || typeof value.refreshGrantFingerprint === "string")
46
46
  && (value.deletedAt === undefined || typeof value.deletedAt === "number")
47
- && (value.replacedAt === undefined || typeof value.replacedAt === "number");
47
+ && (value.replacedAt === undefined || typeof value.replacedAt === "number")
48
+ && (value.lastCodexValidatedAt === undefined || typeof value.lastCodexValidatedAt === "number")
49
+ && (value.lastCodexValidationStatus === undefined || value.lastCodexValidationStatus === "ok" || value.lastCodexValidationStatus === "failed")
50
+ && (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string");
48
51
  }
49
52
 
50
53
  export function refreshGrantFingerprintForToken(refreshToken: string): string {
@@ -98,6 +101,17 @@ function persist(store: CodexAccountStore): void {
98
101
  atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
99
102
  }
100
103
 
104
+ function preservedValidationMetadata(record: CodexAccountCredentialRecord | undefined): Pick<
105
+ CodexAccountCredentialRecord,
106
+ "lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError"
107
+ > {
108
+ return {
109
+ ...(record?.lastCodexValidatedAt !== undefined ? { lastCodexValidatedAt: record.lastCodexValidatedAt } : {}),
110
+ ...(record?.lastCodexValidationStatus !== undefined ? { lastCodexValidationStatus: record.lastCodexValidationStatus } : {}),
111
+ ...(record?.lastCodexValidationError !== undefined ? { lastCodexValidationError: record.lastCodexValidationError } : {}),
112
+ };
113
+ }
114
+
101
115
  export function getCodexAccountCredential(id: string): CodexAccountCredentials | null {
102
116
  const record = readCodexAccountRecord(id);
103
117
  if (!record || record.deletedAt != null) return null;
@@ -115,6 +129,32 @@ export function saveCodexAccountCredential(id: string, cred: CodexAccountCredent
115
129
  generation: (current?.generation ?? 0) + 1,
116
130
  refreshGrantFingerprint,
117
131
  replacedAt: current ? Date.now() : undefined,
132
+ ...preservedValidationMetadata(current),
133
+ };
134
+ persist(store);
135
+ }
136
+
137
+ export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
138
+ const store = loadCodexAccountRecordStore();
139
+ const current = store[id];
140
+ if (!current || current.deletedAt != null || !current.credential) return;
141
+ store[id] = {
142
+ ...current,
143
+ lastCodexValidatedAt: atMs,
144
+ lastCodexValidationStatus: "ok",
145
+ lastCodexValidationError: undefined,
146
+ };
147
+ persist(store);
148
+ }
149
+
150
+ export function markCodexAccountValidationFailed(id: string, reason: string): void {
151
+ const store = loadCodexAccountRecordStore();
152
+ const current = store[id];
153
+ if (!current || current.deletedAt != null || !current.credential) return;
154
+ store[id] = {
155
+ ...current,
156
+ lastCodexValidationStatus: "failed",
157
+ lastCodexValidationError: reason,
118
158
  };
119
159
  persist(store);
120
160
  }
@@ -154,6 +194,7 @@ export function saveCodexAccountCredentialIfGeneration(
154
194
  generation: generation + 1,
155
195
  refreshGrantFingerprint,
156
196
  replacedAt: current.replacedAt,
197
+ ...preservedValidationMetadata(current),
157
198
  };
158
199
  persist(store);
159
200
  return true;
@@ -3,6 +3,7 @@ import { withCodexAccountLogLabel } from "./account-label";
3
3
  import {
4
4
  getCodexAccountCredential,
5
5
  getValidCodexToken,
6
+ markCodexAccountValidated,
6
7
  saveCodexAccountCredential,
7
8
  CodexCredentialGenerationConflictError,
8
9
  CodexCredentialRefreshLockTimeoutError,
@@ -26,6 +27,7 @@ export { clearAccountQuota, getAccountQuota, parseUsageQuota, updateAccountQuota
26
27
  import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
27
28
  import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
28
29
  import { maskEmail } from "../lib/privacy";
30
+ import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
29
31
  export { maskEmail } from "../lib/privacy";
30
32
  import type { CodexAccount, OcxConfig } from "../types";
31
33
 
@@ -138,6 +140,27 @@ function manualImportDisabledResponse(): Response {
138
140
  }, 403);
139
141
  }
140
142
 
143
+ async function verifyCodexAccountWarmup(
144
+ accountId: string,
145
+ accessToken: string,
146
+ chatgptAccountId: string,
147
+ ): Promise<{ ok: true; validatedAt: number } | { ok: false; response: Response }> {
148
+ try {
149
+ await warmCodexAccount({ accessToken, chatgptAccountId });
150
+ return { ok: true, validatedAt: Date.now() };
151
+ } catch (err) {
152
+ return {
153
+ ok: false,
154
+ response: jsonResponse({
155
+ error: "Codex account warmup failed. Reauthenticate the account and try again.",
156
+ code: "codex_warmup_failed",
157
+ reason: codexWarmupFailureReason(err),
158
+ accountId,
159
+ }, 401),
160
+ };
161
+ }
162
+ }
163
+
141
164
  function expireCodexAuthFlow(flowId: string | null, error = "Login cancelled"): void {
142
165
  if (!flowId) return;
143
166
  codexAuthLoginState.set(flowId, { status: "error", error, doneAt: Date.now() });
@@ -385,12 +408,15 @@ export async function handleCodexAuthAPI(
385
408
  // 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
386
409
  const payload = decodeJwtPayload(body.accessToken);
387
410
  const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
411
+ const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
412
+ if (!warmup.ok) return warmup.response;
388
413
  saveCodexAccountCredential(body.id, {
389
414
  accessToken: body.accessToken,
390
415
  refreshToken: body.refreshToken,
391
416
  expiresAt: exp,
392
417
  chatgptAccountId: derivedAccountId,
393
418
  });
419
+ markCodexAccountValidated(body.id, warmup.validatedAt);
394
420
  clearAccountNeedsReauth(body.id);
395
421
  accounts.push(withCodexAccountLogLabel({ id: body.id, email: body.email, plan: body.plan, isMain: false }, accounts));
396
422
  runtimeConfig.codexAccounts = accounts;
@@ -590,12 +616,25 @@ export async function handleCodexAuthAPI(
590
616
  break;
591
617
  }
592
618
 
619
+ const warmup = await verifyCodexAccountWarmup(accountId, cred.access, oauthAccountId);
620
+ if (!warmup.ok) {
621
+ const body = await warmup.response.json().catch(() => ({})) as { error?: string; reason?: string };
622
+ codexAuthLoginState.set(flowId, {
623
+ status: "error",
624
+ error: body.reason ? `${body.error ?? "Codex account warmup failed"} (${body.reason})` : body.error ?? "Codex account warmup failed",
625
+ doneAt: Date.now(),
626
+ });
627
+ completed = true;
628
+ break;
629
+ }
630
+
593
631
  saveCodexAccountCredential(accountId, {
594
632
  accessToken: cred.access,
595
633
  refreshToken: cred.refresh,
596
634
  expiresAt: cred.expires,
597
635
  chatgptAccountId: oauthAccountId,
598
636
  });
637
+ markCodexAccountValidated(accountId, warmup.validatedAt);
599
638
  clearAccountNeedsReauth(accountId);
600
639
  if (quota) {
601
640
  updateAccountQuota(
@@ -78,9 +78,13 @@ function isDefaultCatalogPath(path: string): boolean {
78
78
  */
79
79
  export const NATIVE_OPENAI_MODELS = [
80
80
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
81
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
81
82
  ];
82
83
 
83
- const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = ["gpt-5.3-codex-spark"];
84
+ const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
85
+ "gpt-5.3-codex-spark",
86
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
87
+ ];
84
88
 
85
89
  /**
86
90
  * The ONLY native OpenAI/Codex slugs opencodex advertises. A user's installed Codex ships extra
@@ -102,10 +106,15 @@ function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
102
106
  return /^(?:gpt|codex)-/.test(slug);
103
107
  }
104
108
 
109
+ const NATIVE_GPT56_CONTEXT_WINDOW = 372_000;
110
+
105
111
  const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
106
112
  "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
107
113
  "gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
108
114
  "gpt-5.3-codex-spark": { contextWindow: 128_000, maxContextWindow: 128_000 },
115
+ "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
116
+ "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
117
+ "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
109
118
  };
110
119
 
111
120
  /**
@@ -466,8 +475,8 @@ export function loadCatalogTemplate(): RawEntry | null {
466
475
  }
467
476
 
468
477
  /**
469
- * Codex only accepts its native labels in the catalog. Provider-specific wire values (e.g. Z.AI
470
- * `max`) are mapped at request time by src/reasoning-effort.ts, never advertised directly here.
478
+ * Codex accepts its native labels plus model-defined effort strings such as `max` in current builds.
479
+ * Provider-specific aliases still map at request time by src/reasoning-effort.ts.
471
480
  */
472
481
  const ROUTED_REASONING_LEVELS = CODEX_REASONING_LEVELS;
473
482
 
@@ -501,6 +510,20 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
501
510
  entry.default_reasoning_level = efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
502
511
  }
503
512
 
513
+ function isGpt56NativeSlug(slug: string): boolean {
514
+ return !slug.includes("/") && slug.startsWith("gpt-5.6-");
515
+ }
516
+
517
+ function ensureMaxReasoningLevel(entry: RawEntry): void {
518
+ const levels = Array.isArray(entry.supported_reasoning_levels)
519
+ ? entry.supported_reasoning_levels as Array<{ effort?: string }>
520
+ : [];
521
+ if (levels.some(level => level.effort === "max")) return;
522
+ const maxLevel = ROUTED_REASONING_LEVELS.find(level => level.effort === "max")
523
+ ?? { effort: "max", description: "Maximum reasoning for the hardest problems" };
524
+ entry.supported_reasoning_levels = [...levels, maxLevel];
525
+ }
526
+
504
527
  function deriveEntry(template: RawEntry | null, slug: string, desc: string, priority: number, model?: CatalogModel): RawEntry {
505
528
  if (template) {
506
529
  const e = JSON.parse(JSON.stringify(template)) as RawEntry;
@@ -512,7 +535,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
512
535
  if ("upgrade" in e) e.upgrade = null;
513
536
  delete e.availability_nux; // don't replay another model's "now available" NUX
514
537
  // Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
515
- // and advertise the reasoning ladder Codex accepts (low/medium/high/xhigh).
538
+ // and advertise the reasoning ladder Codex accepts.
516
539
  if (slug.includes("/")) {
517
540
  const modelName = slug.slice(slug.indexOf("/") + 1);
518
541
  if (typeof e.base_instructions === "string") {
@@ -529,6 +552,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
529
552
  applyCatalogModelMetadata(e, model);
530
553
  } else {
531
554
  applyNativeOpenAiContextOverride(e);
555
+ if (isGpt56NativeSlug(slug)) ensureMaxReasoningLevel(e);
532
556
  }
533
557
  return ensureStrictCatalogFields(normalizeServiceTiers(e));
534
558
  }
@@ -540,7 +564,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
540
564
  ...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
541
565
  };
542
566
  if (slug.includes("/")) applyReasoningLevels(entry, model?.reasoningEfforts);
543
- else applyReasoningLevels(entry);
567
+ else applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
544
568
  applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
545
569
  applyCatalogModelMetadata(entry, model);
546
570
  applyNativeOpenAiContextOverride(entry);
@@ -744,8 +768,8 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
744
768
  const reasoningEfforts = capabilities && typeof capabilities.reasoning_effort === "boolean"
745
769
  ? (capabilities.reasoning_effort
746
770
  ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
747
- ? ["low", "medium", "high", "xhigh"]
748
- : ["low", "medium", "high"])
771
+ ? ["low", "medium", "high", "xhigh", "max"]
772
+ : ["low", "medium", "high", "xhigh", "max"])
749
773
  : [])
750
774
  : undefined;
751
775
  const inputModalities = capabilities && typeof capabilities.vision === "boolean"
@@ -0,0 +1,137 @@
1
+ export class CodexWarmupError extends Error {
2
+ code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "invalid_sse" | "no_terminal" | "transport";
3
+ status?: number;
4
+
5
+ constructor(
6
+ code: CodexWarmupError["code"],
7
+ message = "Codex warmup failed",
8
+ options: { status?: number; cause?: unknown } = {},
9
+ ) {
10
+ super(message);
11
+ this.name = "CodexWarmupError";
12
+ this.code = code;
13
+ this.status = options.status;
14
+ if (options.cause !== undefined) this.cause = options.cause;
15
+ }
16
+ }
17
+
18
+ export interface CodexWarmupOptions {
19
+ accessToken: string;
20
+ chatgptAccountId: string;
21
+ model?: string;
22
+ timeoutMs?: number;
23
+ }
24
+
25
+ const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
26
+ const DEFAULT_MODEL = "gpt-5.4-mini";
27
+ const DEFAULT_TIMEOUT_MS = 30_000;
28
+
29
+ function safeWarmupReason(err: unknown): string {
30
+ if (err instanceof CodexWarmupError) {
31
+ return err.status ? `${err.code}:${err.status}` : err.code;
32
+ }
33
+ return "transport";
34
+ }
35
+
36
+ export function codexWarmupFailureReason(err: unknown): string {
37
+ return safeWarmupReason(err);
38
+ }
39
+
40
+ function eventTypeFromData(data: unknown): string | undefined {
41
+ if (!data || typeof data !== "object") return undefined;
42
+ const record = data as Record<string, unknown>;
43
+ return typeof record.type === "string" ? record.type : undefined;
44
+ }
45
+
46
+ function parseSseFrame(frame: string): unknown | null {
47
+ const dataLines = frame
48
+ .split(/\r?\n/)
49
+ .filter(line => line.startsWith("data:"))
50
+ .map(line => line.slice(5).trimStart());
51
+ if (dataLines.length === 0) return null;
52
+ const data = dataLines.join("\n").trim();
53
+ if (!data || data === "[DONE]") return null;
54
+ try {
55
+ return JSON.parse(data) as unknown;
56
+ } catch (err) {
57
+ throw new CodexWarmupError("invalid_sse", "Codex warmup received invalid SSE", { cause: err });
58
+ }
59
+ }
60
+
61
+ async function drainWarmupSse(body: ReadableStream<Uint8Array>): Promise<void> {
62
+ const reader = body.getReader();
63
+ const decoder = new TextDecoder();
64
+ let buffer = "";
65
+
66
+ try {
67
+ for (;;) {
68
+ const { done, value } = await reader.read();
69
+ if (done) break;
70
+ buffer += decoder.decode(value, { stream: true });
71
+
72
+ for (;;) {
73
+ const frameEnd = buffer.search(/\r?\n\r?\n/);
74
+ if (frameEnd < 0) break;
75
+ const frame = buffer.slice(0, frameEnd);
76
+ const delimiterLength = buffer[frameEnd] === "\r" ? 4 : 2;
77
+ buffer = buffer.slice(frameEnd + delimiterLength);
78
+ const parsed = parseSseFrame(frame);
79
+ const type = eventTypeFromData(parsed);
80
+ if (type === "response.completed") return;
81
+ if (type === "response.failed") throw new CodexWarmupError("stream_failed");
82
+ if (type === "response.incomplete") throw new CodexWarmupError("stream_incomplete");
83
+ if (type === "error") throw new CodexWarmupError("stream_error");
84
+ }
85
+ }
86
+
87
+ if (buffer.trim()) {
88
+ const parsed = parseSseFrame(buffer);
89
+ const type = eventTypeFromData(parsed);
90
+ if (type === "response.completed") return;
91
+ if (type === "response.failed") throw new CodexWarmupError("stream_failed");
92
+ if (type === "response.incomplete") throw new CodexWarmupError("stream_incomplete");
93
+ if (type === "error") throw new CodexWarmupError("stream_error");
94
+ }
95
+
96
+ throw new CodexWarmupError("no_terminal", "Codex warmup ended before completion");
97
+ } finally {
98
+ reader.releaseLock();
99
+ }
100
+ }
101
+
102
+ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<void> {
103
+ let res: Response;
104
+ try {
105
+ res = await fetch(CODEX_RESPONSES_URL, {
106
+ method: "POST",
107
+ headers: {
108
+ Authorization: `Bearer ${options.accessToken}`,
109
+ "ChatGPT-Account-Id": options.chatgptAccountId,
110
+ "Content-Type": "application/json",
111
+ },
112
+ body: JSON.stringify({
113
+ model: options.model?.trim() || DEFAULT_MODEL,
114
+ instructions: "Reply with OK.",
115
+ input: "hi",
116
+ stream: true,
117
+ store: false,
118
+ }),
119
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
120
+ });
121
+ } catch (err) {
122
+ throw new CodexWarmupError("transport", "Codex warmup request failed", { cause: err });
123
+ }
124
+
125
+ if (!res.ok) {
126
+ await res.body?.cancel().catch(() => {});
127
+ throw new CodexWarmupError("http_status", "Codex warmup was rejected", { status: res.status });
128
+ }
129
+ if (!res.body) throw new CodexWarmupError("missing_body");
130
+
131
+ try {
132
+ await drainWarmupSse(res.body);
133
+ } finally {
134
+ await res.body?.cancel().catch(() => {});
135
+ }
136
+ }
137
+
@@ -18,9 +18,12 @@ import { getValidAccessTokenForAccount, listOAuthProviders, OAuthLoginRequiredEr
18
18
  import {
19
19
  getValidCodexToken,
20
20
  listCodexAccountIds,
21
+ markCodexAccountValidated,
22
+ markCodexAccountValidationFailed,
21
23
  readCodexAccountRecord,
22
24
  TokenRefreshError,
23
25
  } from "../codex/account-store";
26
+ import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
24
27
 
25
28
  export interface TokenGuardianHandle {
26
29
  stop(): void;
@@ -29,6 +32,7 @@ export interface TokenGuardianHandle {
29
32
  export interface GuardianSweepResult {
30
33
  enabled: boolean;
31
34
  refreshed: string[];
35
+ warmed: string[];
32
36
  failed: string[];
33
37
  skippedBackoff: string[];
34
38
  }
@@ -40,6 +44,8 @@ const DEFAULTS = {
40
44
  leadSeconds: 900,
41
45
  failureBackoffBaseSeconds: 300,
42
46
  failureBackoffMaxSeconds: 3600,
47
+ codexWarmupMaxAgeSeconds: 691_200, // 8d — matches Codex managed-auth last_refresh cadence.
48
+ codexWarmupModel: "gpt-5.4-mini",
43
49
  };
44
50
 
45
51
  interface BackoffEntry {
@@ -67,6 +73,9 @@ function resolved(g: OcxTokenGuardianConfig | undefined) {
67
73
  leadSeconds: num(g?.leadSeconds, DEFAULTS.leadSeconds, 0),
68
74
  backoffBaseSeconds: num(g?.failureBackoffBaseSeconds, DEFAULTS.failureBackoffBaseSeconds, 0),
69
75
  backoffMaxSeconds: num(g?.failureBackoffMaxSeconds, DEFAULTS.failureBackoffMaxSeconds, 0),
76
+ codexWarmupEnabled: g?.codexWarmupEnabled === true,
77
+ codexWarmupMaxAgeSeconds: num(g?.codexWarmupMaxAgeSeconds, DEFAULTS.codexWarmupMaxAgeSeconds, 60),
78
+ codexWarmupModel: g?.codexWarmupModel?.trim() || DEFAULTS.codexWarmupModel,
70
79
  };
71
80
  }
72
81
 
@@ -106,7 +115,7 @@ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: numb
106
115
  export async function guardianSweep(nowMs: number = Date.now()): Promise<GuardianSweepResult> {
107
116
  const config: OcxConfig = loadConfig();
108
117
  const g = config.tokenGuardian;
109
- const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], failed: [], skippedBackoff: [] };
118
+ const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], warmed: [], failed: [], skippedBackoff: [] };
110
119
  if (!g?.enabled) return result;
111
120
 
112
121
  const opts = resolved(g);
@@ -142,18 +151,34 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
142
151
  if (resolveRefreshPolicy("chatgpt", config) === "proactive") {
143
152
  for (const id of listCodexAccountIds()) {
144
153
  const record = readCodexAccountRecord(id);
145
- const cred = record?.deletedAt == null ? record?.credential : undefined;
154
+ if (!record || record.deletedAt != null) continue;
155
+ const cred = record.credential;
146
156
  if (!cred) continue;
147
- if (cred.expiresAt > nowMs + horizonMs) continue;
157
+ const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
158
+ const needsWarmup = opts.codexWarmupEnabled
159
+ && (record.lastCodexValidatedAt === undefined || nowMs - record.lastCodexValidatedAt > opts.codexWarmupMaxAgeSeconds * 1000);
160
+ if (!needsRefresh && !needsWarmup) continue;
148
161
  const key = `codex:${id}`;
149
162
  if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
150
163
  tasks.push(async () => {
151
164
  try {
152
- await getValidCodexToken(id);
165
+ const token = await getValidCodexToken(id);
166
+ if (needsRefresh) result.refreshed.push(key);
167
+ if (needsWarmup) {
168
+ await warmCodexAccount({
169
+ accessToken: token.accessToken,
170
+ chatgptAccountId: token.chatgptAccountId,
171
+ model: opts.codexWarmupModel,
172
+ });
173
+ markCodexAccountValidated(id, Date.now());
174
+ result.warmed.push(key);
175
+ }
153
176
  backoff.delete(key);
154
- result.refreshed.push(key);
155
177
  } catch (err) {
156
178
  const permanent = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired");
179
+ if (needsWarmup && !(err instanceof TokenRefreshError)) {
180
+ markCodexAccountValidationFailed(id, codexWarmupFailureReason(err));
181
+ }
157
182
  recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent);
158
183
  result.failed.push(key);
159
184
  }
@@ -186,8 +211,8 @@ export function startTokenGuardian(): TokenGuardianHandle {
186
211
  const runSweep = () => {
187
212
  void guardianSweep()
188
213
  .then(r => {
189
- if (r.enabled && (r.refreshed.length || r.failed.length)) {
190
- console.log(`🛡️ token-guardian: refreshed ${r.refreshed.length}, failed ${r.failed.length}`);
214
+ if (r.enabled && (r.refreshed.length || r.warmed.length || r.failed.length)) {
215
+ console.log(`🛡️ token-guardian: refreshed ${r.refreshed.length}, warmed ${r.warmed.length}, failed ${r.failed.length}`);
191
216
  }
192
217
  })
193
218
  .catch(err => console.log(`token-guardian sweep error: ${err instanceof Error ? err.message : String(err)}`))
@@ -38,10 +38,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
38
38
  "qwen3-coder-next": 256_000,
39
39
  };
40
40
 
41
- const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
41
+ const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
42
42
 
43
- // Codex does not accept raw "max" in catalog metadata; Kiro xhigh already maps to the maximum
44
- // fake-thinking budget in src/adapters/kiro.ts.
43
+ // Kiro has no upstream reasoning_effort enum; these labels map to fake-thinking budgets in
44
+ // src/adapters/kiro.ts.
45
45
  export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
46
46
  KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
47
47
  );
@@ -60,24 +60,25 @@ export type ProviderConfigSeed = Pick<
60
60
  | "googleMode" | "project" | "location"
61
61
  >;
62
62
 
63
-
64
- const OLLAMA_REASONING_MAP: Record<string, string> = { xhigh: "max" };
65
-
66
63
  // Shared between the OAuth (Claude account) and API-key Anthropic entries so both expose the
67
64
  // same static model seed.
68
65
  const ANTHROPIC_MODELS = ["claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
69
66
  const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000 };
70
67
 
71
68
  const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
72
- const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
73
- const ZAI_GLM_52_REASONING_MAP: Record<string, string> = {
74
- none: "none",
75
- minimal: "none",
76
- low: "high",
77
- medium: "high",
78
- high: "high",
79
- xhigh: "max",
80
- max: "max",
69
+ const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
70
+ const OPENAI_GPT56_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
71
+ const OPENAI_GPT56_CONTEXT_WINDOW = 372_000;
72
+ const OPENAI_GPT56_CONTEXT_WINDOWS = {
73
+ "gpt-5.6-sol": OPENAI_GPT56_CONTEXT_WINDOW,
74
+ "gpt-5.6-terra": OPENAI_GPT56_CONTEXT_WINDOW,
75
+ "gpt-5.6-luna": OPENAI_GPT56_CONTEXT_WINDOW,
76
+ };
77
+ const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
78
+ const OPENROUTER_GPT56_CONTEXT_WINDOWS = {
79
+ "openai/gpt-5.6-sol": OPENAI_GPT56_CONTEXT_WINDOW,
80
+ "openai/gpt-5.6-terra": OPENAI_GPT56_CONTEXT_WINDOW,
81
+ "openai/gpt-5.6-luna": OPENAI_GPT56_CONTEXT_WINDOW,
81
82
  };
82
83
 
83
84
  /**
@@ -124,17 +125,8 @@ const UMANS_MODELS = [
124
125
  "umans-glm-5.1",
125
126
  "umans-qwen3.6-35b-a3b",
126
127
  ];
127
- const UMANS_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
128
- const UMANS_GLM_REASONING_EFFORTS = ["high", "xhigh"];
129
- const UMANS_GLM_REASONING_MAP: Record<string, string> = {
130
- none: "high",
131
- minimal: "high",
132
- low: "high",
133
- medium: "high",
134
- high: "high",
135
- xhigh: "max",
136
- max: "max",
137
- };
128
+ const UMANS_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
129
+ const UMANS_GLM_REASONING_EFFORTS = ["high", "xhigh", "max"];
138
130
  const UMANS_TEXT_ONLY_MODELS = ["umans-glm-5.2", "umans-glm-5.1"];
139
131
  const UMANS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
140
132
  "umans-coder": 262_144,
@@ -260,7 +252,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
260
252
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
261
253
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
262
254
  },
263
- { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5" },
255
+ { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5", models: ["gpt-5.5", ...OPENAI_GPT56_MODELS], modelContextWindows: OPENAI_GPT56_CONTEXT_WINDOWS },
264
256
  {
265
257
  id: "umans",
266
258
  label: "Umans AI Coding Plan",
@@ -278,14 +270,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
278
270
  "umans-coder": UMANS_REASONING_EFFORTS,
279
271
  "umans-kimi-k2.7": UMANS_REASONING_EFFORTS,
280
272
  "umans-kimi-k2.6": UMANS_REASONING_EFFORTS,
281
- "umans-flash": ["low", "medium", "high"],
273
+ "umans-flash": UMANS_REASONING_EFFORTS,
282
274
  "umans-glm-5.2": UMANS_GLM_REASONING_EFFORTS,
283
275
  "umans-glm-5.1": UMANS_GLM_REASONING_EFFORTS,
284
- "umans-qwen3.6-35b-a3b": ["low", "medium", "high"],
285
- },
286
- modelReasoningEffortMap: {
287
- "umans-glm-5.2": UMANS_GLM_REASONING_MAP,
288
- "umans-glm-5.1": UMANS_GLM_REASONING_MAP,
276
+ "umans-qwen3.6-35b-a3b": UMANS_REASONING_EFFORTS,
289
277
  },
290
278
  noVisionModels: UMANS_TEXT_ONLY_MODELS,
291
279
  escapeBuiltinToolNames: true,
@@ -300,8 +288,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
300
288
  "kimi-k2.7-code-highspeed": [],
301
289
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
302
290
  },
291
+ // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
292
+ // the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
303
293
  modelReasoningEffortMap: {
304
- "glm-5.2": ZAI_GLM_52_REASONING_MAP,
305
294
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
306
295
  },
307
296
  thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
@@ -345,12 +334,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
345
334
  "kimi-k2.6": [],
346
335
  "kimi-k2.6-fast": [],
347
336
  "kimi-k2.7-code": [],
348
- "qwen3.5-397b": ["low", "medium", "high"],
337
+ "qwen3.5-397b": ["low", "medium", "high", "xhigh", "max"],
349
338
  "qwen3.5-397b-fast": [],
350
- "qwen3.6-35b": ["low", "medium", "high"],
339
+ "qwen3.6-35b": ["low", "medium", "high", "xhigh", "max"],
351
340
  "qwen3.6-35b-fast": [],
352
341
  },
353
- modelReasoningEffortMap: { "glm-5.2": ZAI_GLM_52_REASONING_MAP },
354
342
  noReasoningModels: ["glm-5.2-fast", "kimi-k2.5-fast", "kimi-k2.6-fast", "qwen3.5-397b-fast", "qwen3.6-35b-fast"],
355
343
  noVisionModels: ["glm-5.2", "glm-5.2-fast", "qwen3.5-397b", "qwen3.5-397b-fast"],
356
344
  noTemperatureModels: ["kimi-k2.7-code"],
@@ -359,13 +347,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
359
347
  autoToolChoiceOnlyModels: ["kimi-k2.7-code"],
360
348
  preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
361
349
  },
362
- { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5"], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000 } },
350
+ { id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000, ...OPENROUTER_GPT56_CONTEXT_WINDOWS } },
363
351
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
364
352
  { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3-pro", jawcodeBundle: "google", extraMetadataAliases: ["gemini"] },
365
353
  { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
366
354
  { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.5-flash-low", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
367
355
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
368
- { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank", reasoningEffortMap: OLLAMA_REASONING_MAP },
356
+ { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
369
357
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
370
358
  { id: "lm-studio", label: "LM Studio (local)", adapter: "openai-chat", baseUrl: "http://localhost:1234/v1", authKind: "local", featured: true, note: "Local — no key needed" },
371
359
  {
@@ -412,7 +400,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
412
400
  models: ["glm-5.2", "glm-5.2[1m]", "glm-5.1", "glm-5", "glm-4.6"],
413
401
  noVisionModels: ZAI_GLM_52_MODELS,
414
402
  modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
415
- modelReasoningEffortMap: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_MAP])),
416
403
  preserveReasoningContentModels: ZAI_GLM_52_MODELS,
417
404
  },
418
405
  { id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
@@ -430,7 +417,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
430
417
  adapter: "openai-chat",
431
418
  authKind: "key",
432
419
  dashboardUrl: "https://ollama.com/settings/keys",
433
- reasoningEffortMap: OLLAMA_REASONING_MAP,
434
420
  models: ["glm-5.2", "deepseek-v4-pro", "qwen3-coder", "gpt-oss:120b", "kimi-k2.6", "minimax-m3", "qwen3.5", "gemma4"],
435
421
  defaultModel: "glm-5.2",
436
422
  noVisionModels: [
@@ -6,6 +6,7 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] =
6
6
  { effort: "medium", description: "Balances speed and reasoning depth" },
7
7
  { effort: "high", description: "Greater reasoning depth for complex problems" },
8
8
  { effort: "xhigh", description: "Extended reasoning for the hardest problems" },
9
+ { effort: "max", description: "Maximum reasoning for the hardest problems" },
9
10
  ];
10
11
 
11
12
  const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
@@ -53,7 +54,6 @@ export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId:
53
54
  function requestToCodexEffort(requested: string): string | undefined {
54
55
  if (requested === "none") return undefined;
55
56
  if (requested === "minimal") return "low";
56
- if (requested === "max") return "xhigh";
57
57
  return CODEX_REASONING_SET.has(requested) ? requested : undefined;
58
58
  }
59
59
 
@@ -82,9 +82,8 @@ export function reasoningEffortMapFor(provider: OcxProviderConfig, modelId: stri
82
82
  }
83
83
 
84
84
  /**
85
- * Translate Codex's reasoning label into the provider's real wire value. The Codex catalog must only
86
- * advertise labels Codex itself accepts (`low`/`medium`/`high`/`xhigh`), but some upstreams use
87
- * different values (`max`) or a smaller subset (`low`/`medium`/`high`).
85
+ * Translate Codex's reasoning label into the provider's real wire value. Prefer identity labels
86
+ * (`xhigh` stays `xhigh`, `max` stays `max`); provider maps are only for real upstream aliases.
88
87
  */
89
88
  export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
90
89
  if (!requested) return undefined;
package/src/router.ts CHANGED
@@ -29,9 +29,8 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
29
29
  },
30
30
  ];
31
31
 
32
- // Merge registry-default effort maps under user values so persisted built-in provider configs
33
- // that predate reasoningEffortMap/modelReasoningEffortMap still get correct wire translations
34
- // (e.g. ollama-cloud xhigh -> max) without a disk migration. User overrides win per-key.
32
+ // Merge registry-default effort maps under user values so built-in provider configs can
33
+ // carry real upstream aliases without a disk migration. User overrides win per-key.
35
34
  function mergeRecord(
36
35
  seed: Record<string, string> | undefined,
37
36
  user: Record<string, string> | undefined,
@@ -159,12 +159,12 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
159
159
  const raw = usage as {
160
160
  input_tokens?: unknown;
161
161
  output_tokens?: unknown;
162
- input_tokens_details?: { cached_tokens?: unknown };
162
+ input_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
163
163
  output_tokens_details?: { reasoning_tokens?: unknown };
164
164
  total_tokens?: unknown;
165
165
  prompt_tokens?: unknown;
166
166
  completion_tokens?: unknown;
167
- prompt_tokens_details?: { cached_tokens?: unknown };
167
+ prompt_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
168
168
  completion_tokens_details?: { reasoning_tokens?: unknown };
169
169
  };
170
170
  if (typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") {
@@ -175,6 +175,9 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
175
175
  ...(typeof raw.input_tokens_details?.cached_tokens === "number"
176
176
  ? { cachedInputTokens: raw.input_tokens_details.cached_tokens }
177
177
  : {}),
178
+ ...(typeof raw.input_tokens_details?.cache_write_tokens === "number"
179
+ ? { cacheCreationInputTokens: raw.input_tokens_details.cache_write_tokens }
180
+ : {}),
178
181
  ...(typeof raw.output_tokens_details?.reasoning_tokens === "number"
179
182
  ? { reasoningOutputTokens: raw.output_tokens_details.reasoning_tokens }
180
183
  : {}),
@@ -188,6 +191,9 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
188
191
  ...(typeof raw.prompt_tokens_details?.cached_tokens === "number"
189
192
  ? { cachedInputTokens: raw.prompt_tokens_details.cached_tokens }
190
193
  : {}),
194
+ ...(typeof raw.prompt_tokens_details?.cache_write_tokens === "number"
195
+ ? { cacheCreationInputTokens: raw.prompt_tokens_details.cache_write_tokens }
196
+ : {}),
191
197
  ...(typeof raw.completion_tokens_details?.reasoning_tokens === "number"
192
198
  ? { reasoningOutputTokens: raw.completion_tokens_details.reasoning_tokens }
193
199
  : {}),
package/src/types.ts CHANGED
@@ -322,6 +322,12 @@ export interface OcxTokenGuardianConfig {
322
322
  failureBackoffBaseSeconds?: number;
323
323
  /** Backoff ceiling (seconds). Default 3600. */
324
324
  failureBackoffMaxSeconds?: number;
325
+ /** Optional Codex pool session warmup sweep. Default false to avoid background synthetic traffic. */
326
+ codexWarmupEnabled?: boolean;
327
+ /** Max age before a Codex pool account is revalidated via `/codex/responses`. Default 691200 (8d). */
328
+ codexWarmupMaxAgeSeconds?: number;
329
+ /** Model used for optional Codex pool warmup. Default gpt-5.4-mini. */
330
+ codexWarmupModel?: string;
325
331
  }
326
332
 
327
333
  export interface OcxVisionSidecarConfig {
@@ -396,7 +402,7 @@ export interface OcxProviderConfig {
396
402
  refreshPolicy?: RefreshPolicy;
397
403
  /**
398
404
  * Provider-wide Codex-visible reasoning tiers for routed models. Use only Codex-supported labels
399
- * here (`low`, `medium`, `high`, `xhigh`); translate to provider-specific wire values with
405
+ * here (`low`, `medium`, `high`, `xhigh`, `max`); translate provider aliases with
400
406
  * `reasoningEffortMap` / `modelReasoningEffortMap` below.
401
407
  */
402
408
  reasoningEfforts?: string[];
@@ -489,4 +495,7 @@ export interface CodexAccountCredentialRecord {
489
495
  refreshGrantFingerprint?: string;
490
496
  deletedAt?: number;
491
497
  replacedAt?: number;
498
+ lastCodexValidatedAt?: number;
499
+ lastCodexValidationStatus?: "ok" | "failed";
500
+ lastCodexValidationError?: string;
492
501
  }