@pure01fx/dsh-openai-codex-auth 0.7.3 → 0.9.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0
6
+
7
+ ### Codex 0.153 compatibility
8
+
9
+ - Tracks Codex main snapshot `ddf04ad` and uses the stable `0.153.4` model-catalog client version after auditing the intervening Responses, WebSocket, OAuth, Catalog, and quota protocol changes.
10
+ - Preserves the complete bounded `response.completed.response.usage` object alongside the exact high-precision `usage_metadata.amount` value across HTTP/SSE, WebSocket, and same-origin status responses.
11
+ - Supports GPT-6-Astra and its Fast alias through the complete Responses Lite contract: stable developer instruction/tool prefix items, namespaced functions, all-turn reasoning context, model-default verbosity, disabled parallel tool calls, the HTTP Lite header, and the WebSocket request-metadata route marker. Standard Responses requests remain unchanged.
12
+ - Recognizes `normal_model_slug` on additional `wham/usage` limits and honors the backend's `allowed` decision when `limit_reached` is absent, while intentionally remaining a passive client without the Luna Reserve opt-in header.
13
+ - Shows an isolated quota snapshot, including remaining reset credits, for every managed account, refreshing once when Codex settings opens or on explicit request; concurrent usage lookups are time-bounded outside the main credential lock, rotating refresh operations are journaled, bounded independently of caller cancellation, single-flighted per account across service processes, recoverable after persistence contention, and never ambiguously retried, stale non-current credentials are renewed without changing the selected account, and one account's failure does not hide the others.
14
+
15
+ ## 0.8.0
16
+
17
+ ### Multi-account authentication
18
+
19
+ - Adds multiple managed ChatGPT OAuth accounts with a global current-account selector, per-account removal, optional email labels, and automatic activation of newly added accounts.
20
+ - Migrates legacy version-1 single-account credential documents atomically to version 2 while keeping DSH credential publication and rollback guarantees.
21
+ - Pins every request/recovery attempt to its starting account so a concurrent account switch cannot replay an authenticated request under another account.
22
+ - Extends the same-origin settings API and Web UI with an account list, explicit switching, and individual logout controls without exposing access or refresh tokens.
23
+
5
24
  ## 0.7.3
6
25
 
7
26
  ### Client model visibility
package/README.md CHANGED
@@ -48,6 +48,14 @@ dsh web
48
48
 
49
49
  设备码登录无需 OAuth callback,适合本机、SSH 隧道和 HTTPS 反向代理场景。
50
50
 
51
+ ## 多账号切换
52
+
53
+ - 已登录后继续点击 **用设备码添加账号** 或 **用浏览器添加账号**,可追加 ChatGPT 账号;新登录的账号会自动成为当前账号。
54
+ - 设置页的 **已添加账号** 列表支持设为当前和单独移除。切换是全局的,只影响切换完成后的新 Codex 请求;已开始的请求继续使用启动时绑定的账号。
55
+ - 移除当前账号时,插件按账号列表顺序选择下一个账号;移除最后一个账号后会清除 `DSH_OPENAI_CODEX_TOKEN`。
56
+ - 原有单账号 `version: 1` 凭据会在首次读取时原子迁移为 `version: 2` 多账号文档,无需重新登录。
57
+ - 如果 `DSH_OPENAI_CODEX_TOKEN` 来自只读环境变量或其他外部 authority,插件会拒绝需要改写当前 token 的登录、切换和退出操作,避免界面与实际请求账号不一致。
58
+
51
59
  ## 两种登录方式
52
60
 
53
61
  ### 设备码登录(推荐)
@@ -82,6 +90,7 @@ http://localhost:1455/auth/callback
82
90
  | --- | --- |
83
91
  | 设备码登录 | 无本机 callback,支持远程/headless 使用 |
84
92
  | 浏览器 OAuth fallback | PKCE + state、端到端 1455 探测、手动 code 回填和临时 callback listener |
93
+ | 多账号管理 | 添加多个 OAuth 账号、全局切换当前账号、单独移除,并自动迁移旧单账号凭据 |
85
94
  | Codex 用量面板 | 展示短周期与周用量、剩余额度和重置时间;Native 模式直接接收 Codex 返回的套餐额度,`wham/usage` 仅作初始读取和 fallback |
86
95
  | 当前路由状态 | 设置页显示 `openai-codex` 当前由本插件 Native Adapter、外部 Adapter 或无人持有;Native 模式同时显示 WebSocket v2 或 HTTP/SSE |
87
96
  | 输入框额度圈 | 登录后显示在输入框右下角;悬停或键盘聚焦显示用量,点击圆圈强制刷新,不做常驻额度轮询 |
