Skip to content

feat(accounts): allow setting web access token for reset credits on PAT accounts - #161

Closed
2han9wen71an wants to merge 4 commits into
zyycn:mainfrom
2han9wen71an:feat/account-web-token-for-reset-credits
Closed

2han9wen71an wants to merge 4 commits into
zyycn:mainfrom
2han9wen71an:feat/account-web-token-for-reset-credits

Conversation

@2han9wen71an

Copy link
Copy Markdown
Contributor

Summary

为支持通过 at-xx(Codex Personal Access Token / PAT)进行 API 调用的账号单独填入网页 Access Token(Bearer ey...)以使用主动额度重置卡(Rate Limit Reset Credits),无需通过重新授权覆盖原有的 PAT 凭据。

背景与问题

  1. API 调用与重置卡接口权限分离:
    • 模型推理请求(如 /codex/responses)支持使用 PAT(at-xx);
    • 但 OpenAI 的额度重置卡查询与消费接口(/wham/rate-limit-reset-credits)是 ChatGPT Web 端的特权端点,只接受 ChatGPT Web Access Token(Bearer ey... 格式的 JWT),不接受 at- 令牌。
  2. 401 自动刷新死循环:
    • 打开重置卡弹窗时,CPR 携带原 at- 凭据请求上游,上游返回 401 Unauthorized;
    • 系统收到 401 后尝试自动 refresh 令牌,但 PAT 账号没有 refresh_token,直接报错 "账号没有刷新令牌,请重新授权"。
  3. 重新授权的破坏性:
    • 此前系统中更新该账号凭据的唯一入口是操作菜单里的“重新授权”,但这会把整个账号转换为 Web 授权码生成的 OAuth 凭据,彻底覆盖并丢失原有的 at- 令牌。

解决方案

  1. 凭据模型扩展与解耦:
    • 在 CodexOAuthCredentialData 与 CodexOAuthSecret 中新增可选的 web_access_token 字段(持久化在 PostgreSQL JSONB 中,天然兼容已有数据)。
    • 在 CodexRuntimeAuthentication 中新增 reset_credits_authorization_header:专用于额度重置卡接口,优先使用 web_access_token,回退使用主 access_token。API 调用仍严格使用原有 access_token(at-xx),互不影响。
  2. 前置阻断与精准错误提示:
    • list_reset_credits / consume_reset_credit 前置检测:若为 at- 账号且未配置 web_access_token,直接返回提示需配置网页 Access Token,避免上游 401 无谓请求;
    • 若网页 Token 过期返回 401,对无 refresh_token 账号精准提示 "网页 Access Token 已过期或失效,请重新填入网页 Access Token",阻断失败的自动刷新。
  3. 独立维护 API:
    • 新增 POST /api/admin/accounts/web-token,接收 { accountId, webAccessToken };
    • 后端对 Token 进行前缀清理、长度校验、JWT 解析与过期时间预检;
    • 通过 CAS 原子更新凭据,保留原 PAT、代理及所有调度属性。
  4. 前端双入口体验:
    • 重置卡弹窗就地输入(ResetCredits.vue):检测到未配置或过期提示时,弹窗内就地展开“配置网页 Access Token”卡片,支持一键“保存并查询”立即加载并兑换卡片;并在标题栏提供 Globe 图标随时主动展开配置。
    • 表格操作菜单独立维护(AccountTableActions.vue + AccountWebTokenModal.vue):在操作菜单中新增“配置网页 Token”项,提供独立弹窗支持查看、更新或清除 Token。

Verification

  • cargo check --workspace 编译通过
  • cargo clippy -p provider-openai -p gateway-admin -p gateway-api 0 警告
  • cargo test -p gateway-admin(171 个测试全过)
  • cargo test -p gateway-api(354 个测试全过,包含新增的 web-token 路由与校验测试)
  • cargo test -p provider-openai credential::(201 个测试全过,包含凭据隔离与 CAS 更新测试)
  • pnpm run typecheck(TypeScript 严格模式 0 错误)
  • pnpm run build(Vite 生产构建成功)

@2han9wen71an
2han9wen71an force-pushed the feat/account-web-token-for-reset-credits branch from 9746aa9 to 8797270 Compare September 18, 2026 13:25
@2han9wen71an
2han9wen71an force-pushed the feat/account-web-token-for-reset-credits branch from 8797270 to 45ae692 Compare September 18, 2026 13:42
@zyycn

zyycn commented Sep 18, 2026

Copy link
Copy Markdown
Owner

感谢提交这个 PR,也感谢把遇到的场景和处理思路整理得这么详细。

目前我这边暂时没有这类 at- 格式的 Token 可用于复现,所以还无法独立确认“PAT 不能调用重置卡接口、必须额外配置网页 Token”这个前提,想请你再帮忙补充一下实际验证。

