Commit Graph
1083 Commits
Author SHA1 Message Date
Wesley Liddick 4f0c079e27 Merge pull request #7681 from william-drakemond/fix/balance-inflight-reservation
fix(billing): Redis 在途预留,防止余额模式并发透支
2026-09-30 14:31:04 +08:00
shaw 9ecb340822 feat(api-key): 增加 API Key 创建数量与频率限制
- 新增 api_key_create.max_active_per_user(默认 200)与
  api_key_create.max_per_user_per_hour(默认 20),0 表示不限制
- 创建频率按固定一小时窗口统计,自定义与自动生成的 Key 统一计入
- 创建计数独立于 Key 生命周期,删除或修改 Key 不再重置计数
- 移除不再使用的 DeleteCreateAttemptCount 缓存方法
2026-09-30 13:44:02 +08:00
Wesley Liddick fe9dadfc7d Merge pull request #7679 from PMExtra/feat/risk-control-allowlist
feat: 新增可信用户风控白名单,保留审计并免除本地自动处罚
2026-09-29 15:53:18 +08:00
William DrakeandClaude Opus 5.5 652a7d74f9 fix(billing): estimate unpriced aliases via billing fallback models; don't renew expired reservations
- Inflight estimate now mirrors billableModelWithFallback: when the primary
  billing model has no price, fall back to the channel-mapped model (also for
  billing_model_source=requested) and to account-level mapped models (max over
  schedulable accounts in the group, cached 30s).
- Composite groups also consider concrete account-mapped models (max) so the
  alias' family fuzzy price cannot underestimate.
- Renew Lua refuses (and purges) members whose score <= now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:49:29 -04:00
William DrakeandClaude Opus 5.5 52c123e6f4 fix(billing): hold inflight reservation until billing lands; price estimate like billing; wire remaining endpoints
Address gaps found in adversarial verification of the in-flight balance
reservation:

- Early release: the reservation is now a ref-counted handle carried on the
  request context. Billing tasks submitted via submit*UsageRecordTask take a
  ref and return it only after RecordUsage has run; with reservation enabled
  the balance cache is deducted synchronously inside the billing task, so the
  reservation is released only after admission can see the deduction. Dropped
  tasks return their ref immediately. The reservation is renewed every ttl/3
  while the handler runs; after the handler returns it is no longer renewed,
  so the post-handler hold is bounded by one TTL.
- Estimate: use the same billing model (channel mapping + BillingModelSource,
  max over requested/mapped when ambiguous) and ModelPricingResolver (group
  ModelPricing incl. wildcards, channel per_request/image/video/token
  overrides), default/group/user rate + peak multiplier, plus image, video,
  audio and search surcharge kinds. Unpriced requests fail open with a
  throttled log; new billing.inflight_reservation.fail_closed_on_unpriced
  (default false) rejects them instead.
- Wire images, embeddings, web/x search, alpha search, grok media generation,
  grok realtime/voice, OpenAI-platform /v1/messages and ResponsesWebSocket
  (session-level reservation with renewal).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:14:43 -04:00
William DrakeandClaude Opus 5.5 43f88c99aa fix(billing): reserve in-flight balance to prevent concurrent overdraft
Balance-mode admission only checked cached balance > threshold, so N
concurrent expensive requests could all pass and overdraw the user.
Add a Redis in-flight reservation: at admission estimate a conservative
per-request cost (input estimate + output price x max_tokens, times rate
multiplier) and atomically require cached_balance - inflight >= estimate
(first in-flight request always admitted). Reservations are released via
defer on every path and expire by TTL. Fails open on estimation / Redis
errors. Configurable via billing.inflight_reservation (enabled by default).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:20:49 -04:00
PMExtra 6a69dd051a feat: add risk control user allowlist 2026-09-29 01:50:21 +08:00
wucm667 aa3a3ea787 feat(dashboard): toggle recent usage between tokens and spending 2026-09-28 14:48:42 +08:00
Yuan LiuandClaude Opus 5.5 1ca7b2a519 fix(scheduler): keep auto reset-credit fields in the scheduler projection
OpenAI candidate filtering reads the Redis metadata projection, but
filterSchedulerExtra dropped auto_reset_credit_enabled, the 5h/7d
reset-credit thresholds and codex_auto_reset_credit_state. The scheduler
therefore always saw automatic reset credits as disabled, so accounts
were paused at the ops quota auto-pause threshold even when a fresh
state showed an available reset credit. The "keep scheduling until the
reset-credit threshold" branch never took effect, and accounts with
credits stayed idle until their window reset naturally.

- Keep the four keys in the scheduler projection; the test round-trips
  the real JSON payload and resolves the credit config from it.
- Once visible, every scheduling pass notifies the auto-reset worker for
  paused credit-enabled accounts, and each notification reloads the
  account. Add a 30s per-account cooldown for the scheduler hot path
  only; other event-driven notifications are unchanged and the
  per-minute scan remains the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:02:50 +08:00
alfadb a2e1618700 fix(opencode): 纠正 CASE 求值顺序注释并补齐 mode 语义护栏 2026-09-23 15:26:44 +08:00
alfadb d03274ad5f feat(opencode): 用量窗口资格支持 OpenCode 平台并扩展到其它厂商挂载 2026-09-23 15:26:44 +08:00
alfadb 0a43efb9a4 fix(opencode): preserve managed usage state on account updates 2026-09-23 15:26:35 +08:00
alfadb f7f0a56f35 fix(opencode): preserve auto-refresh on usage refresh 2026-09-23 15:26:35 +08:00
alfadb ae2735cdb6 feat(opencode): 用量窗口支持同 Key 组共享、活动防抖与超窗强刷
与 Ollama Cloud usage 对齐:OpenCode Go 作为同 Key 聚合订阅,
多个 openai/apikey/base_url=opencode.ai/zen/go/v1 账号共享同一份
用量状态与刷新节奏:

- 组共享:组指纹 sha256("opencode.ai\0"+api_key);auto_refresh 开关
  与 snapshot 按组写(事务 + FOR NO KEY UPDATE + 锚点 CAS + 纯合并,
  写 snapshot 不会抹掉开关);列表/详情经 ResolveAccounts 组内共享;
  账号换 Key → IdentityChanged → 组级关闭 auto_refresh 并清快照
- 活动防抖 + 超窗强刷:settings 新增 debounce_minutes(默认 1,
  1-60,必须小于 interval_minutes);模型请求活动经网关 5 处调用点
  驱动刷新,due = min(lastUsed+debounce, fetchedAt+maxWait),
  成功路径有 5 分钟最小抓取间隔,失败路径退避优先
- 手动刷新限频 10s→30s 且按组;singleflight/RunDue 按组去重;
  ListDue 在 SQL 内按组算 due(CTE 分组取组内 MAX(last_used_at))

验证:go build + 五包回归 OK;新增组共享/三态 due/组单飞/
IdentityChanged/debounce 校验单测全绿;vitest 11 例全绿;
vue-tsc 零错误。
2026-09-23 15:26:35 +08:00
alfadb 6624947b62 feat(opencode): 支持 OpenCode Go 官方用量窗口查询与自动刷新
OpenCode Go 上游(https://opencode.ai/zen/go/v1)官方提供用量端点
GET /zen/go/v1/usage(Bearer 复用消费 key),返回 rolling(5h)/weekly/monthly
三窗口的 percent 与 resetsAt。本提交为 openai apikey 账号(base_url 为
opencode.ai/zen/go/v1)接入该用量窗口:

- 后端:OpenCodeGoUsageService——手动刷新(每账号 10s 节流)、
  周期自动刷新(全局开关 + 账号开关双开启,interval 5-1440 分钟,
  leader lock + 每轮 20 账号 + 并发 4)、失败指数退避(上限 24h)、
  Retry-After 尊重;401→unauthorized、403→failed;snapshot 持久化于
  accounts.extra(单账号写入,无分组 CAS、无会话加密)
- admin API:settings GET/PUT、/:id/opencode-go-usage GET、
  auto-refresh PUT、refresh POST(与 Ollama Cloud usage 同构)
- 前端:账号列表行内三窗口进度条(复用 UsageProgressBar)、
  编辑弹窗内配置面板(详情+手动刷新+auto-refresh)、全局设置卡片、
  en/zh i18n

验证:
- 上游端点已用生产真实 Go key 实测 200,响应
  {"usage":{"rolling":{"status":"ok","percent":6,...},...}}(2026-08-13)
- go build ./... OK;service/handler/repo/dto/cmd 全量回归 OK
- 新增单测 20 例(解析 401/403/畸形 JSON、退避、节流等)全绿
- frontend vitest 新增 7 例 + Ollama 回归 4 例全绿;vue-tsc 零错误
2026-09-23 15:26:35 +08:00
Wesley Liddick d8d5e1dd35 Merge pull request #7282 from feeeei/feat/affiliate-offline-withdrawal
feat(affiliate): 提取记录支持登记线下提现
2026-09-23 11:20:32 +08:00
Wesley Liddick 7c0a2a556c Merge pull request #7160 from StarryKira/codex/log-retention-upstream
feat: 支持运维与请求日志的滚动保留设置
2026-09-23 10:07:25 +08:00
feeeei c19204289d fix(affiliate): 线下提现登记按 Idempotency-Key 幂等
登记请求只有金额,提交成功但响应丢失时,管理员重试会把同一笔站外打款再扣
一次返利额度。现在一次登记由 Idempotency-Key 标识,数据库在同一事务内以唯一
约束去重:

- user_affiliate_ledger 新增 operation_id(幂等键派生的 SHA-256)与部分唯一
  索引(迁移 240);接口要求 Idempotency-Key,缺失或非法返回 400
- 事务内先以 operation_id 写入占位流水,再解冻、扣减并补写额度快照;同键
  重试返回首笔流水的结果并带 X-Idempotency-Replayed,不重复扣减;同键换了
  用户或金额返回 409 IDEMPOTENCY_KEY_CONFLICT;额度不足时占位随事务回滚
- 并发的同键请求在唯一索引上等待先到者:先到者提交则返回同一条流水,回滚
  则自行登记
- 弹窗在网络错误、超时、5xx、408/409 时锁定用户与金额,重试沿用同一请求与
  幂等键;幂等键按管理员、用户、金额暂存于 sessionStorage,登记有确定结果后
  释放
2026-09-23 01:24:59 +08:00
Wesley Liddick 583398d186 Merge pull request #7303 from little-greenbean/fix/upstream-response-close-lifecycle
fix(upstream): prevent EOF stalls when closing response bodies
2026-09-22 21:57:24 +08:00
Wesley Liddick 5cc6ca6f5b Merge pull request #7311 from YogaSakti/fix/scheduler-cache-rpm-fields
fix(scheduler): keep account RPM config in the scheduler projection
2026-09-22 21:57:05 +08:00
Wesley Liddick a079a6596a Merge pull request #7342 from Pluviobyte/codex/fix-inactive-proxy-backup
fix(proxies): exclude inactive fallback targets
2026-09-22 20:29:52 +08:00
Wesley Liddick ed2360d7ea Merge pull request #7341 from Pluviobyte/codex/fix-proxy-expiry-renewal
fix(proxies): preserve renewals during expiry sweeps
2026-09-22 20:29:39 +08:00
Wesley Liddick 483cd56924 Merge pull request #7361 from wucm667/feat/issue-7357-optional-default-groups
feat(simple): make startup default group creation optional
2026-09-22 18:13:18 +08:00
Wesley Liddick fcd07ba0af Merge pull request #7475 from Pluviobyte/codex/fix-proxy-restore-probe
fix(proxies): invalidate billing probe on fallback restore
2026-09-22 18:12:04 +08:00
Wesley Liddick 405727805a Merge pull request #7477 from Pluviobyte/codex/fix-subscription-reduction-remainder
fix(subscriptions): preserve partial days during redemption deductions
2026-09-22 18:11:46 +08:00
Rain 0c0df031c5 fix(subscriptions): preserve partial days during redemption deductions 2026-09-22 15:03:06 +08:00
Rain 85ed17b313 fix(subscriptions): lock negative redemption updates 2026-09-22 15:03:05 +08:00
Rain b754ff6f1b fix(proxies): invalidate billing probe on fallback restore 2026-09-22 14:51:44 +08:00
haruka b8d52fad34 feat(billing): support configurable reasoning effort multipliers 2026-09-20 16:41:49 +08:00
Wesley Liddick acdf3c54da Merge pull request #7401 from StarryKira/codex/codex-credits-referrals-upstream
feat(openai): display Codex credits and support referral invitations
2026-09-20 16:00:28 +08:00
haruka fc96132e72 fix(openai): harden referral outcomes and isolate HTTP transport 2026-09-20 15:47:53 +08:00
shaw e009ea3036 fix(upstream): 恢复 OpenAI HTTP/2 保活容错期限 2026-09-20 14:46:25 +08:00
Brisbanehuang 783a0a9830 feat(content-audit): add independent TypeSafe engine profiles 2026-09-19 20:52:31 -04:00
wucm667 68aa26477f feat(simple): make startup default group creation optional 2026-09-19 16:47:17 +08:00
shaw 6477143533 test(plugin): check type assertions to satisfy errcheck lint 2026-09-19 11:47:14 +08:00
shaw c63bd14a02 feat(plugin): generic host services + read-only status bridge channel
Extend the local plugin system's host<->plugin contract with generic,
capability-neutral facilities, so transport plugins can be built on top
without embedding plugin-specific logic in the host.

Host services (plugin dials a broker-provided HostService gRPC):
- per-plugin namespaced KV store (key/value size and TTL bounded)
- account directory: ListAccounts + ResolveOutboundIdentity, restricted to
  the accounts a plugin's declared capability already covers
- InitHostServices lifecycle hook; plugins that don't implement it run
  unchanged and are simply offered no host services

Read-only runtime status channel:
- HealthResponse gains an opaque, plugin-defined status_json produced
  passively (no config apply, no upstream call, no side effects)
- GET /admin/plugins/:id/status (ungated) and the plugin.status UI-bridge
  verb map to Health, so a status UI can poll live state without a step-up
  prompt or a "test" side effect
- config.test no longer force-shows a host success toast; the plugin UI owns
  result presentation, so plugins that read status do not spam toasts

Document the UI-bridge verb taxonomy (read vs mutate/test gating) and the
status contract under pkg/pluginapi.
2026-09-19 11:25:56 +08:00
Rain 4ccdead2a2 fix(proxies): skip inactive fallback targets 2026-09-19 00:51:01 +08:00
Rain f7280213f7 fix(proxies): skip expiry writes from stale snapshots 2026-09-19 00:51:01 +08:00
Yoga Sakti 4805f069e4 fix(scheduler): keep account RPM config in the scheduler projection
`filterSchedulerExtra` 的白名单没有 `base_rpm` / `rpm_strategy` /
`rpm_sticky_buffer`,写进 `sched:meta:<id>` 的精简投影因此丢掉整套 RPM 配置。
候选过滤走 `ListSchedulableAccounts`,缓存命中时读的正是这份投影:
`isAccountSchedulableForRPM` 拿到的账号上 `GetBaseRPM()` 返回 0,`baseRPM <= 0`
直接放行,已配置限流的账号在选号阶段被当作未开启 RPM,单个自然分钟内被超额调度。
缓存未命中时回退数据库读到完整 extra,所以现象是间歇性的。

选中的账号随后由 `hydrateSelectedAccount` 从 `sched:acc:<id>` 重新水合,
RPM 计数与用户消息队列限速读的是完整账号,不受影响;缺的只是选号阶段这道门。

修法沿用投影里既有的处理方式(见 `openai_passthrough` 一组注释):把判定依赖的
键补进白名单,而不是在判定侧另开一条读取完整账号的路径。

回归测试覆盖序列化往返后的三个键:`tiered` 子用例证明缓冲被裁掉时红区会被误判成
黄区,`sticky_exempt` 子用例证明策略被裁掉时会退回三区判定。

Refs #7225
2026-09-18 13:37:58 +07:00
little-greenbean 3fdd54ca12 fix(upstream): cancel response attempt before closing body 2026-09-18 13:30:35 +08:00
feeeei 57b7dbdc85 feat(affiliate): 提取记录支持登记线下提现
管理员在站外给邀请人打款后,系统里原本无法扣减对应的返利额度,用户仍可把
这部分额度转入余额。现在可在「邀请返利 → 提取记录」点「登记线下提现」,
从该用户的可提取返利额度中扣除已打款金额:

- POST /api/v1/admin/affiliates/users/:user_id/withdraw,请求体只含 amount,
  金额按 8 位小数舍入后校验
- 先解冻已到期的冻结额度,再以 aff_quota >= amount 为条件原子扣减,写入
  action=withdraw 的流水并带额度快照;额度不足返回 AFFILIATE_QUOTA_INSUFFICIENT
- 不新增字段与迁移:线下提现复用已有 action 列区分,操作人由管理端审计日志记录
- 提取记录同时列出转入余额与线下提现,新增「类型」列
- 弹窗选择用户后展示可提取额度,支持一键填入全部;从未产生返利的用户按 0 处理

返利记录改为 LEFT JOIN 订单与被邀请人,兑换码、管理员充值来源的返利也会出现
在明细中,接口里订单相关字段与被邀请人 ID 可为 null。
2026-09-17 23:42:02 +08:00
Wesley Liddick 18328aed05 Merge pull request #7212 from wucm667/feat/issue-7203-redemption-pagination
feat(redeem): paginate user redemption history
2026-09-17 16:43:06 +08:00
Wesley Liddick 9efc167482 Merge pull request #7195 from bitnom/fix/refresh-paused-oauth-accounts
fix(account): keep refreshing OAuth tokens for paused accounts
2026-09-17 16:41:54 +08:00
wucm667 a9ed218989 feat(redeem): paginate user redemption history 2026-09-16 13:19:15 +08:00
HuntercodeT 18d483c2a1 fix: 分组用量汇总不再全表扫 usage_logs
GET /api/v1/admin/groups/usage-summary 在有一定数据量的库上会退化成
usage_logs 全表扫。根因是 getAllGroupUsageSummaryFromRollups 里,tail CTE
的 created_at 下界 state.tail_start 由同一条 SQL 的 state CTE 算出、再用
CROSS JOIN 交给 tail:这个值对 planner 来说要到运行期才知道,既进不了
index cond,也估不准选择性,于是只能顺序扫全表再逐行过滤。

生产实测(1721 万行 usage_logs / PostgreSQL 17):

    Nested Loop  (actual time=28161..48334 rows=34788)
      Join Filter: (ul.created_at >= state_1.tail_start)
      Rows Removed by Join Filter: 17178082
      ->  Seq Scan on usage_logs  (actual time=0.030..47567 rows=17212870)
          Buffers: shared hit=12863 read=951875
    Execution Time: 48720 ms

改成先读一次水位行、在 Go 侧算出 tail_start / closed_before / retained
日期,再把它们当查询参数传进来。同一套数据上:

    Index Scan using idx_usage_logs_created_at on usage_logs
      Index Cond: (created_at >= $7)
    Execution Time: 34 ms

水位有效性的判定(恰好一行、时区名与配置一致、水位不在未来)和无效时
退化成从 epoch 全量重算的行为,与原先 SQL 里 state_values / CASE WHEN
valid 的分支逐条对应,结果口径不变。

另外读取水位的语句不加锁,不参与 #6976 那条 FOR UPDATE / FOR KEY SHARE
的阻塞链。
2026-09-16 10:11:18 +08:00
bitnom 8e34ca5e31 fix(account): keep refreshing OAuth tokens for paused accounts
Paused (schedulable=false) active accounts were excluded from the
background token refresh candidates, so their stored access_token
silently expired. The admin usage-window quota probe uses the stored
token verbatim (no token provider / no refresh), so the probe then gets
HTTP 401 UNAUTHENTICATED and the dashboard reports a false "needs
re-auth" for a perfectly healthy account, while Test Connection and live
traffic (which refresh on demand via the token provider) keep working.

Pause is not permanent rejection: error accounts are already excluded by
the status = 'active' filter, and accounts whose refresh genuinely fails
are rate-limited by the retry-cooldown exclusion (10-minute retry-
exhausted cooldown). Refresh success only persists credentials and never
touches schedulable, so a paused account cannot be silently un-paused.

Drop the schedulable filter and update the SQL-filter test, the
integration pagination test, and the service-level candidate fake.
2026-09-15 10:00:26 -04:00
haruka 6582260bc8 feat: add configurable rolling log retention 2026-09-15 14:21:15 +08:00
alfadb a1d5968b24 feat(ollama): async rate-limit reset for Ollama Cloud usage windows
Real Ollama Cloud 429s are driven by ollama.com usage windows, not by
response headers that OpenAI/Anthropic/CN rate-limit parsing understands.
Handle them separately: apply a never-shortening temporary cooldown first,
then schedule an async probe that learns the true usage-window reset point
with a controlled, coalesced per-api_key refresh.

- ratelimit_service: route real Ollama Cloud usage accounts to a dedicated
  429 path before the CN-provider branch.
- ollama_cloud_usage: add a background probe coordinator (started in Start,
  waited by Stop) that fans out model-429 probes to one controlled refresh
  per api_key group, reusing the recent group snapshot for coalesced members.
- repository/account_repo: add SetRateLimitedIfUnchanged, a write-back CAS
  counterpart to ClearRateLimitIfObserved, so an async reset never overwrites
  a newer 429, an admin clear, a re-armed generation, or a key/state change
  observed by another writer between read and write.
- wire: inject OllamaCloudUsageService into RateLimitService.

Tests: unit tests for service and repository pass; targeted race tests pass;
build and vet clean; git diff --check clean. The CAS integration test compiles
but is not run (no Docker available).
2026-09-13 10:41:47 +08:00
Wesley Liddick c7ed614240 Merge pull request #7001 from SavitarC/feat/ops-token-stats-all-platforms
feat(ops): 扩展 Token 请求统计至所有平台
2026-09-12 19:34:45 +08:00
xpflyingandClaude Fable 5.1 9eb120dd40 fix(openai): 隐私/账号检查客户端改用 Firefox 指纹,规避 chatgpt.com Cloudflare 质询
chatgpt.com 的 Cloudflare 会对 req 内置的 ImpersonateChrome(TLS/HTTP2 指纹与 UA 均为
Chrome/120)返回 403 cf-mitigated=challenge,导致 accounts/check、subscriptions、
account_user_setting 三个 backend-api 调用长期失败:订阅到期时间不再更新、隐私开关显示 Fail。
仅改 UA 版本号无效(指纹仍是 120),Firefox 伪装在同一出口下稳定通过,故切换。

同时:
- 用 cf-mitigated 响应头识别质询页,隐私设置失败时正确标记为 training_set_cf_blocked
- accounts/check / subscriptions 失败日志由 Debug 提升为 Warn 并附 cf_challenge 字段,不再静默

Closes #4825

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 18:10:15 +08:00