@@ -99,9 +108,9 @@ http://localhost:1455/auth/callback
99
108
 
100
109
  登录成功后:
101
110
 
102
- 1. 凭据以 owner-only 权限原子写入 `$DSH_HOME/openai-codex-auth.json`。
103
- 2. access token 通过 DSH credentials 注入 `DSH_OPENAI_CODEX_TOKEN`。
104
- 3. `openai-codex` 模型提供方在请求时读取该凭据。
111
+ 1. 多账号凭据以 owner-only 权限原子写入 `$DSH_HOME/openai-codex-auth.json`;文档保存账号列表与全局当前账号指针。
112
+ 2. 当前账号的 access token 通过 DSH credentials 注入 `DSH_OPENAI_CODEX_TOKEN`。
113
+ 3. `openai-codex` 模型提供方在每个请求开始时读取当前凭据,并把该次尝试固定到同一个账号。
105
114
  4. 输入框右下角显示 Codex 额度圈;悬停或键盘聚焦显示当前用量,点击才强制刷新。Native transport 优先消费 WebSocket `codex.rate_limits` 事件或 HTTP `x-codex-*` headers,未收到直接更新时才在 turn 结束后读取 `wham/usage`。
106
115
  5. 设置页与额度圈只读取登录状态、账号 ID、过期时间和用量摘要,不接触 token。
107
116
 
@@ -111,13 +120,15 @@ http://localhost:1455/auth/callback
111
120
 
112
121
  | 方法 | 路径 | 用途 |
113
122
  | --- | --- | --- |
114
- | GET | `/openai-codex/status` | 登录、flow、用量和 CSRF 状态 |
123
+ | GET | `/openai-codex/status` | 登录、账号列表、当前账号、flow、用量和 CSRF 状态 |
124
+ | POST | `/openai-codex/accounts/current` | 以 `{ "accountId": "…" }` 设定全局当前账号 |
125
+ | POST | `/openai-codex/accounts/logout` | 以 `{ "accountId": "…" }` 单独移除账号 |
115
126
  | POST | `/openai-codex/device/start` | 创建或复用设备码 flow |
116
127
  | POST | `/openai-codex/browser/prepare` | 创建浏览器 flow,返回授权 URL 和一次性 1455 探测 URL |
117
128
  | POST | `/openai-codex/browser/complete` | 接收用户粘贴的 callback URL 或 authorization code |
118
129
  | GET | `/openai-codex/browser/start` | 兼容入口:直接启动并跳转浏览器 OAuth |
119
130
  | POST | `/openai-codex/cancel` | 取消当前登录、关闭临时 listener,保留旧凭据 |
120
- | POST | `/openai-codex/logout` | 取消 flow 并删除本地凭据 |
131
+ | POST | `/openai-codex/logout` | 兼容入口:取消 flow 并移除当前账号 |
121
132
 
122
133
  1456 已完全退役。1455 平时关闭,仅浏览器 OAuth pending 时绑定 loopback;设备码登录完全不使用它。
123
134
 
package/client.js CHANGED
@@ -181,6 +181,7 @@ window.__ModuleLoader__.load({
181
181
  .codexButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}.codexButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.codexButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:not-allowed}
182
182
  .codexButton.primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.codexButton.primary:hover:not(:disabled){border-color:transparent;background:var(--dsw-alias-button-primary-hover)}.codexButton.danger{color:var(--dsw-alias-state-error-primary)}
183
183
  .codexGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:14px}
184
+ .codexAccounts{margin-top:14px;margin-bottom:20px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.codexAccountList{display:flex;flex-direction:column;gap:8px}.codexAccountOption{display:flex;align-items:center;justify-content:space-between;gap:18px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2)}.codexAccountDetails{flex:1;min-width:0}.codexAccountTitle{display:flex;align-items:center;flex-wrap:wrap;gap:6px 10px;min-width:0}.codexAccountTitle strong{font-size:13px;font-weight:500;overflow-wrap:anywhere}.codexAccountTitle .codexBadge{padding:1px 8px;line-height:18px}.codexAccountMeta{display:flex;flex-wrap:wrap;gap:2px 14px;margin-top:4px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.codexAccountActions{flex:none;flex-wrap:nowrap;margin-top:0}.codexAccountActions .codexButton{white-space:nowrap}.codexAccountHeaderActions{display:flex;align-items:center;gap:10px}.codexAccountRefresh{min-height:28px;padding:3px 9px;font-size:12px}.codexAccountQuota{display:flex;flex-wrap:wrap;gap:5px 14px;margin-top:10px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:9px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.codexAccountQuotaItem{white-space:nowrap}.codexAccountQuotaItem strong{color:var(--dsw-alias-label-primary);font-weight:500;font-variant-numeric:tabular-nums}.codexAccountQuotaStatus{color:var(--dsw-alias-label-tertiary)}.codexAccountQuotaStatus.error{color:var(--dsw-alias-state-error-primary)}
184
185
  .codexUsage{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2)}
185
186
  .codexUsageHead{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexUsageName{font-size:13px;font-weight:500}.codexUsageValue{font-size:12px;color:var(--dsw-alias-label-tertiary)}
186
187
  .codexBar{height:6px;margin:10px 0 8px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.codexBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary);transition:width .25s ease}.codexBarFill.high{background:var(--dsw-alias-state-warn-label)}
@@ -203,7 +204,7 @@ window.__ModuleLoader__.load({
203
204
  .codexQuotaTrack{fill:none;stroke:var(--dsw-alias-border-l3);stroke-width:2px}.codexQuotaAvailable{fill:none;stroke:var(--dsw-alias-state-success-primary);stroke-width:2px;stroke-linecap:round;transition:stroke .25s ease,stroke-dasharray .25s ease,stroke-dashoffset .25s ease}.codexQuotaAvailable.high{stroke:var(--dsw-alias-state-warn-label)}.codexQuotaAvailable.critical{stroke:var(--dsw-alias-state-error-primary)}.codexQuotaLoading{animation:codexQuotaPulse .8s ease-in-out infinite alternate}
204
205
  .codexQuotaPanel{z-index:100;box-sizing:border-box;width:272px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:20px;position:absolute;right:0;bottom:calc(100% + 8px)}.codexQuotaPanelHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.codexQuotaPanelHead strong{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.codexQuotaPanelHead span{color:var(--dsw-alias-label-tertiary)}
205
206
  .codexQuotaWindow{padding:7px 0}.codexQuotaWindow+.codexQuotaWindow{border-top:1px solid var(--dsw-alias-border-l2)}.codexQuotaWindowHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexQuotaWindowHead strong{font-weight:500;color:var(--dsw-alias-label-secondary)}.codexQuotaWindowHead span{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}.codexQuotaBar{height:4px;margin:6px 0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-interactive-bg-hover)}.codexQuotaBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary)}.codexQuotaBarFill.high{background:var(--dsw-alias-state-warn-label)}.codexQuotaBarFill.critical{background:var(--dsw-alias-state-error-primary)}.codexQuotaReset{color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage{margin:4px 0;color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage.error{color:var(--dsw-alias-state-error-primary)}
206
- @keyframes codexPulse{to{background-position:-200% 0}}@keyframes codexQuotaPulse{to{opacity:.35}}@media(prefers-reduced-motion:reduce){.codexSkeleton,.codexQuotaLoading{animation:none}}@media(max-width:620px){.codexHero{padding:14px;flex-direction:column}.codexBody{padding:14px}.codexGrid,.codexModelVisibilityList{grid-template-columns:1fr}.codexCode{font-size:20px}}
207
+ @keyframes codexPulse{to{background-position:-200% 0}}@keyframes codexQuotaPulse{to{opacity:.35}}@media(prefers-reduced-motion:reduce){.codexSkeleton,.codexQuotaLoading{animation:none}}@media(max-width:620px){.codexHero{padding:14px;flex-direction:column}.codexBody{padding:14px}.codexGrid,.codexModelVisibilityList{grid-template-columns:1fr}.codexAccountOption{align-items:stretch;flex-direction:column;gap:10px}.codexAccountActions{align-self:flex-start}.codexCode{font-size:20px}}
207
208
  `
208
209
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin="' + PLUGIN_ID + '"]') === null) {
209
210
  const style = document.createElement('style')
@@ -341,6 +342,26 @@ window.__ModuleLoader__.load({
341
342
  )
342
343
  }
343
344
 
345
+ function AccountQuotaSummary(props) {
346
+ const entry = props.entry
347
+ if (props.loading && !entry) return h('div', { className: 'codexAccountQuota' }, h('span', { className: 'codexAccountQuotaStatus' }, '正在读取额度…'))
348
+ if (!entry) return h('div', { className: 'codexAccountQuota' }, h('span', { className: 'codexAccountQuotaStatus' }, '尚未读取额度'))
349
+ if (entry.error) return h('div', { className: 'codexAccountQuota' }, h('span', { className: 'codexAccountQuotaStatus error', role: 'status' }, '额度读取失败:' + entry.error))
350
+ const usage = entry.usage
351
+ if (!usage) return h('div', { className: 'codexAccountQuota' }, h('span', { className: 'codexAccountQuotaStatus' }, '暂无额度数据'))
352
+ const rows = []
353
+ if (usage.planType) rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'plan' }, String(usage.planType).toUpperCase()))
354
+ if (displayQuotaWindow(usage.primary)) rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'primary' }, quotaWindowName(usage.primary, false) + ' ', h('strong', null, '剩余 ' + Math.max(0, Math.round(100 - usage.primary.usedPercent)) + '%')))
355
+ if (displayQuotaWindow(usage.secondary)) rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'secondary' }, quotaWindowName(usage.secondary, true) + ' ', h('strong', null, '剩余 ' + Math.max(0, Math.round(100 - usage.secondary.usedPercent)) + '%')))
356
+ if (Number.isFinite(usage.resetCredits)) rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'resetCredits' }, '额度重置 ', h('strong', null, '剩余 ' + Math.max(0, Math.floor(usage.resetCredits)) + ' 次')))
357
+ if (usage.credits) {
358
+ const credits = usage.credits.unlimited ? '无限' : usage.credits.hasCredits ? (usage.credits.balance || '可用') : '不可用'
359
+ rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'credits' }, 'Credits ', h('strong', null, credits)))
360
+ }
361
+ if (usage.fetchedAt) rows.push(h('span', { className: 'codexAccountQuotaItem', key: 'updated' }, '更新 ' + new Date(usage.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })))
362
+ return h('div', { className: 'codexAccountQuota' }, rows.length > 0 ? rows : h('span', { className: 'codexAccountQuotaStatus' }, '暂无额度数据'))
363
+ }
364
+
344
365
  function quotaTone(used) {
345
366
  return used >= 95 ? ' critical' : used >= 80 ? ' high' : ''
346
367
  }
@@ -484,7 +505,13 @@ window.__ModuleLoader__.load({
484
505
  const [manualInput, setManualInput] = useState('')
485
506
  const [now, setNow] = useState(Date.now())
486
507
  const [modelVisibility, setModelVisibility] = useState(readModelVisibility)
508
+ const [accountUsages, setAccountUsages] = useState({})
509
+ const [accountUsageBusy, setAccountUsageBusy] = useState(false)
510
+ const [accountUsageError, setAccountUsageError] = useState('')
511
+ const [accountStatusSyncError, setAccountStatusSyncError] = useState('')
487
512
  const probeGeneration = useRef(0)
513
+ const accountUsageGeneration = useRef(0)
514
+ const accountUsageScope = useRef('')
488
515
 
489
516
  useEffect(() => {
490
517
  const reload = () => { setModelVisibility(readModelVisibility()) }
@@ -566,6 +593,52 @@ window.__ModuleLoader__.load({
566
593
  return value
567
594
  }, [status])
568
595
 
596
+ const refreshAccountUsages = useCallback(async () => {
597
+ const generation = ++accountUsageGeneration.current
598
+ setAccountUsageBusy(true)
599
+ setAccountUsageError('')
600
+ setAccountStatusSyncError('')
601
+ try {
602
+ const value = await post('/accounts/usage')
603
+ const next = {}
604
+ for (const row of Array.isArray(value.accounts) ? value.accounts : []) {
605
+ if (row && typeof row.accountId === 'string') next[row.accountId] = row
606
+ }
607
+ if (generation === accountUsageGeneration.current) {
608
+ setAccountUsages(next)
609
+ try {
610
+ await requestStatus(false)
611
+ } catch (statusError) {
612
+ if (generation === accountUsageGeneration.current) {
613
+ setAccountStatusSyncError('额度已刷新,但账号状态同步失败:' + messageOf(statusError))
614
+ }
615
+ }
616
+ }
617
+ } catch (usageError) {
618
+ if (generation === accountUsageGeneration.current) setAccountUsageError(messageOf(usageError))
619
+ } finally {
620
+ if (generation === accountUsageGeneration.current) setAccountUsageBusy(false)
621
+ }
622
+ }, [post])
623
+
624
+ const accountUsageIds = status && Array.isArray(status.accounts)
625
+ ? status.accounts.map(account => account.accountId).filter(Boolean).sort().join('\n')
626
+ : ''
627
+ useEffect(() => {
628
+ if (!status || !status.csrf || !accountUsageIds) {
629
+ if (accountUsageScope.current) {
630
+ accountUsageScope.current = ''
631
+ setAccountUsages({})
632
+ setAccountUsageError('')
633
+ setAccountStatusSyncError('')
634
+ }
635
+ return
636
+ }
637
+ if (accountUsageScope.current === accountUsageIds) return
638
+ accountUsageScope.current = accountUsageIds
639
+ void refreshAccountUsages()
640
+ }, [accountUsageIds, refreshAccountUsages, status && status.csrf])
641
+
569
642
  const startDevice = async () => {
570
643
  setBusy('device')
571
644
  setCopied(false)
@@ -665,7 +738,38 @@ window.__ModuleLoader__.load({
665
738
  }
666
739
  }
667
740
 
741
+ const mutateAccount = async (path, accountId, action) => {
742
+ if (busy) return
743
+ setBusy(action + ':' + accountId)
744
+ setError('')
745
+ try {
746
+ await post(path, { accountId })
747
+ await load(false)
748
+ } catch (mutationError) {
749
+ setError(messageOf(mutationError))
750
+ } finally {
751
+ setBusy('')
752
+ }
753
+ }
754
+
755
+ const setCurrentAccount = async (accountId) => {
756
+ await mutateAccount('/accounts/current', accountId, 'current')
757
+ }
758
+
759
+ const removeAccount = async (accountId) => {
760
+ const account = accounts.find((candidate) => candidate.accountId === accountId)
761
+ const label = account && account.email ? account.email : shortAccount(accountId)
762
+ const isCurrent = account && (account.current === true || account.accountId === currentAccountId)
763
+ const isLast = accounts.length === 1
764
+ const consequence = isLast
765
+ ? '移除后将退出 Codex,重新使用需要再次完成 OAuth 登录。'
766
+ : isCurrent ? '移除后会自动切换到列表中的下一个账号。' : '该账号的本地 OAuth 凭据会被删除。'
767
+ if (!window.confirm('确定移除 ' + label + '?\n\n' + consequence)) return
768
+ await mutateAccount('/accounts/logout', accountId, 'remove')
769
+ }
770
+
668
771
  const logout = async () => {
772
+ if (busy) return
669
773
  setBusy('logout')
670
774
  probeGeneration.current += 1
671
775
  try {
@@ -701,6 +805,10 @@ window.__ModuleLoader__.load({
701
805
  const usage = status && status.usage
702
806
  const loading = status === null && !error
703
807
  const connected = Boolean(status && status.loggedIn)
808
+ const accounts = status && Array.isArray(status.accounts) ? status.accounts : []
809
+ const currentAccountId = status && status.currentAccountId
810
+ const currentAccount = accounts.find((account) => account.current === true || account.accountId === currentAccountId)
811
+ const currentAccountLabel = currentAccount && currentAccount.email ? currentAccount.email : shortAccount(status && status.accountId)
704
812
  const pending = Boolean(status && status.loginPending) || watchLogin
705
813
  const devicePending = Boolean(status && status.loginMethod === 'device' && status.device)
706
814
  const browserPending = Boolean(status && status.loginMethod === 'browser' && status.browser)
@@ -731,7 +839,7 @@ window.__ModuleLoader__.load({
731
839
  h('div', { className: 'codexLogo', 'aria-hidden': true }, 'OA'),
732
840
  h('div', null,
733
841
  h('h3', { className: 'codexName' }, 'OpenAI Codex 订阅'),
734
- h('p', { className: 'codexMeta', title: connected ? status.accountId : '' }, loading ? '正在读取 OpenAI 登录状态' : connected ? shortAccount(status.accountId) : '尚未连接 ChatGPT 账号'),
842
+ h('p', { className: 'codexMeta', title: connected ? status.accountId : '' }, loading ? '正在读取 OpenAI 登录状态' : connected ? currentAccountLabel : '尚未连接 ChatGPT 账号'),
735
843
  ),
736
844
  ),
737
845
  h('span', { className: 'codexBadge ' + (loading || pending ? 'pending' : connected ? 'connected' : '') },
@@ -751,6 +859,41 @@ window.__ModuleLoader__.load({
751
859
  h('p', { className: 'codexRouteDetail' }, route.detail),
752
860
  )
753
861
  : null,
862
+ !loading && accounts.length > 0
863
+ ? h('div', { className: 'codexAccounts' },
864
+ h('div', { className: 'codexModelVisibilityHead' },
865
+ h('strong', null, '已添加账号'),
866
+ h('div', { className: 'codexAccountHeaderActions' },
867
+ h('span', { className: 'codexMeta' }, accounts.length + ' 个'),
868
+ h('button', { type: 'button', className: 'codexButton codexAccountRefresh', disabled: accountUsageBusy, onClick: () => { void refreshAccountUsages() } }, accountUsageBusy ? '刷新中…' : '刷新额度'),
869
+ ),
870
+ ),
871
+ h('p', { className: 'codexModelVisibilityHint' }, '当前账号用于新的 Codex 请求;每个账号的额度仅在打开本页或点击刷新时读取。'),
872
+ accountUsageError ? h('p', { className: 'codexAccountQuotaStatus error', role: 'status' }, '额度刷新失败:' + accountUsageError) : null,
873
+ accountStatusSyncError ? h('p', { className: 'codexAccountQuotaStatus error', role: 'status' }, accountStatusSyncError) : null,
874
+ h('div', { className: 'codexAccountList' }, accounts.map((account) => {
875
+ const isCurrent = account.current === true || account.accountId === currentAccountId
876
+ const accountBusy = busy === 'current:' + account.accountId || busy === 'remove:' + account.accountId
877
+ return h('div', { className: 'codexAccountOption', key: account.accountId },
878
+ h('div', { className: 'codexAccountDetails', title: account.accountId },
879
+ h('div', { className: 'codexAccountTitle' },
880
+ h('strong', null, account.email || shortAccount(account.accountId)),
881
+ isCurrent ? h('span', { className: 'codexBadge connected' }, '当前账号') : null,
882
+ ),
883
+ h('div', { className: 'codexAccountMeta' },
884
+ account.email ? h('span', null, shortAccount(account.accountId)) : null,
885
+ account.expiresAt ? h('span', null, '到期 ' + new Date(account.expiresAt).toLocaleString()) : null,
886
+ ),
887
+ h(AccountQuotaSummary, { entry: accountUsages[account.accountId], loading: accountUsageBusy }),
888
+ ),
889
+ h('div', { className: 'codexActions codexAccountActions' },
890
+ !isCurrent ? h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy), onClick: () => { void setCurrentAccount(account.accountId) } }, accountBusy && busy.startsWith('current:') ? '切换中…' : '设为当前') : null,
891
+ h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void removeAccount(account.accountId) } }, accountBusy && busy.startsWith('remove:') ? '移除中…' : '移除'),
892
+ ),
893
+ )
894
+ })),
895
+ )
896
+ : null,
754
897
  loading
755
898
  ? h('div', { 'aria-label': '加载中' }, h('div', { className: 'codexSkeleton' }), h('div', { className: 'codexSkeleton', style: { width: '72%' } }))
756
899
  : connected
@@ -848,11 +991,11 @@ window.__ModuleLoader__.load({
848
991
  : null,
849
992
  h('div', { className: 'codexActions' },
850
993
  h('button', { type: 'button', className: 'codexButton primary', disabled: Boolean(busy) || pending || loading, onClick: () => { void startDevice() } },
851
- busy === 'device' ? '正在申请设备码…' : connected ? '用设备码重新登录' : '使用设备码登录'),
994
+ busy === 'device' ? '正在申请设备码…' : connected ? '用设备码添加账号' : '使用设备码登录'),
852
995
  h('button', {
853
996
  type: 'button', className: 'codexButton', disabled: Boolean(busy) || pending || loading || !browserAvailable,
854
997
  title: browserAvailable ? '展开 callback 检测,不会自动弹出窗口' : '仅支持 HTTP 127.0.0.1/localhost 入口', onClick: () => { void startBrowser() },
855
- }, busy === 'browser' ? '正在准备浏览器登录…' : connected ? '用浏览器重新登录' : '本机浏览器 OAuth'),
998
+ }, busy === 'browser' ? '正在准备浏览器登录…' : connected ? '用浏览器添加账号' : '本机浏览器 OAuth'),
856
999
  pending && !devicePending && !browserPending ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void cancel() } }, busy === 'cancel' ? '取消中…' : '取消登录') : null,
857
1000
  connected ? h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy), onClick: refresh }, busy === 'refresh' ? '刷新中…' : '刷新用量') : null,
858
1001
  connected || (status && status.credentialError) ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void logout() } }, busy === 'logout' ? '退出中…' : '退出登录') : null,
package/lib/catalog.d.ts CHANGED
@@ -31,6 +31,11 @@ export interface NativeCodexModel {
31
31
  additionalSpeedTiers: readonly string[];
32
32
  serviceTiers: readonly NativeCodexServiceTier[];
33
33
  defaultServiceTier?: string;
34
+ /** Requires the model-specific Responses Lite wire contract, not Standard Responses. */
35
+ useResponsesLite?: boolean;
36
+ /** Catalog-provided defaults needed to construct the model-specific request. */
37
+ defaultVerbosity?: string;
38
+ instructionsTemplate?: string;
34
39
  contextWindow?: number;
35
40
  inputModalities: readonly string[];
36
41
  }
package/lib/catalog.js CHANGED
@@ -128,6 +128,7 @@ function parseModel(value) {
128
128
  const additionalSpeedTiers = strictStringList(row?.additional_speed_tiers);
129
129
  const serviceTiers = parseServiceTiers(row?.service_tiers);
130
130
  const inputModalities = strictStringList(row?.input_modalities, ['text', 'image']);
131
+ const useResponsesLite = row?.use_responses_lite ?? false;
131
132
  if (slug === undefined
132
133
  || displayName === undefined
133
134
  || reasoning === undefined
@@ -138,6 +139,7 @@ function parseModel(value) {
138
139
  || priority < -2_147_483_648 || priority > 2_147_483_647
139
140
  || additionalSpeedTiers === undefined
140
141
  || serviceTiers === undefined
142
+ || typeof useResponsesLite !== 'boolean'
141
143
  || inputModalities === undefined
142
144
  || !inputModalities.every(value => ['text', 'image', 'audio'].includes(value))) {
143
145
  throw invalidCatalog('native Codex catalog contained an invalid model entry');
@@ -160,6 +162,18 @@ function parseModel(value) {
160
162
  && nonEmptyString(row.multi_agent_reasoning_effort) === undefined) {
161
163
  throw invalidCatalog('native Codex catalog contained an invalid multi-agent reasoning effort');
162
164
  }
165
+ if (row.default_verbosity !== undefined && row.default_verbosity !== null
166
+ && nonEmptyString(row.default_verbosity) === undefined) {
167
+ throw invalidCatalog('native Codex catalog contained an invalid default verbosity');
168
+ }
169
+ const modelMessages = row.model_messages === undefined || row.model_messages === null
170
+ ? undefined : record(row.model_messages);
171
+ if ((row.model_messages !== undefined && row.model_messages !== null && modelMessages === undefined)
172
+ || (modelMessages?.instructions_template !== undefined
173
+ && modelMessages.instructions_template !== null
174
+ && nonEmptyString(modelMessages.instructions_template) === undefined)) {
175
+ throw invalidCatalog('native Codex catalog contained invalid model instructions');
176
+ }
163
177
  for (const key of ['context_window', 'max_context_window']) {
164
178
  if (row[key] !== undefined && row[key] !== null && positiveNumber(row[key]) === undefined) {
165
179
  throw invalidCatalog('native Codex catalog contained an invalid context window');
@@ -169,6 +183,8 @@ function parseModel(value) {
169
183
  const defaultReasoningLevel = nonEmptyString(row.default_reasoning_level);
170
184
  const multiAgentReasoningEffort = nonEmptyString(row.multi_agent_reasoning_effort);
171
185
  const defaultServiceTier = nonEmptyString(row.default_service_tier);
186
+ const defaultVerbosity = nonEmptyString(row.default_verbosity);
187
+ const instructionsTemplate = nonEmptyString(modelMessages?.instructions_template);
172
188
  const contextWindow = positiveNumber(row.context_window) ?? positiveNumber(row.max_context_window);
173
189
  return {
174
190
  slug,
@@ -183,6 +199,9 @@ function parseModel(value) {
183
199
  additionalSpeedTiers,
184
200
  serviceTiers,
185
201
  ...defaultServiceTier === undefined ? {} : { defaultServiceTier },
202
+ useResponsesLite,
203
+ ...defaultVerbosity === undefined ? {} : { defaultVerbosity },
204
+ ...instructionsTemplate === undefined ? {} : { instructionsTemplate },
186
205
  ...contextWindow === undefined ? {} : { contextWindow },
187
206
  inputModalities,
188
207
  };
package/lib/index.d.ts CHANGED
@@ -7,12 +7,23 @@ import { CODEX_PROVIDER } from './native-adapter.js';
7
7
  export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
8
8
  export { normalizeUsage } from './usage.js';
9
9
  export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
10
- /** Persisted OAuth credential. */
10
+ /** Persisted OAuth credential for one ChatGPT account. */
11
11
  export interface OpenAICodexCredential {
12
12
  access: string;
13
13
  refresh: string;
14
14
  expires: number;
15
15
  accountId: string;
16
+ email?: string;
17
+ }
18
+ /** Current multi-account credential document. */
19
+ export interface OpenAICodexCredentialDocument {
20
+ version: 2;
21
+ currentAccountId: string | null;
22
+ accounts: OpenAICodexCredential[];
23
+ }
24
+ interface ParsedCredentialDocument {
25
+ document: OpenAICodexCredentialDocument;
26
+ migrated: boolean;
16
27
  }
17
28
  /** Plugin configuration. */
18
29
  export interface Config {
@@ -73,6 +84,8 @@ interface TokenResponse {
73
84
  id_token?: unknown;
74
85
  }
75
86
  declare function parseTokenResponse(value: TokenResponse | null, previous?: OpenAICodexCredential): OpenAICodexCredential;
87
+ declare function parseCredentialDocument(text: string, filename: string): ParsedCredentialDocument;
88
+ declare function currentCredential(document: OpenAICodexCredentialDocument | undefined): OpenAICodexCredential | undefined;
76
89
  declare function parseAuthority(authority: string | undefined): URL | undefined;
77
90
  declare function isLoopbackHostname(hostname: string): boolean;
78
91
  declare function isTrustedHost(authority: string | undefined, trustedHosts: readonly string[]): boolean;
@@ -85,6 +98,8 @@ declare function pollDeviceAuthorization(device: DeviceAuthorization, signal?: A
85
98
  declare function resolveCodexRouteStatus(config: NativeRouteConfig, llm: RouteRuntime | undefined): CodexRouteStatus;
86
99
  export declare const internals: {
87
100
  parseTokenResponse: typeof parseTokenResponse;
101
+ parseCredentialDocument: typeof parseCredentialDocument;
102
+ currentCredential: typeof currentCredential;
88
103
  parseAuthority: typeof parseAuthority;
89
104
  isLoopbackHostname: typeof isLoopbackHostname;
90
105
  isTrustedHost: typeof isTrustedHost;
@@ -117,6 +132,8 @@ export declare class OpenAICodexAuth extends Service {
117
132
  private usageError;
118
133
  private usageRefresh;
119
134
  private usageGeneration;
135
+ private accountUsageRequestGeneration;
136
+ private readonly accountUsageCredentialRefreshes;
120
137
  private directUsageSequence;
121
138
  private directUsageAccountId;
122
139
  private usageHasDirectDefault;
@@ -138,10 +155,12 @@ export declare class OpenAICodexAuth extends Service {
138
155
  private restorePublishedCredential;
139
156
  private publicationChangedError;
140
157
  private failAfterPublicationRollback;
158
+ private commitDocument;
159
+ private upsertCurrentCredential;
141
160
  private commitCredential;
142
- private resolveManagedCredentialLocked;
143
- /** Return a valid managed bearer token, refreshing and persisting it when near expiry. */
161
+ /** Return the current managed bearer token, refreshing and migrating it when needed. */
144
162
  bearerToken(signal?: AbortSignal): Promise<string | undefined>;
163
+ private managedCredential;
145
164
  private externalNativeCredential;
146
165
  private resolveNativeCredential;
147
166
  private nativeRecoveryError;
@@ -154,8 +173,24 @@ export declare class OpenAICodexAuth extends Service {
154
173
  private beginBrowserLogin;
155
174
  private createBrowserLogin;
156
175
  private cancelLogin;
176
+ private resetCurrentAccountState;
177
+ private setCurrentAccount;
178
+ private logoutAttempt;
157
179
  private logout;
158
180
  private status;
181
+ private accountRefreshJournalFilename;
182
+ private readAccountRefreshJournal;
183
+ private writeAccountRefreshJournal;
184
+ private clearAccountRefreshJournal;
185
+ private removeDeadFileLock;
186
+ private withAccountRefreshLock;
187
+ private withAccountRefreshLocks;
188
+ private reconcileAccountRefreshJournal;
189
+ private refreshManagedAccount;
190
+ private persistAccountUsageCredential;
191
+ private refreshAccountUsageCredential;
192
+ private accountUsageCredential;
193
+ private accountUsages;
159
194
  private fetchUsage;
160
195
  private write;
161
196
  private sendJson;
@@ -163,12 +198,16 @@ export declare class OpenAICodexAuth extends Service {
163
198
  private trustedManagementRequest;
164
199
  private requireCsrf;
165
200
  private handleStatus;
201
+ private handleAccountUsage;
166
202
  private handleDeviceStart;
167
203
  private handleBrowserStart;
168
204
  private handleBrowserPrepare;
169
205
  private handleBrowserComplete;
170
206
  private handleCallback;
171
207
  private handleCancel;
208
+ private accountIdBody;
209
+ private handleSetCurrentAccount;
210
+ private handleAccountLogout;
172
211
  private handleLogout;
173
212
  }
174
213
  export default OpenAICodexAuth;