我看了一下 sub2api 的实现,可以作为一个参考:它的重置卡请求仍使用账号原有的 access_token,没有额外引入网页 Token;同时会将 PAT 排除在 OAuth 自动刷新之外。当然,源码中的处理方式本身还不能证明所有账号都能正常使用,最终仍需要以真实上游的行为为准。

能否先参考这个方向,核对一下请求头、账号标识和 PAT 的刷新处理,并在可用于测试的真实账号上验证重置卡查询与使用?如果方便,也请补充修复前后的复现步骤、版本、出站环境以及脱敏后的 HTTP 状态和必要错误信息,帮助区分凭据权限问题与请求实现差异;不需要提供真实 Token、Cookie 或其他敏感凭据。

如果实测确认这类账号确实需要独立的网页 Token,我们再基于验证结果讨论双凭据方案,会更容易确认改动范围和维护方式。辛苦了!

@2han9wen71an

Copy link
Copy Markdown
Contributor Author

感谢作者的详细说明!这里澄清一下核心场景与 sub2api 机制上的区别:

1. 为什么 sub2api 可以直接复用原来的 token?

在 sub2api 语境下,大家导入的所谓 access_token,本质上是从网页端(chatgpt.com/api/auth/session)复制提取出的 Session Access Token(ey... 开头的 JWT)。因为这个 Token 拥有完整的用户 Session 权限,所以直接请求 /wham/rate-limit-reset-credits 是天然合法的,无需额外配置。

2. CPR 中 at-(Personal Access Token)账号的特殊性

在 CPR 中,支持直接使用 Personal Access Token(以 at- 开头的 PAT) 导入账号:

  • 没有 OAuth 凭据集:这类账号没有走 OAuth 认证流程,因此没有标准 OAuth 产生的 Session Token 和 Refresh Token,仅有这一串用于调用模型 API(如 /codex/responses)的 at- 令牌;
  • 重置卡接口的要求:重置卡接口(wham/rate-limit-reset-credits)要求完整的 OAuth Session 鉴权(官方 Codex Desktop 客户端和网页端之所以能用,都是因为持有 ey... 格式的标准 Session Token)。如果直接携带 at- 令牌请求该接口,上游会直接返回 401 Unauthorized;
  • 自动刷新的死循环:CPR 遇到 401 会自动尝试 refresh,但由于 PAT 账号本身就没有 Refresh Token,就会抛出截图中的 “账号没有刷新令牌,请重新授权”。

3. 为什么需要单独填入网页 Token,而不是重新授权?

  • 如果点击“重新授权”,会强制走浏览器的完整 OAuth 授权流程,这虽然能拿到 Session Token,但会把账号原本用于 API 调用的 at- 凭据彻底覆盖替换掉,无法再保持以纯 PAT 接入的调用方式;
  • 用户平时在网页端登录使用,直接从网页 session 复制一份 Access Token 是最便捷的方式;
  • 因此,对于以 at- 接入的账号,允许单独填入一个专用于重置卡的网页 Access Token(ey...),既能使用重置卡,又能保证主凭据 at- 不被覆盖破坏。

希望这个细节补充能解答您的疑惑!如果您对凭据管理或交互实现有更好的建议,我们随时沟通调整。

@zyycn

zyycn commented Sep 19, 2026

Copy link
Copy Markdown
Owner

谢谢补充,保留原有 PAT、不因配置重置卡而覆盖主凭据的需求可以理解。我又沿着 sub2api 的导入、凭据保存和重置卡请求链路核对了一遍,有一点需要澄清:从 /api/auth/session 复制 Access Token 只是其中一种来源,不能概括 sub2api 的 access_token 字段。

下面仍引用上一条评论中的固定提交 efe9aab;这些核心后端文件与目前主分支 1a9d49e 没有差异:

  • sub2api 同样支持原始 at- PAT。 PAT 校验函数通过 whoami 校验后,直接返回输入的 accessToken,没有将其兑换成网页 Token;凭据构建函数再将它保存为 credentials["access_token"],并标记 PAT 认证模式。
  • OAuth 授权也是独立来源。 授权码交换逻辑走 authorization_code 流程,其返回的 Access Token 同样进入这个字段,并不需要用户复制网页 session 数据。
  • 导入器也区分 Access Token 与 Session Token。 导入解析同时支持 Codex OAuth auth.json 中的 tokens.access_token 和顶层 accessToken 等字段;遇到 sessionToken 会明确忽略,不把它当成 OAuth refresh_token。讨论时最好分别使用 Access Token、Session Token 和 Refresh Token,避免混淆。

重置卡路径仍通过共享 TokenProvider 获取账号原有凭据,而 TokenProvider 明确跳过 PAT 的 OAuth 自动刷新。因此,“sub2api 能复用原 Token,是因为使用的都是网页 Session Access Token”还不足以解释这里的差异。

