@bitkyc08/opencodex 2.6.31-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 +15 -3
- package/README.md +14 -2
- package/README.zh-CN.md +12 -3
- package/gui/dist/assets/{index-CWujz83O.js → index-D_JZzI0r.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/bridge.ts +8 -1
- package/src/codex/account-store.ts +42 -1
- package/src/codex/auth-api.ts +39 -0
- package/src/codex/catalog.ts +31 -7
- package/src/codex/warmup.ts +137 -0
- package/src/oauth/token-guardian.ts +32 -7
- package/src/providers/kiro-models.ts +3 -3
- package/src/providers/registry.ts +24 -38
- package/src/reasoning-effort.ts +3 -4
- package/src/router.ts +2 -3
- package/src/server/request-log.ts +8 -2
- package/src/types.ts +10 -1
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.
|
|
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.
|
|
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,{})})}));
|
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-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
package/src/adapters/kiro.ts
CHANGED
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
|
-
|
|
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;
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -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(
|
package/src/codex/catalog.ts
CHANGED
|
@@ -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 = [
|
|
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
|
|
470
|
-
*
|
|
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
|
|
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
|
-
|
|
154
|
+
if (!record || record.deletedAt != null) continue;
|
|
155
|
+
const cred = record.credential;
|
|
146
156
|
if (!cred) continue;
|
|
147
|
-
|
|
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
|
-
//
|
|
44
|
-
//
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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":
|
|
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":
|
|
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"
|
|
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: [
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -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.
|
|
86
|
-
*
|
|
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
|
|
33
|
-
//
|
|
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
|
|
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
|
}
|