2373 Commits
Author SHA1 Message Date
shaw 0dec1ad292 fix(service): 消除 isOpenAIGPT56Model 重复声明,统一到 openai_model_alias.go
PR #3898 与 #3909 各自新增了同名函数(billing 精确匹配版 / gateway
前缀匹配版),合并后 internal/service 编译失败。保留前缀匹配语义作为
唯一实现:对 normalizeKnownOpenAICodexModel 的输出(gpt-5.6-* 精确
基名)canonicalize 幂等且精确分支命中,计费行为不变;同时保住
gpt-5.6-*-max 等后缀变体的识别。
2026-07-10 14:09:07 +08:00
Wesley Liddick 8b96acde97 Merge pull request #3898 from Arron196/feat/gpt-5.6-cache-billing-stats
feat(openai): 支持 GPT-5.6 系列缓存写入计费与统计
2026-07-10 13:57:29 +08:00
li b9b013a088 fix(usage): WS passthrough effort 提取补入映射后模型候选
Fixes #3924
2026-07-10 11:56:47 +08:00
Wesley Liddick ddb1a210ce Merge pull request #3912 from superman2003/fix/audit-concurrency-and-recovery
fix: harden billing concurrency and payment recovery
2026-07-10 11:38:45 +08:00
shaw c3ae5fc3c5 fix(usage): effort 提取改用模型候选列表,修复后缀模型元数据丢失
#3909 将 effort 提取的模型参数改为 firstNonEmpty(upstreamModel, ...),
而 OAuth 的 normalizeCodexModel 会剥掉模型 effort 后缀
(gpt-5.4-xhigh -> gpt-5.4),导致后缀式模型且 body 无显式
reasoning 字段的请求在 usage 元数据中丢失 effort。

extractOpenAIReasoningEffortFromBody/extractOpenAIReasoningEffort
改为接受模型候选变参:显式 effort 的 max 保留判定沿用第一个非空
候选(映射后模型),后缀推导回退依次尝试每个候选(原始模型名兜底)。
纯元数据修复,不改变任何转发行为。
2026-07-10 11:09:06 +08:00
Wesley Liddick 72ef0b3884 Merge pull request #3909 from githubbzxs/fix/gpt56-max-compact
fix: support GPT-5.6 max reasoning effort
2026-07-10 10:57:35 +08:00
superman2003 fc66a30ffc fix: harden billing concurrency and payment recovery 2026-07-10 10:56:49 +08:00
Wesley Liddick 066a85949a Merge pull request #3905 from fengshao1227/fix/codex-client-version-bump
fix(openai): 升级 Codex 客户端版本至 0.144.1,修复 gpt-5.6-luna 404
2026-07-10 10:43:51 +08:00
psyche 80b3d4c1f8 fix: 兼容 GPT-5.6 max 推理强度 2026-07-10 10:39:16 +08:00
shaw 000f6dc655 fix(compact): 终态 output 非空但缺 compaction 时从事件流补入(146 等价性收尾)
146 纯流式透传下 Codex 只从 raw output_item.done 收集 item,无论终态
output 形态如何都能拿到 compaction;SSE→JSON 提取链路此前仅在终态 output
为空时修补。补齐最后一处不等价:compact 请求终态 output 非空但缺
compaction、事件流中存在(done 优先 added 兜底)时以 raw JSON 追加。
门控:仅 compact 路径 + 仅缺失时触发,已含 compaction 或非 compact
请求逐字节不变。

Refs #3887 #3777
2026-07-10 10:25:09 +08:00
shaw ae9a01d852 fix(compact): 二轮审计加固——心跳字节不得污染 failover 判定与并发写安全
对首轮修复的对抗式审计发现并修复以下问题:

1. failover 判定污染(真实回归风险):handler 以 "Forward 前后
   c.Writer.Size() 是否变化" 判定响应是否已写出并据此放弃换号。心跳注释
   字节会使该判定恒真,compact 请求一旦在上游等待期间发过心跳,上游
   429/5xx 将不再 failover。新增
   OpenAICompactKeepaliveAdjustedWrittenSize(扣除心跳字节、互斥锁下
   一致读取、仅心跳字节归一化为未写哨兵 -1),快照、failover 比较与
   openAIForwardErrorAlreadyCommunicated 三处判定统一改用该口径;无心跳
   请求完全等价于原 c.Writer.Size()。

2. 并发写竞争:心跳 goroutine 与未被显式拦截的写回路径(Forward 内部
   本地拒绝等)存在 ResponseWriter 数据竞争。StartOpenAICompactSSE-
   Keepalive 现将 c.Writer 替换为 openAICompactKeepaliveWriter:写侧
   方法(Header/Write/WriteString/WriteHeader/WriteHeaderNow/Flush)
   先在互斥锁下停拍,读侧(Status/Size/Written)仅加锁不停拍——任何
   请求侧响应构造与心跳从构造上互斥,热路径状态读取不误杀心跳。

3. 语义拦截补齐:rejectIfCyberSessionBlocked(在用户槽位长等待之后
   执行的直接 c.JSON)与 writeOpenAIFastPolicyBlockedResponse 在心跳
   提交后降级为 response.failed 终止事件;未提交时先停拍再写 JSON,
   状态码语义不变。失败事件 errType 参数化(permission_error 等)。

4. reconstruct 混合形态:done 事件存在但 compaction 只在
   output_item.added 中时也要补入;done 已含 compaction 时跳过 added,
   避免无 id 可去重时收集两份(Codex 要求恰好一个)。

5. 观测性:logOpenAIRemoteCompactOutcome 对心跳提交后的失败(wire 200)
   以 GetOpsStreamError 纠正 outcome,不再误记 succeeded。

新增 5 个测试:failover 口径不变式、包装器停拍语义(-race)、fast
policy 提交前后两态、混合 done/added 形态(含去重)。

Refs #3887 #3777
2026-07-10 10:18:16 +08:00
benjamin 0a5f34a2ef fix(openai): recognize Windows websocket resets 2026-07-10 10:11:21 +08:00
benjamin 062af81fb5 fix(openai): preserve explicit GPT-5.6 cache write prices 2026-07-10 10:00:47 +08:00
shaw 2cffe1cf5f fix(compact): SSE→JSON 保留 raw output_item.done 并为 unary 等待补下游心跳(修复 #3887)
#3887 报告 v0.1.149(已含 #3880 桥接修复)remote compact 仍失败且持续
计费。核实为 #3880 明示的两个遗留:

1. 上游对 unary compact 返回 SSE 且 compaction item 只出现在 raw
   response.output_item.done、终态 completed.response.output 为空
   (#3777 实录形态)时,reconstructResponseOutputFromSSE 只累加
   text/function_call/reasoning 三类 delta,compaction item 被丢弃,
   桥接合成 0 个 output_item.done,Codex 报 "expected exactly one
   compaction output item, got 0" 后盲目重试,每次重试重新消耗上游
   compact 配额。
2. 桥接为全缓冲写回:上游 unary 完成前(大上下文可达数分钟,且上游
   处理期间不发送任何字节)下游连响应头都收不到,反向代理
   (Nginx/Cloudflare Tunnel)空闲超时掐断连接同样触发盲重连
   (同类问题见 #2243/#2976)。

修复:

- reconstructResponseOutputFromSSE 优先以 raw JSON 逐字节收集
  output_item.done item(协议上的最终完整形态),不经窄结构体,
  encrypted_content/summary/opaque 等 compact 专属字段全部保留;
  无 done 事件时退回收集 output_item.added 中的 compaction 类 item;
  两者皆无才回到原 delta 重建。path-based v1 JSON 写回同样受益。
- 新增 openAICompactSSEKeepalive:body-signal 客户端流式 compact 在
  上游等待期间按 gateway.stream_keepalive_interval 向下游写 SSE 注释
  行心跳(eventsource 解析层直接忽略)。首拍延迟一个间隔,快速失败
  仍走 JSON+状态码;首拍后状态码固化为 200,桥接/错误链路
  (writeOpenAICompactSSEBridge、errorResponse、
  handleStreamingAwareError、ensureForwardErrorResponse、
  writeOpenAINonStreamingProtocolError)统一降级为 response.failed
  终止事件并标记 ops 流内错误。
- API-key 账号的 compact 上游请求也强制 accept: application/json
  (#3777 期望行为 4;透传白名单原会放行客户端的 text/event-stream)。

测试:#3777 实录形态经 handleSSEToJSON / 透传 / path-based 三条链路
的修补断言;raw-done 优先不与 delta 重复;added 回退门控;心跳提交、
提交后 2xx 续写、提交后失败降级、未提交行为不变;-race 通过。

Fixes #3887
Refs #3777 #3875 #3880
2026-07-10 09:56:56 +08:00
li 657c4f97d9 fix(openai): 升级 Codex 客户端版本至 0.144.1,修复 gpt-5.6-luna 404
Fixes #3895
2026-07-10 09:56:33 +08:00
benjamin 383f61d0e9 fix(openai): align GPT-5.6 billing with official pricing 2026-07-10 09:42:31 +08:00
benjamin 4a2b10c94e feat(openai): support GPT-5.6 cache write billing 2026-07-10 09:08:58 +08:00
Wesley Liddick dd1a116f48 Merge pull request #3881 from Wei-Shaw/feat/version-rollback
feat: 版本徽章新增近3个历史版本在线回退与手动回退指引
2026-07-09 22:39:41 +08:00
shaw 92b283d9eb fix: 修复 SSE 桥接 appendEvent 中 errcheck 告警 2026-07-09 22:34:13 +08:00
shaw 1c2e6503c6 feat: 版本徽章新增近3个历史版本在线回退与手动回退指引
后端:
- UpdateService 新增 ListRollbackVersions/RollbackToVersion,回退目标强制
  限定为比当前版本更旧的近3个正式版本(排除当前/更新/预发布/草稿),
  越界返回 400 ROLLBACK_VERSION_NOT_ALLOWED;下载复用既有 HTTPS 域名
  白名单 + checksum 校验 + 原子换二进制管线
- GitHubReleaseClient 新增 FetchRecentReleases(releases 列表 API)
- 新增 GET /admin/system/rollback-versions;POST /admin/system/rollback
  支持可选 {"version"} body,无 body 保持原 .backup 本地回退行为不变
- 端点均在 /admin 组 adminAuth(IsAdmin+TokenVersion)保护内

前端:
- VersionBadge「已是最新版本」状态下新增版本回退面板:radio 卡片选择
  近3个版本,手动回退方式按部署形态 tab 切换(脚本部署 curl 命令 /
  Docker 镜像 tag 指引),支持一键复制;release 构建提供一键回退并
  复用重启流程;源码构建仅提示不支持在线回退
- 下拉根元素重置 whitespace-normal,修复 sidebar-brand 的
  white-space:nowrap 继承导致的长文本溢出裁切

测试:服务层过滤/排序/上限/非法目标拒绝、handler 双模式与鉴权错误映射、
GitHub client 列表拉取、前端 API 请求体行为共 16 个新增用例
2026-07-09 22:30:04 +08:00
shaw 16c0613b21 fix(compact): body-signal 客户端流式请求的响应合成回 SSE
v0.1.147 的 body-signal 提升(#3804)把 Codex remote compact v2 的流式
/responses 请求改写为上游 unary /responses/compact(JSON),但把 JSON
文档原样写回给了按 Responses SSE 协议消费的客户端。Codex 只从
response.output_item.done 收集 item 且必须收到 response.completed,
收到 JSON 后报 "stream disconnected before completion: stream closed
before response.completed" 无限重连,每次重连再白烧一次上游 compact
配额(#3875,v0.1.146 无提升逻辑故正常)。

修复保留提升的全部收益(requireCompact 调度过滤、compact 模型映射、
白名单归一化、上游 unary JSON),补上协议转换缺失的响应半程:

- handler 在 body-signal 提升时记录原始 body 的 stream:true 意图
  (MarkOpenAICompactClientStream);path-based 请求不标记,Codex v1
  unary 协议与链式 sub2api 的 JSON 写回行为保持不变。
- 四个非流式写回点(handleNonStreamingResponse / handleSSEToJSON /
  handleNonStreamingResponsePassthrough / handlePassthroughSSEToJSON)
  对已标记的 2xx JSON 响应经 writeOpenAICompactSSEBridge 合成最小
  Responses SSE:每个 output[] item 一条 response.output_item.done
  (原始字段逐字节保留),最后一条 response.completed 携带完整
  response 对象。
- 按 codex-rs 解析器硬约束兜底:先 json.Compact 防 pretty JSON 换行
  破坏 SSE 帧;response.id 缺失时注入 resp_*(必填 string);usage
  缺 input_tokens/output_tokens/total_tokens 整数字段时整体删除,
  避免整条 completed 事件解析失败。
- 非 2xx 一律保持 JSON 原样,Codex 依赖 HTTP 状态码走重试链路。

新增 9 个 service 测试 + 4 个 handler 测试覆盖合成形态、id/usage 兜底、
path-based 不受影响、主链路/透传链路/上游 SSE 提取再合成三条端到端。

Fixes #3875
Refs #3777
2026-07-09 21:40:51 +08:00
shaw 7918b1a9c5 fix: 落实 #3867-#3870 合并审计的全部跟进项
透传规则(跟进 #3868/#3870,refs #3857):
- CC/Messages 4 条协议转换路径改用共享 helper,走语义状态推断 +
  body 归一化,使按错误码配置的透传规则也能命中(原先传 0 恒不命中)
- helper 增加 platform 参数:本服务同时承载 openai/grok 平台账号,
  规则须按 account.Platform 匹配,消除硬编码平台错配
- /v1/responses 两条路径命中透传规则时补记 ops 上游错误事件,
  对齐 CC/Messages 与 antigravity 先例,消除监控盲区

用户角色管理(跟进 #3869):
- 补齐 EN 语言包缺失的 admin.users.form.roleLabel
- 新增"最后一个管理员不可降级"守卫,覆盖跨管理员互降致零 admin 锁死
- 角色变更/创建管理员落审计日志(含操作者 actor_admin_id)
2026-07-09 20:06:09 +08:00
Wesley Liddick 10102807e2 Merge pull request #3869 from Birditch/feat/admin-user-role-and-token-ranking
feat(admin): 用户角色管理 + 用户 Token 排行
2026-07-09 19:50:01 +08:00
Wesley Liddick 751cf5eea3 Merge pull request #3870 from fengshao1227/fix/response-failed-passthrough-rules
fix(gateway): CC/Messages 路径 SSE response.failed 应用错误透传规则
2026-07-09 19:49:32 +08:00
Wesley Liddick a8505877ff Merge pull request #3868 from wucm667/fix/openai-sse-response-failed-error-passthrough
fix: OpenAI SSE response.failed 应用错误透传规则
2026-07-09 19:49:04 +08:00
li 8f97953e51 fix(gateway): SSE response.failed 应用错误透传规则,不再硬编码 502
Fixes #3857
2026-07-09 18:09:37 +08:00
BirditchandClaude Opus 4.8 64fdc11ec4 feat(admin): 用户创建/编辑支持选择与修改角色 (user/admin)
- 创建用户时可指定角色,缺省仍为 user,不再硬编码为普通用户
- 编辑用户时可在 user/admin 之间切换角色
- 后端对角色做 admin/user 合法性校验
- 防锁死保护:管理员不能把自己降级为普通用户(与既有"不能禁用/删除 admin"保护一致;降级其他管理员仍允许)
- 前端创建/编辑弹窗新增角色下拉,复用既有 i18n 文案
- 新增角色创建/更新/非法值/防降级单元测试

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:04:06 -04:00
wucm667 1da3501af5 fix: apply error passthrough to OpenAI response.failed streams 2026-07-09 17:59:38 +08:00
Heatherm Huang 1a0a6ea9a8 fix: stabilize Grok quota probe model 2026-07-09 17:58:31 +08:00
Heatherm Huang b480545c19 fix: improve Grok OAuth, image, and usage flows 2026-07-09 17:54:08 +08:00
shaw 49fb18e073 Merge origin/main into worktree/brave-valley-9578
Resolve conflict in api_key_repo.go: keep both ListAllByUserID (this PR)
and attachLastUsedIPs/latestUsageLogIPs (main), and have ListAllByUserID
attach last-used IPs so the current_concurrency sort path keeps the
last_used_ip column populated.
2026-07-09 17:17:16 +08:00
li 104fd2b6ec fix(messages): /v1/messages 非 cyber response.failed 补全 failover 和错误回写 2026-07-09 16:49:13 +08:00
Wesley Liddick 0438057c0b Merge pull request #3816 from Vibeone/fix/ops-inband-sse-error-logging
fix(ops): 记录固化 200 SSE 流上的就地错误,修复流内限流不进错误看板
2026-07-09 16:35:29 +08:00
Wesley Liddick 843dcddea3 Merge pull request #3853 from fengshao1227/fix/messages-transport-failover
fix(messages): /v1/messages 传输层错误对齐 failover 链路
2026-07-09 16:34:00 +08:00
Wesley Liddick c842c3166c Merge pull request #3847 from heathermhuang/codex/grok-45-official-support
Add official Grok 4.5 support
2026-07-09 16:07:42 +08:00
Heatherm Huang 243678e166 Fix Grok 4.5 alias test expectations 2026-07-09 15:50:58 +08:00
li 7468427e44 fix(messages): /v1/messages 传输层错误对齐 failover 链路,不再直接 502
Fixes #3850 (part 2)
2026-07-09 15:49:10 +08:00
shaw d4952154ff fix: bill Grok video per second and harden video usage logging
Follow-up fixes for the #3775 audit findings:

- Bill Grok video generation per second of output, matching the xAI rate
  card: parse the request duration (1-15s, upstream default 8s) and compute
  cost as per-second price x duration x count. The built-in rate card values
  were already xAI per-second prices but were previously charged per video,
  undercharging up to 15x with a user-controlled duration.
- Group video_price_* fields are now documented and surfaced as per-second
  rates (USD/s); admin UI labels, placeholders and hints updated accordingly.
- Persist video_count/video_resolution/video_duration_seconds on usage_logs
  (migration 172) so video billing is auditable, and exempt any row with
  video_count > 0 from the image_size check constraint: a video billed via a
  token-mode channel price produces billing_mode='token' with image_count=1
  and no image_size, which the previous constraint rejected, dropping the
  whole billing transaction.
- Only refetch the group in apiKeyWithFreshGroupMediaPricing when the group
  object actually looks like it is missing media pricing fields (both media
  multipliers zero and all prices nil, impossible for a normally loaded
  group), removing a per-usage DB query for groups without overrides.
- Frontend: drop the unused admin.groups.mediaPricing locale block, map
  cleared price inputs to null (create) / -1 (update, cleared via backend
  normalizePrice) instead of sending "" that failed *float64 unmarshalling,
  and align video price placeholders with the text-to-video default model
  (grok-imagine-video 0.05/0.07, 1080p only on 1.5 at 0.25).
2026-07-09 15:38:59 +08:00
Wesley Liddick 9ba0fb3084 Merge pull request #3775 from heathermhuang/codex/grok-media-pricing-labels
fix: add Grok video pricing controls
2026-07-09 15:03:38 +08:00
Wesley Liddick b6d2df24d8 Merge pull request #3800 from Ge-limin/feat/codex-models-manifest
feat: Codex 客户端模型清单(manifest)透传接口
2026-07-09 14:40:23 +08:00
Wesley Liddick 52da41fd6e Merge pull request #3809 from hongheshan-svg/fix/upstream-anthropic-429-fallback
fix(ratelimit): Anthropic 无 reset 头的 429 也进入兜底冷却,避免账号永不冷却导致的 429 循环
2026-07-09 14:40:10 +08:00
Wesley Liddick 9392d1fc43 Merge pull request #3841 from fengshao1227/fix/html-escape-site-name-and-sanitize-doc-url
fix(security): HTML-escape site_name 并对 doc_url 统一应用 sanitizeUrl
2026-07-09 14:39:26 +08:00
Wesley Liddick a57157b9fa Merge pull request #3822 from wucm667/feat/api-key-last-used-ip
feat(api-key): 展示 API Key 最近使用 IP
2026-07-09 14:38:01 +08:00
Wesley Liddick 105ac31c37 Merge pull request #3781 from fengshao1227/fix/openai-oauth-empty-mapping-model-guard
fix(scheduler): 空 model_mapping 的 OpenAI OAuth 账号不再吸收全部模型
2026-07-09 14:37:33 +08:00
Wesley Liddick 7302be4d13 Merge pull request #3833 from superman2003/fix/security-and-frontend-hardening
fix(gateway,frontend): 修复 Gemini 鉴权绕过与前端支付/会话缺陷
2026-07-09 14:37:19 +08:00
Heatherm Huang cccba9a82e Add official Grok 4.5 support 2026-07-09 14:26:10 +08:00
li bfb827b879 fix(security): HTML-escape site_name 并对 doc_url 统一应用 sanitizeUrl
Refs #3839 (第 8、12 点)
2026-07-09 10:03:49 +08:00
superman2003andCursor 29a5fcd25e fix(gateway,frontend): 修复鉴权绕过与前端支付/会话缺陷
后端:
- Gemini /v1beta 鉴权中间件补齐主中间件的授权校验: API Key 的 IP 白/黑名单、
  专属分组授权、运行时过期/配额二次检查, 修复经 Gemini 端点绕过 IP ACL、
  越权访问专属分组、以及状态未刷新时的配额/有效期绕过窗口。
- 粘性会话等待计划分支改走 newSelectionResult 以 hydrate 账号凭证, 修复调度
  快照中账号凭证被剥离导致等待路径转发鉴权失败。
- SSE 流式转发客户端断开时不再 break 跳过当前事件 usage 合并, 修复少计费。
- Forward 对 nil gin.Context 的防御补齐; 上游错误体读取失败时记录日志避免静默。

前端:
- logout 将本地会话清理移入 finally, 服务端吊销失败也保证本地登出。
- Stripe 弹窗轮询改用正确的 auth_token 键并加防重入; 收到 INIT 后清除兜底
  超时定时器, onUnmounted 清理 message 监听器。
- token 刷新请求补充 30s 超时, 避免挂起导致请求队列与 loading 永久卡死。
- 路由守卫在公共设置未加载时先 await fetchPublicSettings, 避免 payment/
  risk_control 被误判为未启用而错误拦截。
- 支付状态轮询回调补充防重入与终态守卫。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 09:06:56 +08:00
Wesley Liddick 88581912ba test: add regression tests for fallback pricing pollution 2026-07-08 19:23:38 +00:00
Wesley Liddick 4a30397623 fix: prevent channel pricing overrides from mutating shared fallback pricing 2026-07-08 18:37:33 +00:00