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
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
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
InCerry
53a5c45bd8
fix(gateway): cap lenient json normalization
...
Fixes #3540
2026-07-09 11:15:52 +08:00
shaw
a56eb5b4dc
fix(compact): body-signal 提升上移到 handler 层并对齐 path-based 链路
...
合并 main 解决拆分冲突后,将原先 Forward 内的 body-signal 提升重构到
handler 的 compact 归一化入口之前,修复原方案的四个问题:
- reqStream 未重推导:body-signal 原始请求带 stream:true,Forward 级提升
后 compact 上游返回 JSON(Accept: application/json)却被流式 handler
解析,"stream ended before a terminal event" 会触发最多
max_account_switches 次换号 failover,且每次都白烧一次上游 compact 配额;
handler 级提升让白名单归一化先删除 stream,reqStream 自然为 false。
- requireCompact 调度过滤失效:原方案 path 改写发生在 requireCompact 判定
之后,调度器不会过滤不支持 compact 的账号;现在改写先于该判定。
- passthrough / Grok / chat-completions 桥接分支位于 Forward 检测点之前,
passthrough 账号完全无法命中;handler 级改写对所有分支生效。
- body 归一化口径不一致:body-signal 现在与 path-based 一样走白名单归一化
(prompt_cache_key 等一并删除),而非仅依赖 OAuth 黑名单转换。
检测函数导出为 HasCompactionTriggerInInput 供 handler 使用,保留原 PR 的
7 个单测;新增 6 个 handler 级回归测试(提升、codex 别名路由、尾斜杠、
子路径不误伤、path-based 无双重后缀、普通请求不受影响)。
Refs #3777
2026-07-08 10:04:14 +08:00
li
40c563c4ae
fix(gateway): 记录请求体解析失败的真实原因,不再吞错
...
400 "Failed to parse request body" 此前丢弃底层错误,无法区分
JSON 真非法、还是 body 被截断/被中间件提前消费。
- 服务层 invalid json 错误增补 len/offset/非法字符信息
(仅诊断元数据,不含 body 内容,可安全 wrap);
- handler 层新增 logRequestBodyParseFailure,向服务端日志输出
底层错误 + body 长度 + 转义后的 head/tail 片段(各 256B),
客户端响应文案保持不变;
- 接入全部 9 处入站解析点(messages/count_tokens/responses/
chat_completions/embeddings,Anthropic 与 OpenAI 网关)。
Fixes #3715
2026-07-07 13:53:39 +08:00
Wesley Liddick and GitHub
d1d3400b69
Merge pull request #3645 from bestony/worktree/lucky-harbor-dbe4
...
feat(keys): add api key concurrency stats
2026-07-06 16:45:15 +08:00
shaw
0fd2e9216d
fix(scheduler): 修复 OpenAI 高级调度器审计发现的正确性与性能问题
...
针对 #3692 合并后审计发现的问题集中修复:
- previous_response_id 剥离条件改为按 call_id 全覆盖校验,
部分可重建的工具续链不再被误剥离(不受开关门控的行为回归)
- 粘性加权回退路径补分组归属校验并清理失效绑定,杜绝跨分组账号泄漏
- 账号列表页:无 OpenAI 账号时跳过分数计算、过滤池限定 openai 平台、
负载批查合并为账号并集一次查询,消除全表扫描与 Redis N+1
- 订阅优先模式下常规池不可用时回退订阅池等待计划,
busy-but-waitable 的订阅账号不再导致请求硬失败
- TopK/权重 DB 覆盖显式受总开关门控,与兄弟子开关语义一致
- 前端未分组 OpenAI 账号回退展示基础分,不再显示 "-"
- ListAllWithFilters 等能力正式进入 AccountRepository/AdminService 接口,
移除匿名接口断言与静默降级;负载批查失败补 warn 日志
- SelectAccountWithSchedulerForCapability 增加显式 previousResponseCanMove
参数,移除 "previous_response_can_move" 魔法字符串哨兵
- 设置写入路径补"基础权重不得全为零"聚合校验;
运行时设置批量读取失败的降级路径覆盖全部键并留痕
2026-07-06 11:43:16 +08:00
linshuboy
f26ca5661e
feat: add OpenAI advanced scheduler controls
...
Related: #1089 , #408 , #123
2026-07-05 17:24:38 +08:00
Wesley Liddick and GitHub
3020652fa1
Merge pull request #3565 from zy6p/zy6p/pr-openai-ws-http-bridge
...
feat(openai-ws): 支持 http_bridge ingress 模式
2026-07-02 17:40:32 +08:00
Bestony@Homelab
089a7b7fae
feat(keys): add api key concurrency stats
2026-07-02 15:58:29 +08:00
bdf7ead157
feat(spark-shadow): OpenAI Spark 链接型影子账号
...
背景:gpt-5.3-codex-spark 使用独立于 codex 全局(5h/7d)的配额窗口(数据源是
/wham/usage 响应体的 codex_bengalfox,而非 codex 全局用的 x-codex-* 响应头),且
只能挂在已完成 OAuth 授权的 OpenAI 账号下复用其登录态,不能作为独立账号单独接入。
为此新增“链接型影子账号”(spark shadow account):影子账号本身不持有任何凭据,
通过 parent_account_id 指向母账号,凭据/token/代理透传自母账号并共享母账号的刷新
周期,仅在配额维度(quota_dimension=spark)和用量窗口上与母账号完全独立调度、互不
连坐。
实现:
- 数据模型:migration 154(+154a)给 accounts 表加 parent_account_id /
quota_dimension 列 + 4 条约束(维度合法 / parent⟺非 global 维度一致 / 禁自指 /
FK)+ 2 个 CONCURRENTLY 索引(母账号索引 + 每母账号至多一个影子的唯一索引)。
- 创建:POST /api/v1/admin/accounts/:id/shadow(CreateShadow)—— 一母一影(唯一
索引兜底并发竞态),继承母账号 proxy/分组/并发/优先级(显式传参可覆盖),默认
model_mapping 恒等映射到 spark(拒绝非 spark 模型),母账号必须是真实的 OpenAI
OAuth 账号(非影子)。
- 凭据透传:resolveCredentialAccount 把影子解析回母账号,GetAccessToken / 请求头
/ WS 三条路径统一走此函数;影子自身 Credentials 恒为空(仅允许写 model_mapping),
凭据写入的汇聚点 persistAccountCredentials 对影子早返 no-op,防止误写。
- 调度:parentHealthyForShadow 只看母账号是否仍是 OpenAI OAuth + 凭据/传输是否
可用(active、token 未过期、未处于 401/刷新失败/传输故障导致的临时不可调度冷却),
刻意不看母账号的 global 限流窗口——两条 429 道互不连坐。
- 用量:影子的 codex_5h/7d 走 OpenAIQuotaService.QueryUsage(/wham/usage 的
codex_bengalfox),与母账号走的 WSv2 探测(/responses 头)完全独立的数据源、
刷新节流与 staleness 判定。
- 备份:ExportData 显式排除影子账号(影子不持凭据,通用凭据型导入强制
credentials 非空、无法表达父子链接),按 skipped_shadows 计数提示前端。
- 前端:账号操作菜单新增“创建 Spark 影子”入口,影子行展示回填的母账号信息
(邮箱 / plan / 隐私模式 / 订阅到期 / chatgpt_account_id),批量操作自动跳过
影子账号。
说明:migrations 目录用完整文件名(而非纯数字前缀)标识迁移,故本次新增的
154_account_spark_shadow.sql / 154a_..._notx.sql 与已有的
154_add_ops_system_logs_api_key_id.sql 按序号共存,与目录里 145/151 已有的
先例一致。
测试:新增约 20 个测试文件,覆盖 handler(CreateShadow 校验 / 母账号信息回填)、
repository(影子 round-trip / 一母一影唯一索引 / 迁移 schema)、service(凭据
透传三路径 / 调度母健康门 / 用量窗口来源与刷新节流 / CRS 母账号不变量 / 各类
早返与 fail-closed 场景)及前端组件(账号列表 / 操作菜单 / 用量重置)。
验证(镜像 CI;golangci-lint 首次全量分析耗时过长被跳过,其余全部实测):
- gofmt -l:干净
- go build ./... / go vet ./...:通过
- go test ./... -count=1:全绿(全部包 ok,含 internal/service、
internal/repository、migrations)
- go test -tags integration ./internal/repository/... ./internal/service/...
(真实 Postgres,testcontainers):全绿,含迁移幂等性
(TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate)与影子相关全部用例
- pnpm lint:check / pnpm typecheck / pnpm build(真实 vite 构建)/
pnpm vitest run:全绿(124 文件 760 用例)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-07-01 12:21:45 +08:00
zy6p
0476b5c97a
fix(openai-ws): allow http_bridge accounts in ws ingress selection
...
(cherry picked from commit f0d215ec522e05043f5012a5f1e7aba2bfb57fcc)
(cherry picked from commit 7b14846a65e16570cb7b472b0ed06c6dcb190e0d)
2026-06-30 10:41:41 +08:00
Heatherm Huang
10e623f674
fix: allow grok messages compatibility
2026-06-29 18:33:37 +08:00
Heatherm Huang
4a7148e203
fix: support grok cli compatibility routes
2026-06-29 17:53:19 +08:00
mxyhi
82553c4dca
fix(openai): preserve quota platform in usage billing
2026-06-28 11:13:09 +08:00
Wesley Liddick and GitHub
2fc4fef847
Merge pull request #3310 from heathermhuang/codex/grok-subscription-support
...
feat: add grok subscription support
2026-06-26 15:41:52 +08:00
shaw
fcd3bc1272
fix: return 404 model_not_found instead of 503 when no account supports the model
2026-06-26 15:38:06 +08:00
Heatherm Huang
39be1ec97f
feat: add grok subscription support
2026-06-26 10:36:09 +08:00
FjlI5
bab8a9a93e
fix(openai): log /v1/chat/completions upstream endpoint for chat-only API-key accounts
...
DeriveUpstreamEndpoint maps every OpenAI-platform request to /v1/responses, but
API-key accounts whose upstream only speaks Chat Completions
(!ShouldUseResponsesAPI) are forwarded directly to /v1/chat/completions. The
messages, responses and cyber-policy recording sites derived the endpoint via the
bare GetUpstreamEndpoint, so usage/ops records mislabeled those requests as
/v1/responses. Generalize the existing resolveRawCCUpstreamEndpoint into
resolveOpenAIUpstreamEndpoint and use it at every OpenAI recording site, matching
the already-correct chat-completions client path.
2026-06-18 00:29:04 +08:00
Wesley Liddick and GitHub
16765bde69
Merge pull request #3230 from DaydreamCoding/feat/openai-cyber-policy-passthrough
...
feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
2026-06-16 16:55:51 +08:00
jjaw and shaw
b0579c4891
fix: move user wait queue accounting off hot path
2026-06-16 11:41:54 +08:00
DaydreamCoding and Claude Opus 4.8
b62b573f7f
feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
...
上游对单次请求下发 error.code=cyber_policy 硬阻断时,网关在所有端点
(/v1/responses、/v1/chat/completions、/v1/messages、WebSocket)及流式/
非流式路径下,将该结果原样透传给客户端,绝不 failover、换号或同步拦截;
命中后异步完成审计与计费:
- 风控中心记录 cyber_policy 留痕并发送通知邮件,落库先于发信,SMTP 阻塞
不影响留痕
- ops 错误请求记录,状态码对齐客户端实际接收(流式 200 / 非流式 400)
- 用量明细标记 request_type=cyber,按上游真实 token 计费,HTTP 与
WebSocket 计费口径统一,零 token 命中不误扣
- 会话级自动屏蔽(管理员开关,默认关):命中的会话在可配 TTL 内本地拦截
不再发往上游,仅屏蔽该会话不影响同 Key 其他会话
- 封号计数排除开关:可选让 cyber 命中不计入自动封号,命中当次不判定且
历史行在违规计数中一并排除
WebSocket 多轮连接下 cyber 标记按 turn 生命周期管理,逐轮独立检测与记录;
透传的错误响应不被兜底逻辑追加内容污染。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-12 01:47:01 +08:00
Wesley Liddick and GitHub
0c997d41a9
Merge pull request #3176 from jianjianai/fix/precompute-model-body-replacement
...
优化 OpenAI 网关 failover 流程,避免账号切换时重复对请求体执行 JSON model 替换。
2026-06-10 09:58:19 +08:00
erio
6c88631690
fix(gateway): prevent double-write on error passthrough responses
...
Service layer writes a complete JSON error response then returns error.
Handler's ensureForwardErrorResponse couldn't distinguish this from
"no response written" and appended an SSE event, corrupting the body.
Use gin.Context flag: service marks MarkResponseCommitted(c) after
writing, ensureForwardErrorResponse checks IsResponseCommitted(c)
and skips. Zero function signature changes, zero error wrapping.
2026-06-10 00:15:51 +08:00
jjaw
2c45f91d3c
fix openai failover model body replacement
2026-06-09 21:52:04 +08:00
bwlc and Claude Opus 4.8
9a0e439803
fix(openai): 跨组会话失配保护移到生效的 WSv2 路径并补测
...
87dd5f5d 把 previous_response_id 剥离保护加在了 HTTP Responses 路径,但该路径
在 previousResponseID != "" 时已无条件返回 400(0fcddce6 引入),剥离块恒不可达,
RemovePreviousResponseIDFromBody 也只被这段死分支调用、无覆盖。
- 删除 HTTP Responses 路径的死代码剥离块,留注释指明保护应在 WSv2 路径。
- 在 ResponsesWebSocket 首包(wsFirstMessage)处补回等价保护:previous_response_id
未命中当前分组粘连账号(StickyPreviousHit=false)时剥离,改用首包 input 重建;
带 function_call_output 的工具续链保持原样。这是 previous_response_id 真正生效、
会触发跨组会话链鉴权失败的路径。
- 为 RemovePreviousResponseIDFromBody 增加单元测试(现已有真实 caller)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-07 20:30:56 +08:00
bwlc and Claude Opus 4.8
87dd5f5d72
fix(openai): 切组后剥离失配的 previous_response_id,修复跨组会话鉴权失败
...
用户从公开组切回订阅组后,客户端沿用旧 Responses 会话 ID,订阅组调度到
不拥有该会话链的账号,上游因会话链鉴权不匹配返回“鉴权失败,请检查 API Key”。
两处修复:
- openai_ws_state_store: 本地热缓存 responseToAccount 改为按 {groupID}:{responseID}
命名空间,与 Redis 层 sticky_session:{groupID}:... 一致,避免单实例下跨组命中
其他分组遗留的本地绑定,确保 StickyPreviousHit 信号可信。
- openai_gateway_handler: 转发前若 previous_response_id 未在当前分组命中粘连账号
(StickyPreviousHit=false),主动剥离并改用完整 input 重建上下文;带
function_call_output 的工具续链保持原样(与 WS 重连恢复逻辑一致)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-06 16:54:56 +08:00
Wesley Liddick and GitHub
651b24571c
Merge pull request #3000 from Pluviobyte/codex/sub2api-stream-field-validation
...
fix: validate stream field type across OpenAI-compatible handlers
2026-06-06 15:02:06 +08:00
Wesley Liddick and GitHub
eb24485fc6
Merge pull request #3036 from whatIsNextToTheMoon/fix/openai-response-failed-passthrough
...
fix(openai): preserve upstream response.failed errors
2026-06-06 09:53:08 +08:00
wucm667
8e27ff20af
fix(openai): handle missing messages stream terminal
2026-06-05 18:11:23 +08:00
whatIsNextToTheMoon
5bd3d90434
fix(openai): preserve upstream response.failed errors
2026-06-04 11:17:22 +00:00
Pluviobyte
3571b082fb
fix: validate stream field type
2026-06-03 14:18:12 +08:00
xlx0852
08e19bb15c
fix(openai): bridge oversized websocket requests
2026-06-01 10:42:55 +08:00
name
5a3e193b53
refactor(gateway): shorten OpenAI request body retention
2026-05-30 20:01:39 +08:00
name
2caee9d884
refactor(gateway): snapshot usage worker inputs
2026-05-30 20:01:39 +08:00
name
b1c4be4ac8
refactor(gateway): remove parsed request object graphs
...
Keep large gateway payloads as raw body ranges and bind OpenAI parsed-body caches to the body bytes so failover and mapping do not reuse stale mutable state.
2026-05-30 20:01:38 +08:00
Wesley Liddick and GitHub
69e7c4db30
Merge pull request #2865 from wey-gu/feat/usage-request-context
...
fix(gateway): preserve usage request context
2026-05-29 16:21:59 +08:00
Wesley Liddick and GitHub
21cd382f39
Merge pull request #2855 from gaoren002/fix/concurrency-error-classification
...
fix: classify concurrency acquire failures
2026-05-29 10:33:40 +08:00
shaw
ed1b57c597
fix(openai): gate routing by endpoint capability
2026-05-29 08:58:10 +08:00
Wey Gu
2bd3125d0f
Preserve usage request context
2026-05-28 22:44:25 +08:00
gaoren002
56e96fdd8c
fix: classify concurrency acquire failures
2026-05-28 10:03:41 +00:00
siyuan
08061717b8
fix: enable account failover for OpenAI WS rate limits
2026-05-26 20:07:00 +08:00
6b39b344d8
feat(quota): 用户 × 平台 USD 配额
...
为用户在 anthropic/openai/gemini/antigravity 四个平台上提供日/周/月
三个窗口的 USD 配额管控。配额语义:未设置=不限制,0=禁用,>0=美元上限。
两层模型:
- 配置层:系统默认配额,以及 email/linuxdo/oidc/wechat/github/google/
dingtalk 七个鉴权来源的默认配额,存于 settings,以嵌套 JSON 整体读写
(系统 1 个 key + 每个来源 1 个 key),整体替换语义。
- 运行时层:user_platform_quota 表按用户记录实际配额,与配置层解耦。
后端:新增 ent schema 与 140_user_platform_quotas.sql 迁移、repository
与 service 端口、计费链路集成、管理端与用户端读写接口。
前端:管理端设置页配额编辑、用户配额管理 Modal、用户 Dashboard 展示、
中英文案。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-26 10:49:20 +08:00
Jamie Wong and Claude Opus 4.7
b34cc71bee
fix(openai): also emit response.failed in ensureForwardErrorResponse after Writer.Written
...
Case B: when a slot wait flushes SSE ping comments first (Writer.Written
becomes true), the previous ensureForwardErrorResponse short-circuited
on `c.Writer.Written()` and returned false without notifying the client.
Subsequent upstream errors (http2 timeout, stream INTERNAL_ERROR, etc.)
produced silent EOF; Codex CLI reported "stream closed before
response.completed" just like the user-slot timeout case.
Remove the Written() early return; coerce streamStarted to true when
Writer has already been written to, and let handleStreamingAwareError
walk the existing logic — which now (thanks to the previous commits)
emits a protocol-compliant response.failed for /responses paths and the
legacy `event: error` for others.
Update tests that previously asserted "do not override written response":
the new contract is to *append* an SSE terminal frame so the client sees
a clean close instead of EOF. recoverResponsesPanic inherits this fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-24 22:00:56 +08:00
Jamie Wong and Claude Opus 4.7
cff2f291be
fix(openai): also match bare /responses route in handleStreamingAwareError
...
The first revision compared GetInboundEndpoint(c) against EndpointResponses
("/v1/responses"). NormalizeInboundEndpoint only recognizes paths that
contain the literal "/v1/responses" substring, but the project actually
registers six /responses routes — three of which (top-level
r.POST("/responses", ...) and codexDirect's "/backend-api/codex/responses")
have FullPath values without the "/v1" prefix and therefore fall through
to the default branch.
Codex CLI users targeting the bare /responses route at the production
deployment (observed 2026-05-24 ~11:05 UTC, user 16) never reached the
new writeResponsesFailedSSE path: the endpoint check was false, the
legacy `event: error` frame fired, and the strict SDK kept reporting
"stream closed before response.completed".
Replace the strict equality check with inboundIsResponses(c), which
uses suffix detection on FullPath (falling back to URL.Path when
FullPath is empty in test fixtures) and covers all six route variants:
/v1/responses[/...]
/responses[/...]
/backend-api/codex/responses[/...]
Add test table covering all routes plus negative cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-24 19:32:08 +08:00
Jamie Wong and Claude Opus 4.7
5e5c2062bf
fix(openai): emit response.failed for /v1/responses after stream started
...
When /v1/responses streaming hits the user/account concurrency wait, the
wait loop sends SSE ping comments to keep the connection alive, which
flushes HTTP 200 + headers. If the wait then times out (or any other
post-flush error fires), handleStreamingAwareError previously emitted a
generic `event: error` frame. Codex CLI requires the stream to end with
a Responses terminal event (response.completed/failed/incomplete/cancelled),
so it reports "stream closed before response.completed" and the user-facing
rate-limit intent is lost.
This change detects inbound = /v1/responses in both handleStreamingAwareError
implementations and emits a protocol-compliant response.failed event whose
field set mirrors apicompat.makeResponsesCompletedEvent
(id/object/model/status/output/error). The synthetic id reuses
ctxkey.RequestID so client errors can be grepped against server logs.
sequence_number is intentionally omitted to preserve monotonicity on streams
that already emitted real events.
Other inbound endpoints (/v1/chat/completions, /v1/messages) keep their
legacy formats untouched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-24 10:58:29 +08:00
shaw
1e406fed52
fix: optimize OpenAI account cooldown scheduling
2026-05-23 10:18:43 +08:00
name
2eb622f2f6
Remove ops retry replay storage
2026-05-19 19:37:41 +08:00
wucm667
6381f9e37d
fix(openai): 识别上游静默拒绝并触发 failover
2026-05-19 15:48:36 +08:00