Commit Graph
3854 Commits
Author SHA1 Message Date
Wesley Liddick a2bf6ad77a Merge pull request #3107 from DaydreamCoding/feat/proxy-failover-resilience
feat: 代理生命周期与故障健壮性增强
2026-06-08 08:59:15 +08:00
Wesley Liddick d7eef1e942 Merge pull request #3104 from CoolCoolTomato/fix/api-key-exclusive-group-auth
fix(bug): Fix the bug related to unauthorized use of groups
2026-06-08 08:58:44 +08:00
Wesley Liddick 7b394ed157 Merge pull request #3094 from okbexx/fix-openai-sticky-group-validation
fix: ignore stale OpenAI sticky sessions outside request group
2026-06-08 08:49:09 +08:00
DaydreamCoding af19d44327 feat(proxies): 代理有效期与失败回退
- schema/迁移: 代理有效期、提醒天数、失败回退配置 + 账号 fallback 来源字段
- service/repo/DTO/handler: CRUD 透传新字段 + 校验
- fallback 目标解析纯函数(链式解析 + 环检测 + 兜底)
- SweepExpiredProxies 到期改投账号 + outbox 失效
- ProxyExpiryService 后台到期扫描任务 + wire 注册
- 账号侧手动回切原代理 + fallback 来源徽章/按钮
- 前端: 创建/编辑表单、列表到期徽章、类型/API/i18n
- ops 告警: proxy_expired_count / proxy_expiring_soon_count 指标
- 导入导出携带有效期/回退字段(备用按 name 映射)
- 补全测试 stub + 集成测试 + review 问题修复
2026-06-08 00:01:30 +08:00
DaydreamCodingandClaude Opus 4.8 f20e6bf769 feat(ops): 新增 account_temp_unscheduled_count 告警指标
临时摘除(temp-unschedulable)的账号被 account_error_count 指标显式排除
(acc.HasError && TempUnschedulableUntil == nil),且 SetTempUnschedulable 不
改账号 Status,导致代理/凭据故障触发的自动摘除无法被现有告警覆盖。

新增 account_temp_unscheduled_count 指标,统计当前处于临时不可调度窗口
(TempUnschedulableUntil 未过期) 的账号数,打通对自动摘除的定向告警:
- evaluator computeRuleMetric 新增分支 + handler 允许列表;
- 前端联合类型、告警规则下拉项与 en/zh 文案同步。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:31:45 +08:00
DaydreamCodingandClaude Opus 4.8 217f85999c fix(openai): /responses 传输层错误转 failover + 持久故障临时摘除账号
代理/网络等传输层失败(Do/DoWithTLS 返回 error、无 HTTP 状态码,例如 SOCKS5
代理凭据过期返回 "username/password authentication failed")此前直接写
502 "Upstream request failed" 并返回普通 error:既不 failover 到健康账号,
也不摘除故障账号,导致同一坏账号被反复调度、大量用户持续收到 502。

- 新增 classifyOpenAITransportError:typed-error 优先
  (ECONNREFUSED/EHOSTUNREACH/ENETUNREACH、*net.DNSError.IsNotFound)
  + 字符串兜底(SOCKS5 认证失败无 typed 形式)区分持久 vs 瞬时。
- 新增 handleOpenAIUpstreamTransportError:记录 ops 错误;对持久故障调用
  SetTempUnschedulable(10min,DB) + 内存 BlockAccountScheduling 摘除账号并打
  稳定 WARN 事件;统一返回 *UpstreamFailoverError 让 handler 切换到健康账号;
  context.Canceled(客户端断开)不 failover、不摘除。
- /responses 主路径、passthrough、chat-completions 回退三处统一接入共享 helper。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:31:45 +08:00
Wesley Liddick d01e4fd84d Merge pull request #3075 from bwliangc/feat/openai-cross-group-prev-response
fix(openai): 切组后剥离失配的 previous_response_id,修复跨组会话鉴权失败
2026-06-07 22:21:24 +08:00
CoolCoolTomato 1a86c6ce12 fix: enforce exclusive group access for api keys 2026-06-07 20:46:03 +08:00
bwlcandClaude 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
NeckBozia 16bc876935 fix(usage): sync 5h ResetsAt to SessionWindowEnd and zero expired window
active poll 拿到新 5h ResetsAt 时只回写了 Extra.session_window_utilization,
没回写 SessionWindowEnd column;estimateSetupTokenUsage 读这个 column 作为 5h
窗口结束时间,导致被动采样模式下 dashboard 显示 utilization > 0 但 reset
时间渲染为「现在」。

- syncActiveToPassive 增加 UpdateSessionWindowEnd 回写
- estimateSetupTokenUsage 抄齐 Codex 分支的过期归零 guard,避免 active
  poll 没回写时 UI 渲染矛盾
- UsageProgressBar 区分「待刷新」/「现在」语义;i18n key 中英补齐
2026-06-07 15:23:45 +08:00
okbexx a436296357 fix: validate OpenAI sticky session groups 2026-06-07 14:15:27 +08:00
xzz0081 154e0ed6c5 fix: force Content-Type to application/json on non-streaming responses
When the gateway forces streaming to the upstream, the upstream returns
Content-Type: text/event-stream. In the non-streaming branches, the body
is correctly aggregated into JSON, but WriteFilteredHeaders has already
passed the upstream's text/event-stream header through. Since Gin's
c.JSON/c.Data only set Content-Type via writeContentType when it is not
already present, the SSE header is never overwritten -- so the body is JSON
while the header says SSE.

Downstream middlewares that decide "is this streaming?" by Content-Type
(e.g. new-api) then mis-handle the response as SSE, breaking non-streaming
JSON clients.

Fix: explicitly Set Content-Type to application/json right after
WriteFilteredHeaders in the three non-streaming JSON write paths.
2026-06-07 02:56:26 +08:00
feitianbubu 029b6d61a2 feat(usage): 聚合统计拆分缓存创建与命中 token 2026-06-06 22:32:37 +08:00
bwlcandClaude 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 96b4bf624c Merge pull request #2997 from bobobo2026/codex/openai-responses-sticky-account
Bind OpenAI HTTP response IDs to selected accounts
2026-06-06 14:52:55 +08:00
Wesley Liddick c65f9f19d2 Merge pull request #2955 from paraliine/main
Align OpenAI Codex usage stats with reset windows
2026-06-06 14:16:40 +08:00
Wesley Liddick 5824359e97 Merge pull request #3062 from visa2/fix/multi-instance-bg-job-leader-lock
fix(service): gate periodic background jobs with a leader lock for multi-instance
2026-06-06 13:59:42 +08:00
Wesley Liddick 84cedd3d71 Merge pull request #2960 from CheriWen/fix/cwe-79-stored-xss-key-name
[Security] fix: sanitize API key name to prevent stored XSS
2026-06-06 11:09:08 +08:00
Wesley Liddick ff3c976dbd Merge pull request #3009 from liulinhuai/fix/images-upstream-error-passthrough
fix(images): surface real upstream error instead of generic 502
2026-06-06 11:07:53 +08:00
Wesley Liddick 1dda8626d5 Merge pull request #3018 from Zbl1007/openmeta/2989-0
Fix fixed weekly quota usage after switching from rolling reset
2026-06-06 10:59:18 +08:00
Wesley Liddick 1a0eecb6ef Merge pull request #3047 from fchange/codex/delete-user-api-keys
fix(admin): delete API keys when deleting a user
2026-06-06 10:14:28 +08:00
Wesley Liddick 1512ba747e Merge pull request #2995 from feitianbubu/opt-proxy-quality-pass
fix: treat allowed proxy quality statuses as pass not warn
2026-06-06 09:58:16 +08:00
Wesley Liddick 75af992819 Merge pull request #3026 from Zbl1007/openmeta/2956-deepseek
Surface DeepSeek reasoning-only responses
2026-06-06 09:35:29 +08:00
Wesley Liddick 427d591212 Merge pull request #2930 from touwaeriol/feat/image-token-billing
fix(billing): channel pricing override for image generation + display image_output_tokens
2026-06-06 09:28:46 +08:00
shaw 650981f2e4 fix(lint): 修复 golangci-lint errcheck 与 gofmt 报错
- gateway_service.go buildStableSessionSeed: strings.Builder.WriteString
  返回值用 _, _ = 显式忽略(errcheck)
- openai_codex_transform_additions_test.go: client_metadata 类型断言改为
  双值形式 cm3, _ := ...(errcheck check-type-assertions)
- openai_gateway_service.go: codexCLIUserAgent 常量单独成组,收敛 = 前多余
  空格对齐(gofmt)
2026-06-06 09:21:32 +08:00
eyre 480f0cba22 merge: 合并 Codex 与 Claude Code 双重模拟改造为统一分支
- feat/closer-to-codex:Codex 指纹/prompt/SSE/include/client_metadata 改造
- feat/closer-to-claude-code:Claude Code 指纹对齐 CLI 2.1.161
两组改动改动文件互不重叠,合并无冲突。
2026-06-06 01:06:20 +08:00
eyre 219da4b9e5 feat(claude-mimicry): align Claude Code fingerprint with CLI 2.1.161
- bump impersonated CLI version 2.1.92 -> 2.1.161; derive User-Agent from
  CLICurrentVersion so the two hardcoded copies can no longer drift apart
- fix x-stainless headers to real 2.1.161 values: package-version
  0.70.0 -> 0.94.0, runtime-version v24.13.0 -> v24.3.0 (verified against
  the installed Bun-compiled binary)
- expand the disguise-path system prompt from a 2-block identity skeleton
  to a 3-block layout (billing + identity + tool-agnostic prose), matching
  real CC's multi-block shape; cache breakpoint moved to the last static
  block. Deliberately excludes # Doing tasks / # Using your tools /
  # Executing actions to avoid polluting proxied-client behavior
- stabilize the synthesized metadata.user_id session_id across conversation
  turns: derive it from (account + client discriminator + first user
  message) instead of a per-turn content/body hash. Sticky-routing
  GenerateSessionHash is intentionally left untouched; remove now-dead
  hashBodyForSessionSeed

Tests: update the 3-block system assertions in gateway_prompt_test and
gateway_anthropic_apikey_passthrough_test; add a session_id cross-turn
stability test in gateway_oauth_metadata_test.
2026-06-06 01:01:58 +08:00
eyre 5e6effd79c feat: 模型感知 Codex prompt / client_metadata / anthropic SSE 补全
- 模型感知 instructions:刷新 instructions.txt 至最新 GPT-5-Codex prompt,新增
  GPT-5.1 / GPT-5.2 真实 Codex 编码 agent prompt;新增 openai.CodexBaseInstructionsForModel
  按模型选用(codex 系→GPT-5-Codex,gpt-5.2→GPT-5.2,gpt-5.1/gpt-5→GPT-5.1),
  defaultCodexSynthInstructions 改为按 model 选择
- client_metadata:OAuth /responses 请求用账号真实 openai_device_id 注入
  client_metadata["x-codex-installation-id"];加法式、幂等、不覆盖既有项,
  无 device_id(非 OAuth 账号)则不写入、不伪造
- anthropic 转换补全 SSE 事件 reasoning_text.delta 与 custom_tool_call_input.delta
  (含 custom_tool_call item 注册为 tool_use),与 chat completions 路径对齐
- 新增单元测试覆盖以上行为(apicompat / openai / service)

承接上一提交,仍为加法式改进,不改动既有可工作的请求头。
2026-06-06 00:59:27 +08:00
eyre baf078fdb7 feat: 提升 Codex 行为模拟保真度(仅加法式改进)
- ForceCodexCLI 兜底 User-Agent 补全为完整结构 {originator}/{ver} ({OS} {ver}; {arch}) {terminal},
  与真实 codex_cli_rs UA 对齐,避免被上游指纹识别为非官方客户端
- 合成路径默认 instructions 改用内嵌的真实 Codex base prompt(openai.DefaultInstructions,
  "You are Codex, based on GPT-5..."),替换通用占位符;transform 与 hotpath 两处统一走
  defaultCodexSynthInstructions()
- /responses→chat 转换补全 SSE 事件 reasoning_text.delta 与 custom_tool_call_input.delta
  (流式 ResponsesEventToChatChunks 与缓冲 BufferedResponseAccumulator 两条路径),
  并将 custom_tool_call item 一并注册以正确映射工具索引
- OAuth /responses 请求带 reasoning 时补齐 include:["reasoning.encrypted_content"],
  幂等且加法式,不改动已有 include

保持旧稳定版兼容:不改动现有可工作的请求头(session_id/conversation_id/originator/
OpenAI-Beta 维持原样)。client_metadata(installation-id) 属最新版特征,会与旧稳定版
头指纹混搭冲突,本次未加。
2026-06-06 00:44:49 +08:00
visa2andClaude Opus 4.8 362f9e77bf fix(service): gate periodic background jobs with a leader lock for multi-instance
Three periodic background jobs ran on every instance with no cross-instance
coordination, multiplying their cost (and side effects) by the replica count:

- DashboardAggregationService.runScheduledAggregation: N× heavy GROUP BY
  aggregation queries every minute plus watermark write races.
- PaymentOrderExpiryService.runOnce: N× upstream payment-provider reconcile/
  expiry API calls per pending order.
- SubscriptionExpiryService.sendExpiryReminders: N× full active-subscription
  scans every minute and potential duplicate reminder emails.

Add a LeaderLockCache abstraction so only one instance runs each job per cycle:

- The interface lives in the service layer; the Redis-backed implementation
  (SetNX + compare-and-delete release) lives in the repository layer, so the
  service package keeps its depguard "must not import redis" boundary intact.
- tryAcquireSingletonLeaderLock prefers the cache and falls back to a Postgres
  advisory lock when Redis errors, mirroring the Ops background services. When
  neither backend is configured the job runs ungated, preserving single-instance
  and test behavior (no self-lockout: the lock is released every cycle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:47:24 +08:00
Wesley Liddick 1cecd2716c Merge pull request #2972 from northya/fix/chat-completions-response-failed
fix: 修复 Chat Completions 兼容层误将 response.failed 返回为成功响应
2026-06-05 21:51:23 +08:00
Wesley Liddick 8775047f84 Merge pull request #3051 from wucm667/fix/openai-messages-missing-terminal-event-failover
fix(openai): /v1/messages 流式缺终止事件时纳入 failover 与 ops 错误归因
2026-06-05 21:34:14 +08:00
Wesley Liddick 872e56aed7 Merge pull request #3052 from wucm667/feat/openai-image-ratelimit-cooldown-failover
feat(openai): gpt-image 图片限流按能力维度冷却并 failover,不再误伤文字请求
2026-06-05 21:25:40 +08:00
Wesley Liddick b5b68f86bd Merge pull request #3037 from feitianbubu/pr/fix-auto-mode-invalid-by-security-monitor
fix: cc开启auto mode后无法通过cc客户端验证的问题
2026-06-05 20:43:37 +08:00
wucm667 36721d35a8 feat(openai): cool down image rate limits by capability 2026-06-05 18:12:33 +08:00
wucm667 8e27ff20af fix(openai): handle missing messages stream terminal 2026-06-05 18:11:23 +08:00
方程 705fe7d880 fix(admin): delete user api keys with user 2026-06-05 16:23:56 +08:00
feitianbubuandClaude Opus 4.8 d626ccce1d fix: recognize claude code clients via billing block, not just prompt
Genuine Claude Code CLI sub-requests (e.g. the security monitor) carry
no identity system prompt, so claude_code_only groups wrongly rejected
them with "this group only allows Claude Code clients". Detect the
x-anthropic-billing-header block with cc_entrypoint=cli as a stable
client signal, while keeping the existing header/metadata checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:09:03 +08:00
DaydreamCodingandClaude Opus 4.8 cfb195c7b2 feat(usage): 记录并展示失败请求(用户端+管理端)
- 记录失败请求并在用户端/管理端展示;分类下拉改用统一 Select 组件
- 模型过滤改后端 ILIKE 模糊匹配;新增「Key 名称」列(含已删除标记)与按 Key 过滤;时间列移至末列
- 用户可见「已删除 key 失败请求」:OpsErrorLogFilter 加 MatchDeletedKeyOwner,用户侧归属
  放宽为 (user_id OR deleted_key_owner_user_id),让 key 原所有者能看到删除 key 后继续请求
  导致的认证失败记录(他人仍 NotFound,不泄露存在性)
- 迁移 148:ops_error_logs 用户+时间索引

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:00:57 +08:00
DaydreamCodingandClaude Opus 4.8 ddf063352a feat(ops): 错误日志 key 归因与早退字段补全
让 /admin/ops 错误详情正确归因 API key 并补全早退场景字段,合并三项改动:

- 鉴权早退补全用户/分组/平台字段:引入 ops fallback key(ContextKeyOpsFallbackAPIKey),
  apiKey 一加载成功即写入,覆盖分组停用/删除、Key 停用/过期/额度、用户停用、IP 限制等早退
  路径;ops 错误日志改用 getOpsAPIKey(正式 key 优先、回退键兜底),不改「已鉴权」语义。
- 已删除 key 归因(迁移 145):删除 key 时同一事务写 deleted_api_key_audits 映射,认证失败
  时用明文反查命中原所有者,错误详情展示「已删除 Key 所有者」「尝试的 Key 前缀」。
- 有效 key 报错快照前缀(迁移 147):对绑定有效 key 的错误,落库时快照明文前 8 位到
  api_key_prefix(与 attempted_key_prefix 互斥),key 之后被删仍保留报错当时真实前缀。

均仅对上线后新产生的错误/删除生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:00:57 +08:00
Wesley Liddick f332e0a83c Merge pull request #2872 from wucm667/fix/scheduler-sticky-health-escape
fix(scheduler): session_hash sticky 引入健康度逃逸,慢账号不再独占用户会话
2026-06-05 13:54:37 +08:00
Wesley Liddick 83cce858a5 Merge pull request #3039 from StarryKira/fix/issue-2994-codex-5h-used-percent-selfheal
fix(openai): self-heal stale Codex used% snapshots + lock semantics (#2994)
2026-06-05 13:54:18 +08:00
Wesley Liddick bebeaf57c5 Merge pull request #2854 from ttt132/fix/responses-stream-completed-output
fix: normalize responses streaming terminal output
2026-06-05 13:54:05 +08:00
Wesley Liddick fbd25acae0 Merge pull request #3035 from ghostg00/fix/admin-group-clear-description
fix(group): 管理员清空分组描述时正确持久化
2026-06-05 13:53:20 +08:00
Wesley Liddick 05f0326e99 Merge pull request #2988 from wucm667: content audit auto-ban exempts admin accounts
fix(risk-control): 内容审计 auto-ban 豁免管理员账号,避免封禁管理员/超管
2026-06-05 13:47:10 +08:00
harukaandClaude Opus 4.8 86d9b6bff9 fix(openai): self-heal stale Codex used% snapshots + lock semantics (#2994)
The OpenAI/Codex 5h "used %" inversion that caused fresh accounts to show
~96-99% used (PR #2918, commit b65dde63) was already reverted in #2993, so the
stored value is now the correct "used %" again. This commit hardens that fix:

1. Regression test locking in direct "used %" semantics. The semantics have
   flip-flopped twice (#2918 -> #2993) with no value-level guard — a fresh
   account (secondary_used_percent=1, 5h window) must store
   codex_5h_used_percent=1, not 99.

2. Stale-bounded self-heal in resolveOpenAIQuotaUtilization (the single
   auto-pause chokepoint). An account poisoned with an inflated used% gets
   excluded from scheduling, and a paused account never receives traffic to
   refresh its snapshot — so it stayed stuck until the window's reset_at passed
   (up to 5h/7d). When codex_usage_updated_at is older than 2h, the account is
   no longer auto-paused on that snapshot; it gets one request whose response
   headers refresh the snapshot and self-heal it. A missing timestamp is treated
   as fresh (stays paused), and an actively-served exhausted account refreshes
   the timestamp every response so it never crosses the bound — it cannot escape
   auto-pause.

No change to Normalize(); no 100-x reintroduced; no new dependency wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:07:36 +08:00
ghostg00 bc7ce18574 fix(group): 管理员清空分组描述时正确持久化
UpdateGroup 之前用 `if input.Description != ""` 判空,
把"未提供"和"显式置空"混为一谈,导致管理员在分组编辑表单
里清空备注后保存无效。

将 UpdateGroupRequest / UpdateGroupInput 的 Description 改为
*string:nil 表示未提供(保持原值),"" 表示显式清空。
2026-06-04 19:48:03 +08:00
Zbl1007 9b99f6c1f3 fix(apicompat): surface DeepSeek reasoning-only replies 2026-06-04 11:34:54 +08:00
Zbl1007 fb0195f3dc fix(account): normalize fixed quota windows on edit 2026-06-04 01:06:41 +08:00
Liu LinhuaiandClaude Opus 4.8 381d1d6d6c fix(images): surface real upstream error instead of generic 502
The /v1/images/generations and /v1/images/edits paths routed every
non-failover upstream error through the generic handleErrorResponse,
whose final switch collapses anything that isn't 401/402/403/429 into a
hardcoded 502 "Upstream request failed" — discarding the actual upstream
status code, type, code, message, and param. So a gpt-image-2 400
(invalid_request_error, moderation, unsupported parameter, ...) reached
the client as an opaque 502.

The sibling Chat Completions and Messages compat paths already avoid this
via handleCompatErrorResponse, which preserves the real status and
message. Add the equivalent for images: handleOpenAIImagesErrorResponse
keeps all existing side-effects (ops logging, error-passthrough rules,
ShouldHandleErrorCode, account-disable/secondary-failover) but surfaces
the real upstream status + type/code/message/param by reusing the
existing OpenAIImagesUpstreamError machinery. Both forward paths now call
it instead of handleErrorResponse for non-failover errors.

Failover behavior (5xx / 401 / 403 / 429 / 529) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:23:13 +08:00