当然,上述源码也不能证明真实 PAT 一定能成功查询或消费重置卡。我这边仍没有可用于独立复现的 PAT,所以希望补充的是同一账号、相同出站环境及必要请求头下的实测对照:原始 PAT 与网页 Access Token 查询重置卡时各自的 HTTP 状态和脱敏错误,以及已有的消费验证结果,并注明版本、账号标识和请求头的处理方式。无需提供真实 Token、Cookie,也不必为了验证额外消耗重置卡。

如果对照结果确认请求实现无误、这类 PAT 确实缺少重置卡接口权限,再讨论独立网页 Token 的保存、过期和更新方式,就有明确依据了。感谢帮忙核实!

@2han9wen71an

Copy link
Copy Markdown
Contributor Author

感谢作者的严谨分析!为了给架构设计提供明确的依据,我在服务器上针对真实账号和相同网络出站环境下,对重置卡只读查询端点进行了完整的对比测试(仅调用 GET 查询端点,未触发消费):

1. 测试环境

  • 测试端点:GET https://chatgpt.com/backend-api/wham/rate-limit-reset-credits
  • 网络出站:同一固定 US 出口 IP
  • 请求头:完全对齐(均携带 chatgpt-account-id、User-Agent: Codex-Proxy-RS、Accept: application/json)

2. 测试组 A:使用原始 PAT (at-xxxx)

  • 请求报文:
    GET /backend-api/wham/rate-limit-reset-credits HTTP/2
    Host: chatgpt.com
    Authorization: Bearer at-***[REDACTED]***
    chatgpt-account-id: 28b90dfd-***[REDACTED]***
    User-Agent: Codex-Proxy-RS
    Accept: application/json
  • 上游实际响应:
    HTTP/2 401 Unauthorized
    Content-Type: text/plain
    x-openai-authorization-error: 401
    x-openai-ide-error-code: no_matching_rule
    x-openai-internal-caller: unknown_through_ide
    CF-RAY: a3d86f7afc82c132-SJC
    
    {
      "error": {
        "message": "Unauthorized",
        "type": "rejected_by_access_enforcement",
        "code": "no_matching_rule",
        "param": null
      },
      "status": 401
    }
  • 分析:
    上游返回了明确的 "type": "rejected_by_access_enforcement" 与 "code": "no_matching_rule"。这证明在 OpenAI 的边缘网关访问控制策略中,at-(IDE/CLI PAT)的鉴权规则库未包含 /wham/ 路径,属于上游确定的凭据权限隔离,并非客户端缺少 Header。

3. 测试组 B:使用 OAuth Session Token (ey-xxxx) 作为对照

  • 请求报文:
    GET /backend-api/wham/rate-limit-reset-credits HTTP/2
    Host: chatgpt.com
    Authorization: Bearer ey***[REDACTED]***
    chatgpt-account-id: 9648ea1a-***[REDACTED]***
    User-Agent: Codex-Proxy-RS
    Accept: application/json
  • 上游实际响应:
    HTTP/2 200 OK
    Content-Type: application/json
    CF-RAY: a3d89851...-SJC
    
    {
      "credits": [
        {
          "id": "RateLimitResetCredit_***[REDACTED]***",
          "reset_type": "codex_rate_limits",
          "is_supported_by_plan": true,
          "status": "available",
          "granted_at": "2026-09-04T02:38:35.230386Z",
          "expires_at": "2026-10-04T02:38:35.230386Z",
          "title": "Full reset"
        }
      ],
      "available_count": 2,
      "total_earned_count": 0,
      "immediate_reset_purchase_eligible": false,
      "history_enabled": true
    }

4. 结论与 sub2api 差异说明

  1. 实测证实:无论 Header 如何设置,上游网关均拒绝 at- 令牌访问 /wham/ 端点(返回 no_matching_rule),只有包含完整 Session 权限的 ey... Token 才能访问重置卡接口;
  2. sub2api 的实际情况:sub2api 源码中统一使用 access_token,是因为其设计初期未考虑对以纯 at- 接入账号使用重置卡的场景;在 sub2api 中若账号为 PAT,调用重置卡同样会因该上游网关规则而失败;
  3. 方案必要性:由于通过“重新授权”会使用浏览器 OAuth 凭据彻底覆盖原有的 at- PAT,为了让 PAT 账号能够使用重置卡且不破坏原有的 API 调用凭据,单独提供一个专用于重置卡的 Web Token 是目前最合理且最小侵入的解决方案。

@zyycn
zyycn force-pushed the main branch 2 times, most recently from 45fd212 to c79e032 Compare September 19, 2026 17:14
2han9wen71an added a commit to 2han9wen71an/codex-proxy-rs that referenced this pull request Sep 22, 2026
profiles/me and the reset-credits endpoints share the same account-scoped
auth; prefer the configured web access token and stop rejecting accounts
whose primary access token expired but carry a valid web session token.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants