Compare commits

...
946 Commits
Author SHA1 Message Date
Wesley Liddick 5de5e2bed0 Merge pull request #6874 from hansnow/fix/gpt-image-2-5-oauth
fix(openai): 支持 Image 2.5 并修复 OAuth 生图主控模型
2026-09-09 14:43:55 +08:00
hansnow 7ccc8a6f50 fix(openai): 支持 Image 2.5 并修复 OAuth 生图主控模型 2026-09-09 11:04:59 +08:00
shaw 270eac6973 chore: update sponsors 2026-09-08 21:18:24 +08:00
shaw 570106ac17 chore: update sponsors 2026-09-08 20:57:43 +08:00
Wesley Liddick 775348b402 Merge pull request #6706 from ouchihao/codex/issue-3679-draggable-custom-page-button
feat(custom-page): make the open-link button draggable
2026-09-08 20:47:42 +08:00
Wesley Liddick d9efbf8229 Merge pull request #6839 from mason0510/fix/grok-external-web-access
fix(grok): 移除 Chat Completions 中不支持的 external_web_access 参数
2026-09-08 20:47:25 +08:00
Wesley Liddick 233f3dffe6 Merge pull request #6281 from wuji-labs/feat/http2-long-stream-keepalive
feat(upstream): add HTTP/2 PING keepalive for long LLM generation streams
2026-09-08 20:47:07 +08:00
Wesley Liddick f9d7826711 Merge pull request #6513 from anguobao123/codex/optional-apple-container-network-subnet
feat(deploy): support optional Apple network subnet
2026-09-08 20:46:51 +08:00
Wesley Liddick c54897a59d Merge pull request #6815 from Pluviobyte/codex/fix-directed-proxy-backups
fix(proxies): make backup references directed and shareable
2026-09-08 20:46:32 +08:00
Wesley Liddick 29646ba6b4 Merge pull request #6488 from aofee/codex/feat-grok-media-eligibility-ui
feat: add Grok media eligibility controls
2026-09-08 20:46:20 +08:00
Wesley Liddick 6f0d0ababc Merge pull request #6320 from spongehah/fix/runtime-block-honor-persisted-cooldown
fix(schedule): 选号按持久化冷却条件清除本机账号级阻断
2026-09-08 20:46:07 +08:00
Wesley Liddick 52f7bcaedd Merge pull request #6759 from ssharkkky/feat/cmv2-user-ranking-toggle
feat(channel-monitor-v2): add user ranking toggle and keep missing TTFT neutral
2026-09-08 20:44:07 +08:00
Wesley Liddick 95acbf1f03 Merge pull request #6424 from cyhhao/fix/bound-ops-system-log-storage
fix(ops): bound database-backed system log growth
2026-09-08 20:43:54 +08:00
Wesley Liddick 43569bb44c Merge pull request #6434 from swjturay/codex/upstream-client-disconnect-drain
fix(openai-ws): drain upstream after client disconnect
2026-09-08 20:42:59 +08:00
Wesley Liddick 15557e7796 Merge pull request #6235 from StarryKira/codex/fix-6232-channel-cache-invalidation
fix: broadcast channel cache invalidations across instances
2026-09-08 20:42:45 +08:00
Wesley Liddick e2fd418a96 Merge pull request #6372 from clansty/fix/openai-oauth-429-fallback-disabled
fix: 修复关闭 429 默认回避后仍触发 OAuth 运行时冷却
2026-09-08 20:42:35 +08:00
Wesley Liddick 4e9b01fd59 Merge pull request #6838 from atogumo/fix/claude-code-probe-any-model
fix(gateway): accept Claude Code max_tokens=1 probes for any model
2026-09-08 20:42:24 +08:00
Wesley Liddick aa167a25a7 Merge pull request #6775 from xlplbo/fix/plugin-zip-handle-windows
fix(plugin): 提交插件包前先关闭 ZIP 读取器,修正 Windows 下两例单测
2026-09-08 20:12:42 +08:00
Wesley Liddick 5cbb9191a7 Merge pull request #6836 from r266-tech/fix-proxy-expiry-json-range
fix(proxies): reject malformed list responses
2026-09-08 20:12:30 +08:00
Wesley Liddick ae4cc14b28 Merge pull request #6819 from LeeeeeeM/codex/fix-registration-visibility
fix: respect registration visibility on login
2026-09-08 20:12:14 +08:00
Wesley Liddick fa556700c2 Merge pull request #6762 from ouchihao/codex/issue-2045-payment-help-markdown
feat(payment): render help text as sanitized Markdown
2026-09-08 20:11:56 +08:00
Wesley Liddick 68773aab98 Merge pull request #6764 from ouchihao/codex/issue-1499-select-failed-token-refresh
fix(accounts): keep failed token refresh accounts selected
2026-09-08 20:11:08 +08:00
Wesley Liddick 1e6114c270 Merge pull request #6812 from Pluviobyte/codex/fix-model-plaza-subscriptions
fix(model-plaza): include active subscription groups
2026-09-08 20:10:42 +08:00
Wesley Liddick 9e7039ebca Merge pull request #6811 from Pluviobyte/codex/fix-proxy-partial-update
fix(proxies): preserve omitted expiry and fallback settings
2026-09-08 20:10:20 +08:00
Wesley Liddick 1923d1c27d Merge pull request #6814 from feeeei/fix/go-redis-pool-nil-ctx-panic
fix(redis): 升级 go-redis 至 v9.22.0,修复连接池 nil-ctx panic 竞态
2026-09-08 20:09:59 +08:00
Wesley Liddick b638445234 Merge pull request #6754 from ccemoji/codex/minimal-upstream-fix
fix: cancel streaming upstream before closing body
2026-09-08 20:09:37 +08:00
Wesley Liddick 9bcf8a5b80 Merge pull request #6760 from wucm667/feat/issue-6752-weekly-cost-estimate
feat(accounts): show estimated weekly account cost
2026-09-08 20:09:25 +08:00
Wesley Liddick 979fe247f9 Merge pull request #6821 from 22zyfeng-collab/fix/antigravity-oauth-plan-type
fix(antigravity): preserve OAuth plan_type when saving accounts
2026-09-08 20:09:12 +08:00
Wesley Liddick 6378fb0cba Merge pull request #6798 from savvym/codex/fix-account-menu-overflow
fix(frontend): 修复账号操作菜单在视口底部被截断
2026-09-08 20:08:53 +08:00
Wesley Liddick 3ee92b40c5 Merge pull request #6659 from ouchihao/codex/issue-77-remove-inactive-groups
fix(accounts): allow removing inactive group assignments
2026-09-08 20:08:40 +08:00
Wesley Liddick 4892b8f17e Merge pull request #6810 from Pluviobyte/codex/fix-announcement-proxy-date-range
fix: validate announcement and proxy date ranges
2026-09-08 20:08:24 +08:00
Wesley Liddick 28b807a735 Merge pull request #6816 from Pluviobyte/codex/fix-repeated-proxy-fallback
fix(proxies): allow repeated expiry fallback and preserve origin
2026-09-08 20:07:08 +08:00
atogumo 4e5d67df35 fix(gateway): accept Claude Code max_tokens=1 probes for any model
The claude_code_only probe exemption only fired for models containing
"haiku". Claude Code also sends max_tokens=1 requests without a system
prompt to the model currently in use, e.g. when switching models or
refreshing the context usage, so on sonnet/opus groups those requests failed
the system-prompt check and were rejected as non-Claude-Code clients.

Recognise the probe from the request body itself (max_tokens == 1) once the
User-Agent check has passed, regardless of model, and carry max_tokens into
the ParsedRequest projection so the hot path sees it too. The haiku context
flag and the local haiku interception are unchanged; non-haiku probes reach
the pool as ordinary one-token requests. One output token offers nothing to
a client that already forges the CLI User-Agent, so this widens no attack
surface.

Fixes #6591
2026-09-08 19:12:37 +08:00
Wesley Liddick dbe92a1c24 Merge pull request #6758 from DuskLin/feat/minimax-platform
feat(minimax): 增加 MiniMax 平台支持
2026-09-08 18:50:06 +08:00
r266-tech 188e3a9f90 fix(proxies): reject malformed list responses 2026-09-08 18:29:17 +08:00
刘佳林 10940ecf39 fix(minimax): gofmt MiniMax allowlist comments
golangci-lint gofmt failed on aligned comments in the default upstream host list.
2026-09-08 17:49:11 +08:00
刘佳林 3495635a52 fix(minimax): 补齐阈值停调与额度监控接线
调度阈值评估和渠道监控 quota 取数补上 MiniMax 分支;GetCodingPlanProvider 改为只认官方域名,避免把中转 Key 发到厂商额度端点。composite /v1/models 与 Codex 目录纳入 MiniMax;迁移改名为 237;allowlist 去掉未使用的 www.* / api.minimax.cn,abab 前缀收窄到 5/6/7。
2026-09-08 17:41:29 +08:00
mason0510 c8deeb0b05 fix(grok): 移除 Chat Completions 中不支持的 external_web_access 参数
模型:Claude Sonnet 5
修复内容:在 Grok Chat Completions raw 转发流程中添加 external_web_access 参数过滤,修复 Grok API 返回 400 "Argument not supported: external_web_access" 错误
修改理由:Grok xAI API 不支持 external_web_access 参数,Sub2API Responses API 路径已有过滤,但 Chat Completions 路径缺失,导致 grok-4.5 等模型调用失败返回 502
验证结果:代码已通过本地 Demo 验证递归删除逻辑正常,待远端编译部署后验证真实 API 调用
影响范围:仅影响 Grok 平台 Chat Completions API 请求体处理,不影响 Responses API 和其他平台
回滚方式:git revert 本次提交,或手动移除 openai_gateway_chat_completions_raw.go line 159-163 的 sanitizeGrokUnsupportedFields 调用

[AI: 梁心语/Claude Sonnet 5 @ 2026-09-08T17:30]
2026-09-08 16:41:09 +08:00
22zyfeng-collab b6ee9f0a32 fix(antigravity): preserve OAuth subscription plan across account creation 2026-09-08 15:02:32 +08:00
G.Wood-Sun 6c8ad0bd4a fix: respect registration visibility on login 2026-09-08 14:44:41 +08:00
Rain 7137fae2a6 test(proxies): preserve incoming references when editing backups 2026-09-08 13:58:34 +08:00
Rain 859de4c8fd fix(proxies): allow repeated expiry fallback and preserve origin 2026-09-08 13:56:20 +08:00
Rain e835f79819 fix(proxies): make backup references directed and shareable 2026-09-08 13:56:19 +08:00
feeeei 99eba19ffc fix: 升级 go-redis 至 v9.22.0 修复连接池 nil-ctx panic
上游竞态:调用方超时后 wantConn.ctx 被置 nil,拨号 goroutine 用 nil ctx
创建超时上下文触发 panic(go-redis#3630,v9.18.0 起修复)。
生产环境在连接池饥饿 + 请求超时风暴下每秒触发上万次。

同步将 ZRangeByScore 迁移至 ZRangeArgs:v9.22.0 将其标记为 deprecated,
golangci-lint staticcheck 拦截(SA1019)。ZRangeArgs{ByScore: true} 与原
调用发出的 Redis 命令参数完全等价(LIMIT 编码逻辑相同)。
2026-09-08 13:46:59 +08:00
Rain 38cfd7e2d9 fix(proxies): preserve omitted expiry and fallback settings 2026-09-08 13:38:41 +08:00
Rain 3fc08745a6 fix(model-plaza): include active subscription groups 2026-09-08 13:37:53 +08:00
Rain efc6e4a816 fix: validate announcement and proxy date ranges 2026-09-08 13:37:52 +08:00
savvym df64b5f368 fix(frontend): keep account action menu within viewport 2026-09-08 10:46:56 +08:00
github-actions[bot] 772a0382f0 chore: sync VERSION to 0.2.3 [skip ci] 2026-09-08 02:35:11 +00:00
Wesley Liddick 8fa67d477d Merge pull request #6791 from Wei-Shaw/fix/group-model-allowlist-schema-repair
fix(db): 升级自愈 groups.model_allowlist 列,修复 API 密钥/订阅列表加载失败
2026-09-08 10:03:23 +08:00
shaw 4a7739acdf fix(db): converge groups.model_allowlist on upgrade
235 只在「models_list_config 存在且 model_allowlist 不存在」时重命名,而迁移一旦
写入 schema_migrations 就会按「文件名 + checksum」整份跳过,不再重跑。因此只要数据库
在 235 之后回到旧结构(为回滚旧版本镜像手工改回列名、或按旧结构做了部分恢复),
应用依旧能正常启动,但每个关联 groups 的查询都会失败:

    pq: column groups.model_allowlist does not exist

表现为「API 密钥」「我的订阅」「管理端订阅列表」等页面加载失败。

新增可重放的收敛迁移 236,升级即自愈:只有旧列则重命名(数据原样保留);两列并存
则在新列仍为默认空值时回填旧列配置;两列都没有则按默认值补建。收尾统一把
model_allowlist 归一成 NOT NULL DEFAULT '{}',与 ent schema 对齐。

判定列是否存在改用 attrelid = 'groups'::regclass(跟随 search_path),
不再写死 table_schema = 'public',避免与 ALTER TABLE 的解析路径分叉。

Fixes #6780
2026-09-08 09:15:23 +08:00
Wesley Liddick 14e0a49e17 Merge pull request #6767 from alfadb/fix/ollama-cloud-max-tokens-all-platforms
fix(ollama): 修复 DeepSeek 多协议输出上限并自动选择 Bearer 鉴权
2026-09-08 08:26:51 +08:00
Wesley Liddick b439dddfda Merge pull request #6772 from ZeroClover/fix/account-test-model-names
fix(accounts): populate display names for live test models
2026-09-08 08:25:54 +08:00
xlplboandClaude Fable 5.1 b42157a725 test(service): 修正 Windows 下两例环境相关用例
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAJmqcVA9TLacTCD1bLh4i
2026-09-07 23:21:29 +08:00
xlplboandClaude Fable 5.1 335fcdc1d6 fix(plugin): 提交插件包前先关闭 ZIP 读取器
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAJmqcVA9TLacTCD1bLh4i
2026-09-07 23:21:29 +08:00
Zero Clover e094a3f40b fix(accounts): populate display names for live test models 2026-09-07 21:59:35 +08:00
alfadb 57387445f3 fix(ollama): select Bearer auth for Anthropic endpoints automatically 2026-09-07 21:54:22 +08:00
alfadb 2fc24d8879 fix(ollama): normalize Messages base URLs before token clamping 2026-09-07 21:54:22 +08:00
alfadb cc91155fe4 fix(ollama): clamp DeepSeek output limits across API protocols 2026-09-07 21:54:21 +08:00
github-actions[bot] b7dba62678 chore: sync VERSION to 0.2.2 [skip ci] 2026-09-07 13:01:57 +00:00
Wesley Liddick 5485f368b2 Merge pull request #6690 from alfadb/fix/astra-pro-mode
fix(openai): 保留 Codex OAuth 请求中的 Astra reasoning.mode
2026-09-07 20:42:37 +08:00
Wesley Liddick f8351e9e33 Merge pull request #6658 from ZeroClover/feat/group-model-allowlist
feat(groups)!: enforce model allowlists across gateway endpoints
2026-09-07 20:25:57 +08:00
chihao.ou 941a0487ab fix(accounts): allow removing inactive group assignments 2026-09-07 20:05:08 +08:00
chihao.ou 7f0f579bb0 fix(accounts): keep failed token refresh accounts selected 2026-09-07 20:01:24 +08:00
chihao.ou dfb3978b9d feat(payment): render help text as sanitized Markdown 2026-09-07 19:51:03 +08:00
shaw 2031c1d5ad fix(groups): merge main and resolve model allowlist conflicts
Resolve three conflicts from main's #6680 (supplementUnmappedOpenAIModels):

1. gateway_handler.go — keep PR's allowlist refactoring (ModelAllowlistEnabled,
   writeAllowlistedModelsList, modelListingSource); main's OpenAI branch in
   writeModelsList was auto-merged.
2. group_models_list.go (modify/delete) — delete file per PR intent; relocate
   supplementUnmappedOpenAIModels to group_model_allowlist.go where gateway_service
   and openai_codex_models_service can still call it.
3. openai_codex_models_service.go — combine main's supplementUnmappedOpenAIModels
   wrapping with PR's ModelAllowlistEnabled() guard.

Also fix two auto-merged test files that still referenced the old
GroupModelsListConfig / ModelsListConfig types.
2026-09-07 19:44:03 +08:00
刘佳林and刘佳林 6f2295bfc7 test(minimax): 按平台注册表推导调度器 snapshot 期望数量
新增 MiniMax 后 canonical token / 账号查询数增加,硬编码期望值过期。
改为从 schedulerSnapshotPlatforms 派生 bucket 与 query 数量,避免下次加平台再踩坑。
账号查询计数辅助函数仅用于单测,放在 unit test 文件以免 unused。

Co-authored-by: 刘佳林 <DuskLin@users.noreply.github.com>
2026-09-07 11:40:11 +00:00
Wesley Liddick 8193b80a59 Merge pull request #6743 from MarushiruDonato/fix/gpt6-astra-instructions
fix(openai): 修正 GPT-6 Astra 提示词选择
2026-09-07 19:22:43 +08:00
shaw f6d580200a fix: update #6688 test assertions for #6678 image modality and #6718 ultra level
- TestCodexAliasFailoverMappingHonorsModelRouting/no_routing_rule: expect
  ["text","image"] instead of ["text"] — image input is resolved per-account
  via isOpenAICodexImageInputModel, independent of routing rules
- TestCodexAliasFailoverMappingHonorsModelRouting/routing_rule: expect
  effort levels to include "ultra" (added by #6718)
2026-09-07 19:13:01 +08:00
shaw b491c3271b fix: rename redeemMaxErrorsPerHour to redeemMaxFailedAttempts in tests
#6669 renamed the constant from redeemMaxErrorsPerHour to
redeemMaxFailedAttempts (fixed 10-minute window), but #6670's tests
still referenced the old name, breaking compilation on main.
2026-09-07 19:00:04 +08:00
shaw d106b3b5a0 fix: resolve merge conflict with main and fix #6718 missing group parameter
- Resolve conflict in openai_codex_models_service_test.go: keep both
  GPT-6 Astra instructions template assertion (from this PR) and ultra
  reasoning level + multi_agent fields (from #6718 on main)
- Fix compilation error on main: add missing `group *Group` parameter
  to buildCodexModelsManifestForAccounts calls in
  openai_codex_model_metadata_test.go (lines 24, 32) — caused by #6688
  changing the function signature while #6718 added new callers with
  the old signature
2026-09-07 18:58:00 +08:00
Wesley Liddick 10fe591e54 Merge pull request #6660 from ouchihao/codex/issue-312-onboarding-button-shadow
fix(onboarding): remove button text shadows
2026-09-07 18:51:44 +08:00
Wesley Liddick 66cd8a72bc Merge pull request #6738 from Milesians/codex/fix-interval-multiplier-display
fix: 修正区间倍率展示并补齐模型广场 1h 缓存价格
2026-09-07 18:51:00 +08:00
Wesley Liddick dc46daa690 Merge pull request #6726 from DuskLin/feat/openai-reasoning-effort-mapping-deny
feat(groups): 推理强度映射转发值支持拒绝
2026-09-07 18:50:41 +08:00
Wesley Liddick 65246e69d7 Merge pull request #6643 from MarushiruDonato/fix/codex-manifest-reactive-vmodel
fix(frontend): 修复 Codex Manifest 编辑状态丢失
2026-09-07 18:50:27 +08:00
Wesley Liddick de69bf1e03 Merge pull request #6655 from ouchihao/codex/issue-275-account-expiry-presets
feat(accounts): add month and year expiry presets
2026-09-07 18:50:13 +08:00
Wesley Liddick eba85dc478 Merge pull request #6680 from cyhhao/fix/openai-mixed-account-model-list
fix(openai): retain default models in mixed account catalogs
2026-09-07 18:50:03 +08:00
Wesley Liddick abf70750fd Merge pull request #6669 from ssharkkky/fix/redeem-fixed-failure-window
fix(redeem): use a fixed 10-minute failure window
2026-09-07 18:49:51 +08:00
Wesley Liddick 88b697d5df Merge pull request #6673 from ssharkkky/fix/backup-migrations-advisory-lock
fix(backup): serialize pg_dump/psql with the migrations advisory lock
2026-09-07 18:49:34 +08:00
Wesley Liddick 3eb5a84ea1 Merge pull request #6700 from qiu121/codex/fix/deepseek-account-cost-peak
fix(billing): 修复 DeepSeek 高峰账号成本少计
2026-09-07 18:49:19 +08:00
Wesley Liddick 71e5a4baf7 Merge pull request #6486 from aofee/codex/fix-grok-media-inconclusive-eligibility
fix(grok): do not reject media accounts on incomplete billing
2026-09-07 18:48:39 +08:00
Wesley Liddick 4bfcbd0c78 Merge pull request #6498 from aofee/codex/fix-ws-pending-turn-terminal
fix(openai-ws): track pending passthrough turns
2026-09-07 18:48:04 +08:00
Wesley Liddick 2b1e5e7f5c Merge pull request #6429 from spin6lock/fix/billing-glm-5.3-flash-fallback-pricing
fix(billing): 补上 glm-5.3 / glm-5.3-flash 兜底价,避免被 glm-5 子串匹配抢走
2026-09-07 18:47:46 +08:00
Wesley Liddick cbb4b7e53e Merge pull request #6678 from gebdalaoli-arch/codex/stale-astra-input-modalities-v021
[codex] Fix stale official Astra input modalities
2026-09-07 18:47:31 +08:00
Wesley Liddick 81c0f11d7a Merge pull request #6736 from qiu121/codex/fix/openai-stream-failure-observability
fix(openai): 记录流式失败的上游诊断信息
2026-09-07 18:46:58 +08:00
Wesley Liddick 561fc1c3e4 Merge pull request #6718 from lyy0709/codex/astra-ultra-catalog
fix(codex): preserve Astra Ultra workflow metadata
2026-09-07 18:46:11 +08:00
Wesley Liddick 43491f67a1 Merge pull request #6733 from AndrewZhuCC/fix/codex-allowed-tools
fix(codex): preserve allowed_tools selection constraints
2026-09-07 18:45:48 +08:00
Wesley Liddick c05bc4d3cc Merge pull request #6705 from ouchihao/codex/issue-4827-usage-api-key-filter
fix(usage): load all API keys for record filters
2026-09-07 18:45:35 +08:00
Wesley Liddick b9ebc8f209 Merge pull request #6637 from varrge/fix/gemini-flash-tier-pricing
fix(billing): resolve Gemini 3.7/3.8 Flash thinking-tier prices
2026-09-07 18:44:55 +08:00
Wesley Liddick f7c48ba59a Merge pull request #6674 from wuji-labs/fix/antigravity-toolconfig-always-present
fix(antigravity): always set toolConfig for reasoning models without tools
2026-09-07 18:44:41 +08:00
Wesley Liddick 22c16d0517 Merge pull request #6708 from r266-tech/r266/fix-ws-turn-rate-limit-6679
fix(openai-ws): recover later passthrough turns from account quota exhaustion
2026-09-07 18:44:18 +08:00
Wesley Liddick 2cc1e7ef90 Merge pull request #6677 from Lynricsy/fix/claude-thinking-block-binding
fix(claude): support thinking block binding beta
2026-09-07 18:43:52 +08:00
wucm667 35e69af41b feat(accounts): show estimated OpenAI weekly cost 2026-09-07 18:37:15 +08:00
Zero Clover 81ff9384cb fix(groups): merge main and resolve model allowlist conflicts 2026-09-07 18:33:39 +08:00
TT 0d3dbae710 feat(channel-monitor-v2): add user ranking toggle and keep missing TTFT neutral
Give operators a V2 setting (channel_monitor_hide_user_ranking) to hide
the user ranking tab and the /users payload from non-admin channel
monitor v2 viewers. Default false (ranking stays visible). Admin
endpoints and views always keep the ranking.

Also stop painting first-token '-' as a red/empty failure when traffic
is sync-only and has no first-token samples: a missing TTFT is
"not applicable" (health-unknown), not a breached latency budget.

Changes:
- new setting key + fail-open parse (only literal "true" hides), wired
  through public/admin settings DTOs, handlers, and the V2 runtime
- ChannelMonitorV2Service.Users returns an empty list to non-admin
  viewers when the toggle is on
- frontend: feature flag + SettingsView toggle + user V2 view hides the
  ranking tab and skips the /users call
- monitorFormat: isTtftUnavailable/ttftDisplayState keep missing TTFT
  neutral

Tests:
- setting parse defaults to visible and only literal "true" hides
- V2 Users honors the toggle for non-admin viewers
- frontend MetricCell/monitorFormat keep missing TTFT neutral
2026-09-07 18:31:03 +08:00
刘佳林 19382f275e feat(minimax): 增加 MiniMax 平台支持
把 MiniMax 做成与 Kimi / 智谱 / DeepSeek 同级的国产供应商平台:账号创建/编辑、分组、Composite 路由、原生 Token Plan / Coding Plan 额度,以及 OpenAI 兼容模型列表。

额度走 GET api.minimaxi.com|io /v1/api/openplatform/coding_plan/remains(Bearer)。只解析 model_name=general 的 remaining percent;周窗口仅在 current_weekly_status=1 时计入。

Related to #6742
2026-09-07 18:22:56 +08:00
alfadb 0aaed397cc fix: preserve Astra reasoning mode for Codex OAuth 2026-09-07 17:58:20 +08:00
Wesley Liddick 909b8c7ef4 Merge pull request #6648 from ouchihao/codex/issue-3667-turnstile-loading
fix(auth): show a loading placeholder for Turnstile
2026-09-07 17:38:11 +08:00
Wesley Liddick b8ff74ad70 Merge pull request #6668 from ssharkkky/fix/channel-monitor-gemini-role
fix(monitors): send explicit user role in Gemini monitor probes
2026-09-07 17:38:00 +08:00
Wesley Liddick 96cf8bd76a Merge pull request #6665 from ssharkkky/fix/grok-cross-client-map-default-doc
fix(settings): describe Grok cross-client mapping default as enabled
2026-09-07 17:37:47 +08:00
Wesley Liddick 1f716aa181 Merge pull request #6667 from ssharkkky/fix/antigravity-user-agent-version
fix(antigravity): refresh default user agent version
2026-09-07 17:37:37 +08:00
Wesley Liddick 111c41cf3f Merge pull request #6684 from SakuraPuare/feat/i18n-build-gate
fix(i18n): fail builds on missing locale keys
2026-09-07 17:37:25 +08:00
Wesley Liddick 787a6a33df Merge pull request #6481 from alfadb/fix/claude-cli-version-floor
fix(claude): 让伪装的 Claude Code 版本真正生效,修复 Fable 5.1 仍然 400
2026-09-07 17:37:12 +08:00
Wesley Liddick f8e1e7fed6 Merge pull request #6711 from wucm667/fix/issue-6701-model-not-found-failover
Fix failover for model-not-found responses
2026-09-07 17:36:58 +08:00
Wesley Liddick f09a5b602e Merge pull request #6704 from aofee/codex/fix-http-bridge-session-isolation
fix(openai-ws): 修复 HTTP bridge 并发连接抢占与状态串用
2026-09-07 17:36:36 +08:00
Wesley Liddick 027e3f2d54 Merge pull request #6688 from suixincrazy/fix/codex-manifest-failover-alias
fix(openai): honor model_routing for Codex manifests and scheduling
2026-09-07 17:36:26 +08:00
Wesley Liddick 8116d235ef Merge pull request #6629 from superdav42/fix/preserve-function-arguments-done
fix(apicompat): preserve function arguments from done events
2026-09-07 17:36:10 +08:00
Wesley Liddick 959cbe3d87 Merge pull request #5453 from wucm667/fix/issue-5451-ws-passthrough-before-turn
fix(openai): reacquire passthrough turn slots
2026-09-07 17:35:57 +08:00
Wesley Liddick c0420e2b8a Merge pull request #6484 from aofee/codex/fix-fable-credits-required-scope
fix(anthropic): keep Fable credits_required model-scoped
2026-09-07 17:33:59 +08:00
Wesley Liddick 9578ddbddd Merge pull request #6687 from Chenning-Tao/codex/fix-native-delegation-ws
fix(openai): preserve named standalone Responses inputs
2026-09-07 17:31:17 +08:00
Wesley Liddick e274de45b7 Merge pull request #6702 from a1gaoshanlaiyige/fix/message-cache-json-escaping
fix(gateway): use JSON escaping for message cache promotion
2026-09-07 17:30:54 +08:00
Wesley Liddick ce328fb37a Merge pull request #6670 from ssharkkky/fix/payment-fulfillment-isolation
fix(payment): isolate fulfillment from redeem limits
2026-09-07 17:30:43 +08:00
ccemoji be4ab92b2b fix: cancel streaming upstream before closing body 2026-09-07 16:47:51 +08:00
Wesley Liddick 8f7f2b5d92 Merge pull request #6727 from wucm667/feat/issue-6723-subscription-usage-link
feat(admin): link subscriptions to user usage
2026-09-07 16:45:32 +08:00
alfadb 3efeba33a4 test(claude): 版本覆盖用例字面量随内置基线升至 2.1.259
这三个用例的字面量 2.1.251 写于内置基线为 2.1.220 的时期,本意是取一个
高于基线的补丁位验证"覆盖不得低于 pin"规则。基线升至 2.1.258 后,2.1.251
落在 pin 之下:IsSupportedCLIVersion 应判 false、resolveCLIVersion 应回落
基线,与用例原意相悖,CI 随之失败。

改为真正高于当前 pin 的最小补丁位 2.1.259,用例语义不变;实现未放宽,
constants_cli_version_test.go 中独立的 Fable 5.1 闸门下限 2.1.251 保持不变。
2026-09-07 16:34:35 +08:00
alfadb ce157b32ed fix(claude): 指纹自愈路径同样执行 claude-cli 版本下限
GetOrCreateFingerprint 的 heal 分支(畸形缓存自愈)此前在版本下限逻辑之外:
畸形缓存 + 合法但过旧的客户端 UA 会在首次读取时把过旧 UA 原样落库并返回,
下一次读取才被抬到下限——该窗口内账号仍以过旧版本撞上游客户端版本闸门
(PR #6481 review 指出的边界 case)。

修复:把 floorClaudeCLIUserAgentVersion 后置块从 healthy else 内移到
heal/healthy if-else 之后、TTL 续期检查之前,作为两条缓存命中路径的共同
后置条件;heal 分支 needWrite 本已为 true,单次落库语义不变。

测试:TestGetOrCreateFingerprintHealsPoisonedCacheUsingValidClientUA
期望改为首次返回/落库即 claude-cli/CLICurrentVersion(保留 ClientID 不变、
恰好一次落库断言);floor/UA 全部既有用例保持绿。
2026-09-07 16:34:35 +08:00
alfadb 3cb2381bdc fix(claude): 让伪装的 Claude Code 版本真正生效,修复 Fable 5.1 仍然 400
claude-fable-5-1 已登记进 DefaultModels,但 OAuth 账号实际调用仍然全部失败:

  400 claude_code_version_too_old
  "Claude Code 2.1.220 does not support this model;
   version 2.1.251 or newer is required."

两个独立的原因,缺一不可:

1. CLICurrentVersion 停留在 2.1.220,低于上游对该模型的客户端版本闸门。
   升到 2.1.258(当前官方 CLI 最新版,npm @anthropic-ai/claude-code)。

2. 只升常量对所有已有账号无效。上游请求实际发送的 User-Agent 来自账号级
   持久指纹(IdentityService,Redis fingerprint:<accountID>),不是
   DefaultHeaders;defaultFingerprint 只在首次创建指纹时使用。
   GetOrCreateFingerprint 的缓存命中路径只有两条改写分支:UA 畸形时自愈,
   或客户端送来更新版本时升级。指纹 7 天 TTL 但活跃账号每 24h 续期,实际
   近乎永不过期,系统内也没有重置入口——客户端版本比缓存旧时(例如缓存
   2.1.220、客户端 2.1.75),存量账号会永远对上游声称 2.1.220。

   生产实测:升级 CLICurrentVersion 并部署后仍报同样的 400,读 Redis 确认
   出问题账号的缓存值为 "claude-cli/2.1.220 (external, cli)"。

修复:新增 floorClaudeCLIUserAgentVersion,把 CLICurrentVersion 作为缓存
指纹里 claude-cli 版本的下限,缓存命中与首次创建两条路径共用。

  - 只对 claude-cli 产品生效,其它客户端一律不动
  - 只升不降:版本等于或高于下限(含客户端上报的更新版本)不做任何改动
  - 只替换版本号段,UA 其余部分原样保留,例如
    "(external, claude-desktop-3p, agent-sdk/0.3.100)" 不受影响
  - 与既有的"客户端更新版本"升级分支相互独立、二者取更新者
  - X-Stainless-* 维持既有 merge 语义,不被下限逻辑覆盖

cc_version 无需单独处理:billing attribution block 的版本已由
syncBillingHeaderVersion 跟随实际发送的 User-Agent 同步。

测试:identity_service_version_floor_test.go 覆盖 helper 的 8 种 UA 形态与
5 个 GetOrCreateFingerprint 场景,其中一个直接复现线上故障(缓存 2.1.220 +
客户端 2.1.75 → 写回 2.1.258,Stainless 字段保持不变)。
constants_cli_version_test.go 锁定常量与 DefaultHeaders 的 UA 同源,并断言
不低于 Fable 5.1 的闸门下限。回退实现后对应用例确认转红。

identity_service_user_agent_validation_test.go 中 3 个把版本钉死在旧字面量
的既有用例改为钉 CLICurrentVersion 或更高版本——它们断言的正是本次有意改变
的行为,各自的原始意图(哨兵拒绝、合法升级生效、健康缓存不重写)保持不变。
2026-09-07 16:34:35 +08:00
Wesley Liddick a7d2c782df Merge pull request #5717 from wucm667/feat/issue-2629-current-main
feat(simple-mode): allow basic account grouping
2026-09-07 15:43:22 +08:00
Wesley Liddick a3aa9bae6d Merge pull request #6662 from ZeroClover/fix/codex-manifest-pinned-accounts
fix(openai): reuse pinned accounts for model lists and fix manifest controls
2026-09-07 15:35:49 +08:00
Robert Guo b939fa9d4a fix(openai): select GPT-6 Astra instructions correctly 2026-09-07 14:12:21 +08:00
milesians d03c42d798 fix: size plaza pricing columns to fit cache prices 2026-09-07 11:56:53 +08:00
qiu121 6aabbdf547 fix(openai): 记录流式失败的上游诊断信息
- 在 Responses 流已经输出语义内容后记录 response.failed 的上游事件,使运维可以查询原始错误、账号和上游 request ID,定位 OpenAI 临时处理失败而不丢失诊断上下文。
- 保留已提交语义内容后禁止切号重放的安全边界,避免客户端收到重复或混合响应;补充原生与透传路径的回归测试,确保该错误可观测且行为一致。

Refs: #5165, #6714
2026-09-07 11:27:08 +08:00
lyy0709 ad4b2f30f5 test(codex): assert alias workflow effort before dereferencing 2026-09-07 11:23:11 +08:00
milesians ff758f37d2 fix: preserve plaza 1h pricing and align interval fallbacks 2026-09-07 11:07:35 +08:00
zhuanzhi 2e31d8b702 fix(codex): preserve allowed_tools selection constraints
Keep allowed_tools policies intact for upstream validation and rewrite their
function references alongside reserved tool-name aliases.
2026-09-07 10:47:51 +08:00
wucm667 8c8fd6e298 feat(admin): link subscriptions to user usage 2026-09-07 10:13:18 +08:00
刘佳林 f62ec2e4a0 feat(groups): 推理强度映射转发值支持拒绝
分组推理强度映射的转发值新增 deny。命中对应请求档位时直接拒绝请求,
返回 403 permission_error,而不是改写后转发。
2026-09-07 10:12:42 +08:00
milesians 415a0dc9d0 fix: display multiplier-only interval prices 2026-09-07 10:11:07 +08:00
wucm667 b59f3bf46c fix(simple-mode): enforce basic group boundaries 2026-09-07 09:20:48 +08:00
lyy0709 2db78bd3d6 fix(codex): preserve Astra Ultra workflow metadata 2026-09-07 01:12:55 +08:00
Zero Clover f88d62ad28 fix(openai): use live account models in connection tests 2026-09-07 00:23:49 +08:00
wucm667 8363d537e0 fix(gateway): fail over model-not-found responses 2026-09-06 20:38:17 +08:00
r266-tech b8ed245085 fix(openai-ws): recover later turns from quota exhaustion 2026-09-06 18:42:06 +08:00
chihao.ou 76efc52fb6 feat(custom-page): make the open-link button draggable 2026-09-06 17:52:24 +08:00
chihao.ou dc6b318c34 fix(usage): load all API keys for record filters 2026-09-06 17:35:10 +08:00
aofee 96884dd1ae fix(openai-ws): isolate HTTP bridge connection state 2026-09-06 17:05:06 +08:00
a1gaoshanlaiyige 0b061c5fa1 fix(gateway): use JSON escaping for message cache promotion
Use encoding/json instead of Go string quoting when promoting message content to cached text blocks. Add regression coverage for control characters, Unicode, literal backslashes, text preservation, and idempotence.
2026-09-06 16:42:57 +08:00
qiu121 958a21ad6e fix(billing): 修复 DeepSeek 高峰账号成本少计
- 将两个用量记录入口的请求级定价时刻传递给账号成本统计,使跨峰谷请求的成本与客户计费采用同一时间基准。
- 默认目录成本改用统一计费入口并隔离分组与渠道售价,使 DeepSeek 高峰成本正确应用两倍价格,同时保留服务层级、长上下文和单次推理等级倍率。
- 增加 Flash 与 Pro 的峰谷边界、缓存用量、规则优先级及真实网关回归测试,确保账号成本按最终上游模型计算且客户扣款金额与次数保持一致。

验证:Go 1.27.0 下全量 unit 与 integration 测试命令通过;golangci-lint 2.13.0 报告 0 个问题。定向回归无失败或跳过,全量套件保留原有条件跳过项。
2026-09-06 15:15:21 +08:00
suixincrazy 28167bbf85 fix(openai): honor model_routing for Codex manifests and scheduling
An OpenAI group that keeps a failover account for a shared public alias
produced a Codex model descriptor with no reasoning levels and text-only
input, which leaves the client's reasoning-effort slider with nothing to
select.

Two accounts explicitly mapping one alias to different upstream models
makes codexExplicitModelTargetsConflictForPlatform true. With no
upstream_model_metadata snapshot to intersect, groupCodexModelMetadata
short-circuits to {reasoningConflict, inputModalitiesConflict}, so
DefaultReasoningLevel is nil (the key vanishes under omitempty) and
SupportedReasoningLevels is an empty array. mergeMissingCodexModelFields
fills only missing-or-null fields, so an empty array is never repaired.

Failing closed is right when the alias target is genuinely ambiguous, but
a group that declares model_routing for the alias has already resolved
that ambiguity: the operator stated which accounts serve it and which is
primary. Treating that as a conflict rejects a deliberate failover setup.

Manifest side: when a routing rule names the alias, restrict capability
declaration to the routed accounts and skip the conflict override.
Capabilities then fall back to the same by-name derivation used for a
single account without a metadata snapshot. Groups with no rule, a
disabled rule, or a rule for another alias keep failing closed.

Scheduling side: model_routing was stored and read platform-agnostically
(admin_group.go, Group.GetRoutingAccountIDs) but four gates in
gateway_scheduling.go only consulted it for Anthropic, so an OpenAI
group's rules were accepted and displayed yet never honored. Without
this, the manifest would honor a rule that scheduling ignores. Both
gates now share one predicate, with the allowed platform set defined in
a single place.

Selection inside a routing list is unchanged: the list is a membership
set, and the winner is still decided by Priority, then load, then
LastUsedAt.

Tests: alias-failover manifest recovery plus the three negative cases,
the platform predicate table, and an OpenAI routed selection that must
beat a higher-priority unrouted account.
2026-09-06 10:12:20 +08:00
Chenning-Tao 985428d9d2 fix(openai): preserve named standalone Responses inputs 2026-09-06 09:33:53 +08:00
wucm667 4a4fd35e0c fix(simple-mode): guard non-empty group deletion 2026-09-06 08:11:23 +08:00
wucm667 f46038820b fix(simple-mode): enforce group capability allowlist 2026-09-06 08:11:23 +08:00
wucm667 05ad6b49a1 fix(simple-mode): enforce basic group capabilities 2026-09-06 08:10:41 +08:00
wucm667 a3675552bd feat(simple-mode): allow basic account grouping 2026-09-06 08:10:41 +08:00
Steven Moder b3ad9b67cf fix(i18n): fail builds on missing locale keys 2026-09-06 04:16:06 +08:00
cyh f49a393566 fix(openai): retain default models in mixed account catalogs 2026-09-06 02:02:49 +08:00
DAlaoli 8a46947865 Fix stale official Astra input modalities 2026-09-06 00:50:01 +08:00
LynricsyandWine Fox 7ae0312091 fix(claude): 🐛 补齐 thinking block binding beta
Co-authored-by: Wine Fox <fox@ling.plus>
2026-09-06 00:34:05 +08:00
WUJI-Labs 58e35a4f32 fix(antigravity): always set toolConfig for reasoning models without tools
Upstream returns 400 INVALID_ARGUMENT when toolConfig is absent. The guard
`if !isReasoning || len(tools) > 0` skipped it for reasoning models called
without tools, contradicting the intent stated in the comment above it.
Set toolConfig unconditionally and cover the three cases with a unit test.
2026-09-06 00:25:21 +08:00
TT 95023e7d49 fix(backup): serialize pg_dump/psql with the migrations advisory lock 2026-09-06 00:11:56 +08:00
TT 7a70de401c fix(payment): isolate fulfillment from redeem limits
Payment and admin fulfillment shared the public Redeem entry point, so
fulfillment retries were blocked by, and contributed to, the user's
public redeem failure counter, and lookup errors were silently treated as
"code does not exist" on a partial re-run.

- resolveRedeemAction now distinguishes a genuine not-found lookup from
  other lookup errors (which now fail the fulfillment instead of being
  swallowed and re-created).
- Add validatePaymentRedeemCode: before reusing an existing code during
  fulfillment, verify it belongs to the order (code id, balance type,
  amount, user, status), so a stale or foreign code can never be
  credited against the wrong order/user.
- Split Redeem into a policy-based core: public redemption enforces the
  failure counter; payment fulfillment (redeemForPaymentFulfillment) and
  admin fulfillment (RedeemForAdminFulfillment) bypass it, while all code
  validation and transactional updates remain identical. Admin
  fulfillment keeps the normal redeem-level affiliate rebate.
- The admin CreateAndRedeem endpoint now uses RedeemForAdminFulfillment.

Tests: updated resolveRedeemAction cases, new TestValidatePaymentRedeemCode,
code-used-by-another-user rejection, payment/admin rate-limit bypass
coverage, and public-path regression coverage.
2026-09-05 23:56:59 +08:00
TT 666a5c6f87 fix(redeem): use a fixed 10-minute failure window
The redeem failure counter used INCR followed by Expire(24h) on every
attempt, so each failed attempt refreshed the key TTL and the window
never actually closed for a user who kept retrying within 24h.

- Count attempts in a fixed 10-minute window via a small Lua script
  (INCR, then PEXPIRE only when the key is created or has no TTL), which
  is atomic and also repairs keys that lost their TTL.
- Move the counter to the redeem:ratelimit:v2: key space so pre-existing
  24h counters do not seed the new window.
- Rename the service threshold to redeemMaxFailedAttempts (30) to reflect
  that it bounds attempts per window, not per hour.

Public redeem behavior is unchanged apart from the window semantics:
the check fails open when the counter read fails, and a failed
increment never masks the underlying domain error.
2026-09-05 23:38:50 +08:00
TT c7343d2aa2 fix(monitors): send explicit user role in Gemini monitor probes
The Antigravity native Gemini route (generateContent) expects each
contents entry to carry an explicit "user" role, matching the request
shape used by the established Antigravity account-test flow. Without it
the probe body is rejected with 400 INVALID_ARGUMENT, so Gemini channel
monitors report false failures.

Add the explicit role to the Gemini provider adapter body and cover it
with a regression test that parses the generated request body.
2026-09-05 23:30:45 +08:00
TT a6430bfe20 fix(antigravity): refresh default user agent version
The Antigravity service fingerprints clients by the version embedded in the User-Agent header. The built-in default (1.23.2) had drifted far behind current real client builds, so requests using the default UA started failing OAuth and probe flows.

Bump DefaultUserAgentVersion from 1.23.2 to 2.9.1 (current client build) and update the constant test accordingly. The default remains overridable via ANTIGRAVITY_USER_AGENT_VERSION and the admin setting.
2026-09-05 23:28:33 +08:00
TT 32bf3d0f33 fix(settings): describe Grok cross-client mapping default as enabled
The Grok cross-client model mapping is enabled by default: InitializeDefaultSettings writes "true", and the parser treats a missing/empty value as enabled ("Default true (missing/empty -> enabled)"). However the comment above the defaults and the admin settings UI hint (EN and ZH) both claimed "disabled by default", contradicting the actual behavior.

Align the comment and both UI hints with the real default. No behavior change.
2026-09-05 23:17:34 +08:00
Zero Clover cb31033972 feat(openai): reuse pinned accounts for standard model discovery 2026-09-05 19:58:47 +08:00
chihao.ou 3aeab296d8 fix(onboarding): remove button text shadows 2026-09-05 19:55:03 +08:00
Zero Clover f3bbb95310 fix(groups): react immediately to Codex manifest edits and unify dialog toggles 2026-09-05 19:46:25 +08:00
chihao.ou 00eabe8ab9 feat(accounts): add month and year expiry presets 2026-09-05 19:20:10 +08:00
chihao.ou a16070ccf6 fix(auth): show a loading placeholder for Turnstile 2026-09-05 18:25:51 +08:00
github-actions[bot] ab99d56e96 chore: sync VERSION to 0.2.1 [skip ci] 2026-09-05 09:45:17 +00:00
Robert Guo b1ce821c49 fix(frontend): preserve Codex manifest edit state 2026-09-05 17:18:01 +08:00
Wesley Liddick 578785ee7f Merge pull request #6640 from cyhhao/fix/claude-billing-fingerprint
fix(claude): align billing fingerprint with outbound User-Agent
2026-09-05 16:59:33 +08:00
Wesley Liddick ed7c8f2208 Merge pull request #6572 from lyy0709/codex/gpt-6-astra
feat(openai): complete GPT-6 Astra support
2026-09-05 16:50:40 +08:00
cyh 2e1c7c00a6 test(claude): check request body close errors 2026-09-05 16:43:54 +08:00
cyh 994ca26e92 fix(claude): align billing fingerprint with outbound user agent 2026-09-05 16:34:00 +08:00
Wesley Liddick d2f87c612e Merge pull request #6638 from feeeei/feat/images-url-to-b64json-backfill
feat(images): 账户开关将生图结果 url 下载回填为 b64_json
2026-09-05 16:24:40 +08:00
lyy0709 62cd63bfda fix(openai): retain capabilities when model listing is unavailable 2026-09-05 16:24:15 +08:00
lyy0709 dee35a841d merge: reconcile Astra fixes with upstream capability sync 2026-09-05 16:19:57 +08:00
Wesley Liddick fb44e2dd09 Merge pull request #6514 from StarryKira/codex/anthropic-reasoning-pricing
feat: add Anthropic reasoning effort and Fable 5.1 max pricing
2026-09-05 16:19:18 +08:00
Wesley Liddick c42d78e26c Merge pull request #6571 from DuskLin/feat/codex-ultrafast-tier
feat(codex): support ultrafast service tier
2026-09-05 16:18:16 +08:00
Wesley Liddick 2dc287f2e0 Merge pull request #6636 from specialpointcentral/cyber-ws-recording-fix
fix(openai): record WS cyber policy failures across paths
2026-09-05 16:17:33 +08:00
Wesley Liddick 83094abf21 Merge pull request #6536 from wucm667/fix/issue-6526-mapped-model-scheduling
fix(openai): schedule accounts with mapped model
2026-09-05 16:17:04 +08:00
Wesley Liddick aafe93b335 Merge pull request #6557 from Ritel-T/pr/compact-admin-account-list
perf(admin): return compact account lists for lite requests
2026-09-05 16:16:26 +08:00
Wesley Liddick cc52c93ad7 Merge pull request #6539 from creamtea47/codex/fix-delegation-bootstrap-resume
fix(openai): 兼容历史任务委派续聊
2026-09-05 16:16:00 +08:00
lyy0709 d7f048a265 fix(openai): preserve synced Astra capabilities and continuation 2026-09-05 16:13:28 +08:00
Zero Clover 55c5eed9fc test(repository): persist model allowlists in group fixtures 2026-09-05 16:11:46 +08:00
Wesley Liddick 0d9e7e1530 Merge pull request #6628 from baryon/contrib/openai-gpt6-astra-capability-sync
fix(openai): persist GPT-6 Astra Codex capabilities from upstream sync
2026-09-05 16:07:50 +08:00
Wesley Liddick 9b4fcfb897 Merge pull request #6606 from luckydududu/feat/claude-cli-version-env-override
feat(claude): allow operators to override the advertised CLI version
2026-09-05 16:05:33 +08:00
Zero Clover cff3f89850 feat(groups)!: enforce model allowlists across gateway endpoints 2026-09-05 16:05:17 +08:00
Wesley Liddick 9517f12cdd Merge pull request #6510 from wall-wxk/fix/release-session-slot-on-forward-failure
fix(gateway): 转发失败的请求立即释放会话槽,不再占用整个空闲超时窗口
2026-09-05 16:05:06 +08:00
Wesley Liddick 40f4d450be Merge pull request #6507 from wucm667/fix/issue-6501-glm53-thinking-effort
fix(gateway): normalize GLM-5.3 thinking effort
2026-09-05 16:04:16 +08:00
Wesley Liddick 620eb3fd00 Merge pull request #6581 from r266-tech/r266/forward-opencode-session
fix(openai): scope OpenCode session forwarding
2026-09-05 16:03:57 +08:00
Wesley Liddick c6ff69fd12 Merge pull request #6542 from wucm667/fix/issue-6530-gateway-concurrency-origin
fix(handler): distinguish gateway admission rejections
2026-09-05 16:03:17 +08:00
Wesley Liddick 7b271bbee9 Merge pull request #6529 from wucm667/fix/issue-6521-preserve-passthrough-reasoning
fix(openai): preserve reasoning effort in API-key passthrough
2026-09-05 16:02:56 +08:00
Wesley Liddick 68f099707e Merge pull request #6553 from wucm667/fix/issue-6547-heartbeat-bootstrap
fix(openai): normalize automation heartbeat bootstrap
2026-09-05 16:02:36 +08:00
Varrge 3bdb869fca fix(billing): resolve Gemini 3.7/3.8 Flash thinking-tier prices 2026-09-05 15:54:38 +08:00
feeeei c227863d51 fix(images): 生图 url 回填下载拒绝私网目的地并只信字节嗅探
账户开关 images_url_to_b64_json 下载的图片 url 来自上游响应,是不可信输入。此前沿用
base_url 的 security.url_allowlist 策略,默认配置下只做格式校验,上游返回
localhost:8080 之类地址时网关会真的去请求内网。

- 下载 URL 主机为 localhost、回环、私网、链路本地、未指定地址的字面量时直接拒绝,
  不受 security.url_allowlist 配置影响(urlvalidator 导出 IsBlockedHost)
- 新增 ctx 标记 WithHTTPUpstreamPublicHostsOnly:共享上游客户端对带标记的请求无视配置
  校验解析后 IP,并为其克隆客户端挂 redirectChecker,重定向保持跟随但每一跳都校验
- 下载内容改为只信 http.DetectContentType 嗅探结果且限 png/jpeg/webp/gif,
  响应头不再作为依据
- 下载失败仍只记日志并保留原样;超时与端口策略不变
2026-09-05 15:50:56 +08:00
lyy0709 3a8c9f16de Merge remote-tracking branch 'upstream/main' into codex/gpt-6-astra 2026-09-05 15:48:49 +08:00
Qi HU 2da31290a1 fix(openai): record WS cyber policy failures across paths
Cyber-policy detection only inspected response.failed, and the ingress check ran after error-specific early returns. As a result, error-shaped failures and the connection-pool, HTTP-bridge, and WSv2 paths could miss cyber-policy hits.

Centralize event marking and invoke it before retry and failover returns across the WebSocket implementations. Preserve the recorded guard during account failover and defer connection blocking until the logical failover chain completes, avoiding duplicate records while ensuring subsequent turns are blocked.
2026-09-05 15:32:29 +08:00
feeeei 3c53ba01a7 feat(images): 账户开关将生图结果 url 下载回填为 b64_json
部分第三方 GPT-image 上游把 Images 响应里的 b64_json 换成 url,按官方接口实现的
客户端读不到图。账户 extra.images_url_to_b64_json 开启后,OpenAI API Key 的
Images 非流式响应里 b64_json 缺失或为空且 url 非空的项,由网关下载 url 内容并以
base64 回填 b64_json,url 与其余字段原样保留;异步生图任务复用同一路径。

- 下载经账户代理发起,沿用 base_url 的出站 URL 校验策略(validateUpstreamBaseURL
  抽出 validateOutboundURL,nil cfg 按仅格式校验处理),单图上限 20MB 且须为图片
  内容(响应头或字节嗅探);data: URL 直接取 base64 载荷
- 单项下载失败仅记日志并保留原样;客户端显式 response_format=url 时不回填;
  流式响应不处理
- 创建/编辑账户弹窗在 OpenAI API Key 的 Responses 模式块后新增“生图结果 URL 转
  base64”开关,中英文案;新增 openai_images_b64_backfill_test.go
- 只被 unit 标签测试引用的定价目录测试辅助补上 //go:build unit(admin 包整文件加标签,
  service 包的 newPricingServiceFromModelPricing 搬到 pricing_stub_helpers_unit_test.go),
  消除 golangci-lint 在默认标签下的 unused 报错
2026-09-05 15:17:57 +08:00
Wesley Liddick 07bf8b92fd Merge pull request #6550 from weiness/fix/alipay-payment-reconciliation
修复支付宝回调延迟或丢失,已支付订单可能长时间停留在待支付状态的问题
2026-09-05 15:08:56 +08:00
Wesley Liddick 63dc24b5e8 Merge pull request #6626 from wucm667/feat/issue-6624-none-reasoning-mapping
feat(openai): allow none as reasoning mapping source
2026-09-05 15:06:32 +08:00
Wesley Liddick 8f1d6af3ed Merge pull request #6531 from wucm667/fix/issue-6524-continuation-unavailable-recovery
fix(openai): recover unavailable continuation responses
2026-09-05 15:06:02 +08:00
Wesley Liddick 85f431a072 Merge pull request #6620 from wucm667/fix/issue-6615-astra-messages-cache
fix(openai): enable Astra messages prompt caching
2026-09-05 15:04:13 +08:00
Wesley Liddick d7fb9b5d35 Merge pull request #6593 from wucm667/fix/issue-6584-chat-tool-discovery
fix(apicompat): preserve discovered tools in chat fallback
2026-09-05 15:03:52 +08:00
Wesley Liddick eba6ea5639 Merge pull request #6590 from wucm667/fix/issue-6582-search-tool-capability
fix(codex): advertise search capability for bridged routes
2026-09-05 15:03:31 +08:00
Wesley Liddick 2ad6e8fb75 Merge pull request #6599 from wucm667/fix/issue-6598-gemini-custom-models-list
fix(gemini): apply custom list to native models endpoint
2026-09-05 15:03:08 +08:00
Wesley Liddick 570bd084c2 Merge pull request #6594 from wucm667/fix/issue-6592-preserve-zero-system-metrics
fix(ops): preserve zero-valued system metrics
2026-09-05 15:02:40 +08:00
Wesley Liddick 15278fe2f9 Merge pull request #6580 from OG-Wang/fix/sidebar-collapse-when-active
fix(frontend): allow collapsing sidebar groups while a child route is active
2026-09-05 15:02:30 +08:00
Wesley Liddick c4e6dcfd80 Merge pull request #6602 from ZeroClover/feat/codex-manifest-pinned-accounts
feat(codex): pinned-accounts Codex model manifest for OpenAI groups
2026-09-05 14:51:17 +08:00
Wesley Liddick 701f844755 Merge pull request #6555 from feeeei/feat/usage-upstream-request-id
feat(usage): 用量记录落库直接上游的请求 ID,并支持按账户配置响应头名
2026-09-05 14:36:07 +08:00
Wesley Liddick 91cf660377 Merge pull request #6397 from feeeei/fix/openai-ws-replay-oom-6382
fix(openai-ws): eliminate O(T²) replay copying and invalid_encrypted_content retry storms
2026-09-05 14:32:11 +08:00
Wesley Liddick 78e1aaedb9 Merge pull request #6535 from feeeei/feat/pricing-custom-file-hot-reload
feat(pricing): fallback/override 定价文件按内容哈希热重载,改动无需重启生效
2026-09-05 14:31:09 +08:00
shaw 708b85a6a4 fix: complete upstream request id handling 2026-09-05 14:18:13 +08:00
shaw 1cab4d8c22 fix: make custom pricing reload coherent 2026-09-05 14:18:13 +08:00
Wesley Liddick d8b9a83f8c Merge pull request #6492 from feeeei/main
fix(gateway): 负载感知调度补齐渠道模型限制的逐账号检查
2026-09-05 14:06:21 +08:00
Long LiandCursor a016bd4ba5 style: gofmt upstream models capability sync tests
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 14:58:51 +09:00
Long LiandCursor 983a3db3af fix(openai): persist GPT-6 Astra Codex capabilities from upstream sync
Official OpenAI hosts and models.dev entries without an api field left
Astra falling back to reasoning effort none and empty Fast tiers. Persist
complete per-model metadata, advertise Astra reasoning/Fast locally, and
surface partial sync warnings when image models remain incomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 14:45:06 +09:00
wucm667 7969751f1f feat(openai): allow none as reasoning mapping source 2026-09-05 12:32:30 +08:00
David Stone 6271c517da fix(apicompat): preserve arguments from done events 2026-09-04 21:18:36 -06:00
wucm667 4aaf5cb73b fix(openai): enable Astra messages prompt caching 2026-09-05 10:58:05 +08:00
LuckyandClaude Opus 5 e9bad40b10 feat(claude): allow operators to override the advertised CLI version
Anthropic gates newer models on a minimum Claude Code client version. With
CLICurrentVersion pinned at 2.1.220, claude-fable-5-1 fails every request with
"Claude Code 2.1.220 does not support this model; version 2.1.251 or newer is
required", and the only fix is a release that bumps one constant.

Add SUB2API_CLAUDE_CLI_VERSION, mirroring what internal/pkg/xai already does
with XAI_GROK_CLI_VERSION. CLICurrentVersion remains the built-in pin; the new
CLIVersion() is what every consumer reads. With no env var set the resolved
value equals the pin, so existing deployments are unaffected.

The value is resolved once at package init rather than per call: the User-Agent
header and the request body's cc_version are written by different code paths for
the same request, and reading different values would make that request
contradict itself.

Validation is strict (three numeric segments, not below the pin) because
identity_service.go documents what a malformed version does once it reaches an
account's persistent fingerprint: sustained 429s without reset headers, 5s
fallback cooldown, pool collapse into a 503 storm, and no reset entry point.
Invalid values warn and fall back rather than being silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168uDMAj9anaJrnH8CeFze6
2026-09-04 18:01:34 +00:00
Zero Clover 1e28ef5942 feat(codex): pinned-accounts Codex model manifest for OpenAI groups 2026-09-05 00:10:24 +08:00
wucm667 28f673e4cd fix(gemini): honor custom native models list 2026-09-04 22:29:36 +08:00
wucm667 9ddf69698d fix(ops): preserve zero-valued system metrics 2026-09-04 18:51:24 +08:00
wucm667 51631d0324 fix(apicompat): preserve discovered tools in chat fallback 2026-09-04 18:33:23 +08:00
wucm667 c7538cd256 fix(codex): advertise search capability for bridged routes 2026-09-04 16:43:15 +08:00
r266-tech 6cf645f87c fix(openai): scope OpenCode session forwarding 2026-09-04 12:45:06 +08:00
Rick aa556c372f fix(frontend): allow collapsing sidebar groups while a child route is active
A collapsible sidebar group was force-expanded whenever the active route
was one of its children, so clicking the chevron did nothing: the toggle
only updated the internal set while isGroupActive() kept reporting the
group as expanded. Any click that produces no visible change reads as a
bug to users.

Track per-group expand/collapse overrides instead. A group without an
entry keeps the automatic behavior (expanded while active, so context is
not lost on load or navigation), but a chevron click records the user's
choice and that choice wins, letting an active group be collapsed and
re-expanded at will.
2026-09-04 12:42:20 +08:00
刘佳林 126ac24c8c feat(codex): support ultrafast service tier
Accept service_tier=ultrafast on the OpenAI/Codex gateway, advertise it
on GPT-5.6 Sol in the Codex model catalog, and include it in ChatGPT
OAuth routing hints so Codex clients can select Ultrafast.
2026-09-04 10:14:29 +08:00
lyy0709 3c8be00131 feat(openai): add GPT-6 Astra support 2026-09-04 10:04:44 +08:00
wucm667 f5ae32e53f fix(gateway): normalize GLM-5.3 thinking effort 2026-09-04 08:15:41 +08:00
Ritel-T a3a6a85b73 test(admin): assert nested credential redaction in lite lists 2026-09-03 23:32:34 +09:00
Ritel-T db15e0090a fix: keep account table on compact list responses 2026-09-03 21:20:09 +09:00
Ritel-T 994192ef4f fix: type admin account list responses 2026-09-03 21:20:09 +09:00
Ritel-T 3e60c3b86b fix: add compact admin account list DTO 2026-09-03 21:20:08 +09:00
feeeei de27905e88 feat(usage): 用量记录落库上游请求标识并支持按账户配置响应头名
- usage_logs 新增 upstream_request_id 列(迁移 232)与仅索引非空行的 CONCURRENTLY 部分索引(迁移 233,runner 登记失效索引清理)
- 账户 extra.upstream_request_id_header 指定要读取的上游响应头,只读该头;留空不记录;创建/更新/批量入口校验头名合法且不超过 64 字节
- ForwardResult/OpenAIForwardResult 携带 UpstreamHeaders,中央计费按账户解析落库,WS 轮次保持 NULL
- 管理端用量明细新增默认隐藏的"上游ID"列,带与请求ID列一致的复制按钮,并同步导出
- 创建/编辑账户弹窗新增"上游ID"输入(UpstreamRequestIdHeaderField),提示按接入类型与平台分条列出常见头名
- HelpTooltip 悬停模式下指针移入提示框时保持打开,便于选中复制提示内容
2026-09-03 18:02:15 +08:00
wucm667 28dde982cb fix(openai): normalize automation heartbeat bootstrap 2026-09-03 17:29:04 +08:00
weiness de8d756afa fix(payment): reconcile pending Alipay orders 2026-09-03 16:33:30 +08:00
shaw b1748c4ea9 chore: update sponsors 2026-09-03 15:40:51 +08:00
Wesley Liddick 432ebc498c Merge pull request #6179 from wwcchh0123/codex/upstream-error-proxy-attribution
fix(ops): preserve event-time proxy attribution for upstream errors
2026-09-03 15:34:59 +08:00
Wei-Shaw abc07bb079 test(ops): satisfy errcheck and gofmt in proxy attribution tests 2026-09-03 15:23:22 +08:00
wucm667 78fc7b171e test: skip live token comparison on network errors 2026-09-03 15:16:18 +08:00
Wei-Shaw 4c1f920d58 fix(ops): close proxy attribution gaps and bound queued upstream events
Follow-up to the event-time proxy snapshot: every upstream attempt now
carries attribution, the direct/unknown contract is enforced everywhere,
and the queued upstream_errors array has hard bounds again.

Attribution coverage
- compact fallback retry helper and the streaming compact failover event
  now stamp proxy_id/proxy_name; the non-streaming SSE compact retry
  previously dropped its first attempt on `continue` and now records it
- raw chat-completions stream truncation events (new on main) attributed
- Anthropic/Bedrock transport errors get attribution inside the shared
  handleUpstreamTransportError helper, so all five callers inherit it
- OpenAI first-output timeout takes attribution from the caller: HTTP
  callers pass the HTTP rule, the WS passthrough relay passes the WS rule
  (no managed proxy => unknown, never direct, because http.DefaultClient
  may honor environment proxies)

Contract hardening
- single opsUpstreamProxyAttribution returns (id, name) from one decision;
  a hydrated Proxy without a durable ID is unknown, not id=null+name
- both normalizers (struct at append time, JSON on detail reads) enforce
  proxy_id==null <=> sentinel name; blank managed name => "proxy"
- detail-read normalization edits only the attribution keys via sjson, so
  keys from older struct versions and stored key order are preserved

Queue bounds
- keep at most 256 attempts and ~512 KiB serialized per entry, newest
  first; the oldest retained event records dropped_earlier_attempts
- attempts outside the 16-event body window trim url/message to 512 bytes
- restore the empty-event drop rule, judged before the body window clears
  detail so older real attempts survive

Tests cover each site above (managed proxy, direct, WS unknown), the
normalizer invariants, key/order preservation, and the count/byte bounds.
2026-09-03 15:11:36 +08:00
wwcchh0123 e9e3c46cb7 fix(ops): snapshot proxy on upstream errors 2026-09-03 14:20:39 +08:00
wucm667 62198286e9 [verified] fix(handler): distinguish gateway admission rejections 2026-09-03 12:48:02 +08:00
NellPoi af90a9bd1f fix(openai): 兼容历史任务委派续聊 2026-09-03 11:39:23 +08:00
wucm667 5e4958c889 fix(openai): schedule accounts with mapped model 2026-09-03 10:51:12 +08:00
feeeei fdc2ec17e6 feat(pricing): fallback/override 定价文件按内容哈希随定时器热重载
PricingService 的定时器(hash_check_interval_minutes,默认 10 分钟)每轮先做
远程目录哈希同步,再比对 fallback_file/override_file 两个文件内容的联合 sha256
(customFilesHash),变化时从本地 model_pricing.json 重建叠加层,只替换内存数据
与指纹,不改远程同步锚点 localHash。只配置了自定义文件而 remote_url 为空时
调度器同样启动。

- 三条重建路径(启动加载、远程下载、热重载)统一走 buildPricingData,指纹在合并
  读取之前采样,并发改文件只会落后于数据、下一轮补重建
- 删除文件视为该层清空,下一轮生效;文件存在但不可读或不是 JSON 对象时保留旧数据、
  不更新指纹,每轮重试并告警
- config.example.yaml 同步生效说明;新增 pricing_service_hot_reload_test.go
2026-09-03 10:35:20 +08:00
wucm667 b6740c937f fix(openai): recover unavailable continuation responses 2026-09-03 10:26:12 +08:00
wucm667 10c8f523c4 fix(openai): preserve passthrough reasoning effort 2026-09-03 09:42:50 +08:00
haruka 8249ab37db feat: add Anthropic reasoning effort pricing controls 2026-09-02 20:16:57 +08:00
anguobao123 1859519573 feat(deploy): support optional Apple network subnet 2026-09-02 20:08:57 +08:00
wall-wxkandClaude Opus 4.6 9be9c0b68c fix(gateway): 转发失败的请求立即释放会话槽,不再占用整个空闲超时窗口
会话数量限制(max_sessions)此前只有空闲超时自动过期,没有任何主动释放
机制:请求在选号阶段注册会话后,即使转发失败(上游错误/客户端断开/换号
耗尽),该 session hash 也会占住槽位直到空闲超时(默认 5 分钟),导致
max_sessions 受限的账号在窗口内拒绝所有新会话。

修复:
- SessionLimitCache 新增 UnregisterSession(Redis ZREM),立即移除会话注册
- GatewayService 新增 ReleaseAccountSession,适用条件与 checkAndRegisterSession 对齐
- Messages 主路径:记录 failover 链上注册过会话槽的账号,请求最终失败时
  统一释放;failover continue / profit veto / 兜底分组切换时按次即时释放;
  成功及"上游已计量部分结果"(#5148)保持既有空闲超时语义
- CountTokens:转发失败时同样释放选号时注册的会话槽

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-02 18:45:20 +08:00
aofee a7f1ed8fa4 fix(openai-ws): track pending passthrough turns 2026-09-02 14:59:13 +08:00
WUJI-Labs f5e4da5e72 style(upstream): gofmt http_upstream_profile.go 2026-09-02 12:40:23 +08:00
WUJI-Labs 9dc4c40bff test(upstream): align HTTP/2 keepalive tests with long-stream profile API 2026-09-02 12:28:37 +08:00
WUJI-Labs 81fd853005 feat(upstream): add HTTP/2 PING keepalive for long LLM generation streams
Long-running LLM inferences (such as high-thinking Gemini models) may experience
silent TCP disconnections across intermediate proxies, NAT gateways, or firewalls
when no data frames are emitted for tens of seconds to minutes.

Go's default http2.Transport sets ReadIdleTimeout=0 (disabling active PING checks).
This commit introduces HTTPUpstreamProfileLongStream with:
- ReadIdleTimeout = 10s
- PingTimeout = 5s

When a pooled H2 connection stays idle for 10s during long streams, active PING frames
are dispatched. Dead connections failing to respond within 5s are proactively evicted,
preventing client generation requests from hanging on dead sockets or failing with EOF/502.

Signed-off-by: WUJI-Labs <zoujincool@gmail.com>
2026-09-02 12:11:17 +08:00
aofee 01bd9b71a2 feat: add Grok media eligibility controls 2026-09-02 11:50:33 +08:00
feeeei 9ad3865690 fix(gateway): 负载感知调度补齐渠道模型限制的逐账号检查
渠道 billing_model_source=upstream 时,限制模型只能按账号映射后的上游模型
逐账号判定。GatewayService.SelectAccountWithLoadAwareness 的负载感知各层
(模型路由候选、路由内粘性 gate、无路由粘性 gate、负载感知候选及其兜底)
此前都没有这一判定,load_batch_enabled 默认开启,anthropic/gemini/antigravity
分组的渠道白名单实际不生效。

现在入口算一次 needsUpstreamChannelRestrictionCheck,各层共用同一判定;
候选因渠道限制筛空时记 warn 并返回带 channel pricing restriction 的
ErrNoAvailableAccounts,与预检路径一致。
2026-09-02 11:46:37 +08:00
github-actions[bot] 5097b31457 chore: sync VERSION to 0.2.0 [skip ci] 2026-09-02 03:24:57 +00:00
Wesley Liddick aa23648835 Merge pull request #6480 from Wei-Shaw/fix/group-model-pricing-ui
fix(frontend): 优化分组模型定价弹窗布局
2026-09-02 11:09:55 +08:00
aofee a774230661 fix(grok): keep inconclusive billing accounts eligible 2026-09-02 11:09:21 +08:00
aofee 222181efd6 fix(anthropic): scope Fable credits errors to model 2026-09-02 11:02:38 +08:00
shaw 1a33dc8cc4 fix(frontend): 优化分组模型定价弹窗布局 2026-09-02 10:40:21 +08:00
Wesley Liddick 3510aa22b5 Merge pull request #6425 from DuskLin/feat/openai-reasoning-effort-model-scope
feat(group): OpenAI 推理强度映射支持按模型名/前缀/后缀匹配
2026-09-02 10:18:07 +08:00
刘佳林 77729e2723 merge: resolve conflicts with upstream/main
Keep model-scoped reasoning effort mappings together with the
deny-or-downgrade ceiling control that landed on main.
2026-09-02 10:01:42 +08:00
shaw 05ea883e26 fix(ent): correct group field indexes after merge 2026-09-02 10:01:33 +08:00
Wesley Liddick 6566039bc8 Merge pull request #6470 from aofee/codex/fix-ws-passthrough-missing-terminal
fix(openai-ws): reject passthrough close before terminal event
2026-09-02 09:42:13 +08:00
Wesley Liddick e50bffb7e3 Merge pull request #6469 from wucm667/fix/issue-6465-apikey-chat-cache-identity
fix(openai): preserve API key chat cache identity
2026-09-02 09:25:02 +08:00
Wesley Liddick f2804eb2c8 Merge pull request #6444 from Ritel-T/pr/group-free-openai-fast
Add group-level Standard billing for OpenAI Fast requests
2026-09-02 09:23:49 +08:00
Wesley Liddick 559960865a Merge pull request #6447 from DuskLin/feat/openai-reasoning-effort-over-limit
feat(groups): 推理强度上限增加超限自动降档与拒绝访问
2026-09-02 09:07:53 +08:00
Wesley Liddick a4fb58e428 Merge pull request #6443 from Ritel-T/pr/group-openai-fast
Add group-level OpenAI Fast enforcement
2026-09-02 09:07:05 +08:00
Wesley Liddick a668aa8b36 Merge pull request #6460 from HuntercodeT/fix/model-not-found-not-overridden-by-rate-limit
fix(gateway): stop a per-model cooldown from turning 404 model_not_found into 429
2026-09-02 09:06:17 +08:00
Wesley Liddick 8d0b5ede28 Merge pull request #6458 from HuntercodeT/fix/scheduler-projection-keeps-openai-passthrough
fix(scheduler): keep OpenAI passthrough flag in the scheduler snapshot projection
2026-09-02 09:05:51 +08:00
Wesley Liddick 2786ae9de6 Merge pull request #6474 from StarryKira/codex/fix-6472-fable-5-1
feat: support Claude Fable 5.1 and split cache-write pricing
2026-09-02 09:05:29 +08:00
haruka 34b8bf1a6e feat: support Claude Fable 5.1 2026-09-02 05:47:01 +08:00
Ritel-T 52c7d88340 chore: merge upstream v0.1.185 2026-09-02 03:29:52 +09:00
Ritel-T 9e2d97f250 chore: merge upstream v0.1.185 2026-09-02 03:28:24 +09:00
aofee bfe0a5a873 fix(openai-ws): reject close before terminal event 2026-09-01 20:44:11 +08:00
wucm667 504919a054 fix(openai): preserve API key chat cache identity 2026-09-01 19:10:37 +08:00
Wesley Liddick 0d27f45ead Merge pull request #6454 from alfadb/fix/anthropic-fallbacks-sanitize
fix(gateway): 缺 server-side-fallback beta 时剥离 Anthropic fallbacks 字段
2026-09-01 17:47:34 +08:00
Wesley Liddick 65380be9c8 Merge pull request #6463 from DuskLin/feat/kimi-native-responses
feat(kimi): 原生转发 OpenAI Responses API
2026-09-01 17:46:36 +08:00
Wesley Liddick d596d0844f Merge pull request #6450 from WizardPro/codex/fix-scheduled-automation-bootstrap
fix(openai): accept scheduled automation bootstrap without call id
2026-09-01 17:30:04 +08:00
shaw cd04848b97 chore: update sponsors 2026-09-01 17:19:36 +08:00
刘佳林 e377c4358c feat(kimi): forward native OpenAI Responses API
Moonshot pay-as-you-go and Kimi For Coding both expose native
/v1/responses. Stop converting Kimi Responses traffic to Chat
Completions, and treat Kimi like DeepSeek for protocol selection while
keeping /v1/responses URL construction.
2026-09-01 16:50:04 +08:00
HuntercodeT 3438580213 fix(gateway): stop a per-model cooldown from turning 404 model_not_found into 429
classifySelectionFailureError upgrades a selection failure to 429
rate_limit_error whenever the scheduler's compact reason string matches
model_rate_limited=(\d+) with a positive count. It does so unconditionally,
including when the fallback it was handed is the authoritative 404
model_not_found classification.

Selection failure reasons are additive, so both can appear at once, e.g.
"pool=9, filtered: model_not_supported=8 model_rate_limited=1": eight accounts
cannot serve the model at all and the ninth happens to sit in a per-model
cooldown. classifyNoAccountError only produces ModelNotFound through
DiagnoseModelAvailabilityForPlatform, a dedicated database query over
persistent eligibility (active + schedulable + model_mapping), so it has
already established that no account in the group can serve the model. One
transient cooldown does not make "All available accounts are currently
rate-limited" true.

The mislabel is not cosmetic:

- Retrying can never succeed, yet 429 explicitly invites a retry.
- Clients that treat 429 as a rate limit retry hard and swallow the body;
  Codex surfaces only "exceeded retry limit, last status: 429", discarding the
  one message that names the real problem. Operators then look for capacity
  and rate limits instead of a model whitelist.
- Ops attribution becomes self-contradictory: classifyNoAccountErrorFromGin
  has already marked the request business-limited for local model
  configuration, while call sites gate markOpsRoutingCapacityLimitedIfNoAvailable
  on ModelNotFound, which the override clears, so the same request is also
  counted as routing capacity limited.

Return the fallback untouched when it is ModelNotFound. The 429 upgrade is
preserved for every other fallback, which is the case it was written for:
accounts that can serve the model exist but are all cooling down, where the
fallback is 503 and retrying is meaningful.

Claude-Session: https://claude.ai/code/session_01LwvyJVV2sdqiu41Z9JDNUq
2026-09-01 15:18:30 +08:00
HuntercodeT e93e6368fc fix(scheduler): keep OpenAI passthrough flag in the scheduler snapshot projection
filterSchedulerExtra is an allowlist and never carried openai_passthrough /
openai_oauth_passthrough, while credentials.model_mapping is preserved in full.
Candidate filtering (ListSchedulableAccounts) reads that trimmed projection
(sched:meta:<id>), so there a passthrough account looks exactly like a
whitelist-mode one: Account.IsModelSupported falls back to the model_mapping
whitelist and the account is dropped as model_not_supported. Forwarding, which
hydrates the full account from sched:acc:<id>, still honors passthrough - hence
"testing the account directly succeeds, but the gateway reports no available
accounts supporting model X".

The practical impact is that a stale leftover whitelist silently shrinks the
candidate pool of accounts that were explicitly configured to let the upstream
decide. When no account in the group lists the requested model, the group is
fully dark for it even though every account can serve it.

This is not a regression of #4936: that fix corrected the predicate
(Account.IsModelSupported short-circuits passthrough before the mapping check),
this one restores the input the predicate is fed. The projection allowlist
predates #4936 by three and a half months and has never carried the flag, so
the fix simply never took effect on the snapshot path.

Add both keys to the allowlist, plus a regression test that round-trips the
projection through JSON (the real sched:meta write/read path) and asserts the
model gate still lets a non-whitelisted model through for a passthrough account.

Claude-Session: https://claude.ai/code/session_01LwvyJVV2sdqiu41Z9JDNUq
2026-09-01 15:13:11 +08:00
alfadb 200b1406d7 fix(gateway): strip Anthropic fallbacks unless server-side-fallback beta is present
Clients now send body.fallbacks; upstream rejects it without
anthropic-beta: server-side-fallback-2026-07-01. Mirror the
context_management sanitize so OAuth mimic / default API-key
beta paths no longer 400 with "fallbacks: Extra inputs are not permitted".
2026-09-01 14:23:33 +08:00
WizardPro 421a83282a fix(openai): accept scheduled automation bootstrap without call id 2026-09-01 11:36:26 +08:00
github-actions[bot] a2fb09260a chore: sync VERSION to 0.1.185 [skip ci] 2026-09-01 02:52:28 +00:00
刘佳林 aa7a811e66 feat(groups): add deny-or-downgrade control for reasoning effort ceiling
OpenAI/Composite groups can now reject requests that exceed the max
reasoning effort instead of always rewriting them down to the ceiling.
Existing groups keep the previous auto-downgrade behavior.
2026-09-01 10:15:45 +08:00
Wesley Liddick 2ac784c51a Merge pull request #6353 from feeeei/main
refactor(billing): 长上下文阶梯计价改为价格目录数据驱动
2026-09-01 09:32:06 +08:00
Wesley Liddick 6b39d0b453 Merge pull request #6379 from baryon/contrib/codex-priority-service-tier
fix(codex): advertise Fast service tier in generated catalogs
2026-09-01 09:02:24 +08:00
Wesley Liddick f218c8d40a Merge pull request #6394 from qiujun8023/fix/account-stats-deepseek-peak
fix(billing): apply model pricing policies to account stats cost
2026-09-01 08:58:49 +08:00
Wesley Liddick 1cce2b38ee Merge pull request #6417 from fengshao1227/fix/ws-ingress-capacity-shed-client-code
fix(openai): ctx_pool WS ingress 下发前改写上游容量降载错误码(Codex 会话被硬终止)
2026-09-01 08:54:00 +08:00
Wesley Liddick 9d9ed1cc62 Merge pull request #6415 from icebreaker-forked/fix/retry-transient-db-startup
fix(repository): PostgreSQL 恢复期间退避重试,避免首次启动退出
2026-09-01 08:53:25 +08:00
Wesley Liddick 2d7767eaf2 Merge pull request #6407 from wucm667/fix/issue-6402-delegation-bootstrap
fix(openai): accept delegation bootstrap without call id
2026-09-01 08:51:46 +08:00
Wesley Liddick 064d9b74e8 Merge pull request #6432 from okbexx/fix/codex-disabled-account-capabilities
fix(codex): ignore persistently disabled catalog accounts
2026-09-01 08:46:42 +08:00
Wesley Liddick e87c47c957 Merge pull request #6435 from wucm667/fix/issue-6420-apikey-responses-instructions
fix(openai): avoid synthesized instructions for API keys
2026-09-01 08:45:58 +08:00
Wesley Liddick bdedd6c544 Merge pull request #6433 from swjturay/codex/upstream-stale-ws-recycle
fix(openai-ws): recycle stale idle pool connections without reader liveness
2026-09-01 08:45:30 +08:00
wucm667 5fb5dfb34c fix(openai): reacquire passthrough turn slots 2026-09-01 08:13:29 +08:00
Ritel-T 9908d3ca2f Bill free Fast requests at Standard price 2026-09-01 04:34:24 +09:00
Ritel-T 4df6b0636b Add the free Fast group switch 2026-09-01 04:34:24 +09:00
Ritel-T 498e06c582 Localize the free Fast control 2026-09-01 04:34:24 +09:00
Ritel-T e6722126ba Add frontend free Fast types 2026-09-01 04:34:24 +09:00
Ritel-T e619ca386e Carry free Fast through auth snapshots 2026-09-01 04:34:24 +09:00
Ritel-T 8224434d21 Preserve free Fast when duplicating groups 2026-09-01 04:34:24 +09:00
Ritel-T fd55f3248f Manage free Fast through group lifecycle 2026-09-01 04:34:24 +09:00
Ritel-T 08d6c153b7 Return free Fast in admin responses 2026-09-01 04:34:24 +09:00
Ritel-T 636d6be690 Expose free Fast in admin requests 2026-09-01 04:34:24 +09:00
Ritel-T 7dcf728461 Project free Fast into API key auth 2026-09-01 04:34:24 +09:00
Ritel-T fb409787c6 Persist the free Fast group policy 2026-09-01 04:34:24 +09:00
Ritel-T e7caedc22a Regenerate Group persistence for free Fast 2026-09-01 04:34:24 +09:00
Ritel-T eebf4e3ffb Add the free Fast migration 2026-09-01 04:34:23 +09:00
Ritel-T c7cf84ad39 Preserve Group Fast in integration fixtures 2026-09-01 04:34:06 +09:00
Ritel-T 0681aa2566 Cover Group Fast in native Responses 2026-09-01 04:34:06 +09:00
Ritel-T 17747df842 Add the group Fast admin switch 2026-09-01 04:34:06 +09:00
Ritel-T 3fbce499d8 Add group Fast form helpers 2026-09-01 04:34:06 +09:00
Ritel-T 1a40e5690f Localize the group Fast control 2026-09-01 04:34:06 +09:00
Ritel-T 7903716ad1 Add frontend group Fast types 2026-09-01 04:34:06 +09:00
Ritel-T f8eae45048 Force OpenAI Fast across gateway paths 2026-09-01 04:34:06 +09:00
Ritel-T a44a1019f8 Carry group Fast through auth snapshots 2026-09-01 04:34:06 +09:00
Ritel-T 9fba8ed43b Return group Fast in admin responses 2026-09-01 04:34:06 +09:00
Ritel-T 7f5e915dd1 Expose group Fast in admin requests 2026-09-01 04:34:06 +09:00
Ritel-T 7e06816571 Preserve group Fast when duplicating groups 2026-09-01 04:34:06 +09:00
Ritel-T e9e66da5a8 Manage group Fast through group lifecycle 2026-09-01 04:34:06 +09:00
Ritel-T 94bb9354ce Accept group Fast in admin service inputs 2026-09-01 04:34:06 +09:00
Ritel-T 95982a508f Project group Fast into API key auth 2026-09-01 04:34:06 +09:00
Ritel-T 563979e91e Persist the group Fast setting 2026-09-01 04:34:06 +09:00
Ritel-T 1cb28fd429 Regenerate Group persistence for Fast mode 2026-09-01 04:34:06 +09:00
Ritel-T aa679efe41 Add the group Fast migration 2026-09-01 04:19:38 +09:00
feeeei e2cfaa46e7 fix(pricing): 目录 cache 侧字段契约哨兵与 Gemini pro 缓存写入价修正
阶梯计价数据驱动后,目录条目的字段缺失或版本拼接会直接变成计费偏差,
解析层补两道数据契约哨兵,并修正出厂快照中 Gemini pro 系的缓存写入价:

- orphanCacheTierFields / warnOrphanCacheTierFields:条目带 cache 侧
  above 档(含 _priority/_flex 变体与 above_1hr 形态)却没有基础价时打
  WARN——cache 侧 above 档不参与计费取值,按"基础价 × 输入倍率"计,基础价
  缺失即该分项按 0 计;变体缺自身基础价时沿 变体→1h→标准 回落链任一存在
  即不算孤儿,与计费回落一致
- isLopsidedLongContextLadder / warnLopsidedLongContextLadders:above 档
  折算出只有一侧带附加费的阶梯时打 WARN(基础价与 above 档来自不同价格
  版本的形态),显式 long_context_* 字段不告警
- deriveLongContextFromAboveTierFields 注释改为陈述契约:cache above 档
  须恰为基础价 × 输入倍率
- 出厂快照 6 个 Gemini pro 条目补 cache_creation_input_token_cost(= 标准
  输入价,Google 对缓存写入不另收费)、above_200k 改为输入 above 价、带
  priority 输入价的 4 条补 priority 孪生,与 model-price-repo 数据修正同值

测试:
- 解析层:孤儿字段 WARN(含 priority/1h 孤儿正例、1h 回落 5m 反例)、单侧
  阶梯 WARN、出厂快照契约(全条目无孤儿字段、无单侧阶梯、Gemini pro 缓存
  写入价 = 输入价)
- 用量契约:extractGeminiUsage 与 Antigravity 非流式/流式转换均不产生
  cache_creation token(Gemini usageMetadata 无该类别)
- 计费:gemini-2.5-pro 阶梯对 cache_read / cache_creation 分项生效,阈值
  计入缓存写入 token;阶梯表场景与夹具同步为现行目录值
2026-09-01 01:26:55 +08:00
feeeei 593fc93659 feat(pricing): 新增 override_file 价格目录覆盖补丁
新增 pricing.override_file 配置(默认关闭):稀疏补丁文件按 JSON 字段
浅合并覆盖目录/回退数据,作为最高优先级数据源,用于本地自定义官方
目录条目(如显式 long_context_input_token_threshold=0 关闭某模型阶梯)
而无需自建价格镜像:

- applyPricingOverrides 挂在 parsePricingData 解析入口,只修补已存在
  条目(补丁字段覆盖同名字段,值为 null 删除字段);目录、回退与灾备
  路径都经该入口,覆盖语义三处一致
- mergeOverrideOnlyModels 在回退合并后把目录/回退都没有的模型作为
  独立条目并入(复用主解析路径,须自带价格字段通过有效性过滤);
  不在主解析时抢先建条目,避免纯补丁挡住回退完整条目、其余分项价
  静默变 0
- 最终未生效的条目(模型名拼错、纯补丁落在不存在的模型上)打 WARN
  哨兵;override 文件缺失/损坏仅跳过合并,不影响目录加载
- 生效时机为重启或下次目录下载,与 fallback_file 一致;fallback_file
  只补缺失模型的语义不变
2026-09-01 01:26:55 +08:00
feeeei 530fb20f25 refactor(billing): 长上下文阶梯计价改为价格目录数据驱动
删除 GPT/Gemini 系列写死的阶梯规则,阶梯一律由目录数据驱动:

- 解析层折算 LiteLLM 的 *_above_XXXk_tokens 绝对价字段为阈值+倍率
  (阈值取自字段名,倍率 = above/基础价,cache 侧跟随输入倍率,
  xAI 按 provider 用达到即进高档语义);显式 long_context_* 字段
  按"字段存在"优先,显式 threshold=0 或 multiplier=1 可关闭阶梯
- 删除 OpenAI 272K 常量与 applyModelSpecificPricingPolicy 的长上下文
  强补分支、fallbackPrices/静态兜底条目的 LongContext 字段、Gemini 旧
  边际规则全链路(CalculateCostWithLongContext / RecordUsageWithLongContext /
  阶梯表 marginal 基准);保留 fastRatio 与 GPT-5.6 cache_write 1.25x
  防御分支、grok 静态兜底阶梯
- 渠道配置定价区间时完全以渠道区间为准,不再叠加目录阶梯
- 长上下文倍率 ≤0 在应用点按 1 计,防止显式字段残缺把分项乘 0 免费
- 目录重载时对比新旧数据,原有阶梯丢失打 WARN 哨兵告警

行为变化:Gemini 由边际 2x(仅 /v1beta、全模型)改为整单换档(数据
驱动、仅带 above_200k 的 pro 系),渠道平价之上叠加目录阶梯;Claude
sonnet-4/4.5 随目录数据获得 200K 整单阶梯(与官方定价一致);模型广场
官方参考价列与实付列同源展示目录阶梯。
2026-09-01 01:26:55 +08:00
Wesley Liddick 200602b41b Merge pull request #6438 from Brisbanehuang/fix/codex-custom-provider-image-input
fix(codex): preserve known GPT image input capabilities
2026-08-31 21:57:09 +08:00
Brisbanehuang e2624fb65c fix(codex): preserve known image input capabilities 2026-08-31 09:42:58 -04:00
shaw cc6a8e5174 chore: update sponsors 2026-08-31 21:29:13 +08:00
Ray a10ff1255c fix(openai-ws): classify canceled client streams 2026-08-31 21:04:29 +08:00
Ray 6d5f027848 fix(openai-ws): recycle stale idle pool connections 2026-08-31 21:04:29 +08:00
wucm667 e21b849a92 fix(openai): avoid synthesized instructions for API keys 2026-08-31 20:58:49 +08:00
Jarl ba345f1052 fix(codex): ignore persistently disabled catalog accounts 2026-08-31 20:26:29 +08:00
spin6lock c74a4f521c fix(billing): 补上 glm-5.3 / glm-5.3-flash 兜底价,避免被 glm-5 子串匹配抢走 2026-08-31 19:51:50 +08:00
刘佳林 7c01ec9be8 feat(groups): scope OpenAI reasoning effort mappings by model
Allow OpenAI/Composite group mappings to match by exact model name,
prefix, or suffix. Empty type and model still apply to every model.
The admin form groups multiple request-to-forward pairs under one
type+model scope.
2026-08-31 19:21:31 +08:00
cyh 163eb7ffae fix(ops): bound database-backed system logs 2026-08-31 19:04:58 +08:00
github-actions[bot] 52374af940 chore: sync VERSION to 0.1.184 [skip ci] 2026-08-31 09:28:15 +00:00
ice breaker b6a7b8b7df fix(repository): allow eight transient database retries 2026-08-31 17:10:31 +08:00
li 1dc0a09009 fix(openai): ctx_pool WS ingress 下发前改写上游容量降载错误码
上游容量降载(server_is_overloaded / slow_down)被 ctx_pool 的 ingress 直写路径
原样转发给客户端。Codex CLI 按闭集判定,这两个码属致命集,客户端打印
"Selected model is at capacity" 并直接终止会话,不退避重试。

仓库已有 sanitizeOpenAICapacityShedErrorCodeForClient 处理这件事,且注释写明了
Codex 的闭集判定与"账号状态判定使用改写前原始 payload"的前提。它此前只在
HTTP/SSE(openai_gateway_response_handling.go)与 http_bridge
(openai_ws_http_bridge.go)两条路径被调用,ctx_pool ingress 直写路径是唯一漏掉
的一条——同一账号同一时刻切到 http_bridge 就能正常退避重试。

改写结果写入独立的 clientMessage,不动 upstreamMessage:写出点之后的
markOpenAIWSClientVisibleFailure 与 handleOpenAIWSTerminalTransientFailure
仍按原始 payload 判定账号状态,与 http_bridge 的写法一致。

新增用例覆盖改写生效与改写范围两侧(非容量类错误码必须原样下发)。
2026-08-31 16:53:57 +08:00
Wesley Liddick e98ef32eb2 Merge pull request #6414 from fengshao1227/fix/anthropic-responses-stream-lifecycle
fix(apicompat): Anthropic→Responses 流式转换丢失助手文本(thinking 块顶掉未关闭的 message item + content_index 从不推进)
2026-08-31 16:43:40 +08:00
ice breaker 863667ce67 fix: retry transient database startup errors 2026-08-31 16:25:13 +08:00
li 8f5451587e fix(apicompat): Anthropic→Responses 流式转换在 thinking 块前关闭 message item 并推进 content_index
thinking 块开始时不关闭已打开的 message item,直接覆盖 CurrentItemType/
CurrentItemID,累积的助手文本既不进 state.Outputs 也拿不到 output_item.done,
response.completed 只带 reasoning——客户端收到「成功但无输出」。同 switch 的
tool_use 分支本来就先调 closeCurrentResponsesItem,只有 thinking 漏了。

content_index 只在 item 打开时置 0,从不推进。同一 message item 里第二个 text
块会再发一次 content_part.added(content_index=0),与第一个 part 撞在同一下标,
SDK 累积式 helper 按 content[content_index] 写入时后者覆盖前者。反方向
responses_to_anthropic.go 有三处 ContentBlockIndex++,正方向一处都没有。

新增用例覆盖两条坏顺序,外加一条生命周期不变式用例(output_item.added 与
output_item.done 必须配平、terminal 事件按announce 顺序逐项携带)。
2026-08-31 16:15:08 +08:00
Wesley Liddick 85b593fd2f Merge pull request #6411 from Wei-Shaw/fix/openai-ttft-setting-ui
fix(openai): move TTFT metric mode to admin settings
2026-08-31 15:39:12 +08:00
shaw 0fcec63b68 test(openai): isolate visible TTFT fixture 2026-08-31 15:15:44 +08:00
shaw 8553c91c1a test(api): update settings contract fixture 2026-08-31 15:05:39 +08:00
shaw c66e700f05 fix(openai): move TTFT metric mode to admin settings 2026-08-31 14:52:07 +08:00
Wesley Liddick fdf9751c12 Merge pull request #6378 from Ritel-T/fix/codex-oauth-service-tier-billing
fix(openai): preserve Codex OAuth Fast billing tier
2026-08-31 13:56:00 +08:00
wucm667 1be69e56aa fix(openai): accept delegation bootstrap without call id 2026-08-31 12:57:06 +08:00
qiujunandClaude Opus 5 e7c029875c test(billing): 图片输出按 output 子集计价
上一条改用统一定价管线后,TestTryModelFilePricing_WithImageOutput 挂在 0.3 vs
0.28:原算式把 ImageOutputTokens 当 OutputTokens 之外的额外量叠加,等于按文本
output 价和图片价各收一次,测试固化了这个重复计费。

ImageOutputTokens 本就是 OutputTokens 的子集(Gemini 取自 candidatesTokensDetails
的 IMAGE 档,OpenAI 取自 output_tokens_details),统一管线先扣除再按图片单价计,
未配图片价时回退到 output 价、总额不变,正是这个语义。期望改为 0.28。

影响:涉及图片输出的模型,账号统计成本会降低"图片 token × 文本 output 单价"。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:33:06 +08:00
Wesley Liddick 0678b24d58 Merge pull request #6283 from okbexx/feat/usage-compaction-kind
feat(usage): track native compaction requests
2026-08-31 11:19:37 +08:00
Wesley Liddick b369fbca15 Merge pull request #6395 from lylelllll/fix/account-expiry-timezone
fix(frontend): clarify local timezone for expiry inputs
2026-08-31 11:18:55 +08:00
Wesley Liddick 36ee193e3c Merge pull request #6367 from qiujun8023/fix/usage-stats-cards-mobile-horizontal-scroll
fix(frontend): prevent hidden cache tooltip from causing horizontal scroll on mobile
2026-08-31 11:18:03 +08:00
Wesley Liddick 1dd0f2e5dc Merge pull request #6384 from alfadb/fix/unify-usage-window-style
fix(frontend): unify usage window rendering and tighten alert thresholds
2026-08-31 11:17:09 +08:00
Wesley Liddick f34735f462 Merge pull request #6388 from alfadb/feature/ollama-usage-cn-platforms
feat(ollama): 用量窗口支持挂在国产三家平台下的 Ollama Cloud 账号
2026-08-31 11:16:25 +08:00
Wesley Liddick a2d7d41185 Merge pull request #6325 from 251392463/fix/grok-responses-vision-tool-output
fix(grok): preserve Responses vision tool output images
2026-08-31 11:15:33 +08:00
Wesley Liddick 03ab687686 Merge pull request #5866 from wucm667/fix/issue-5863-scope-monitor-groups
fix: scope channel monitor groups by user access
2026-08-31 11:15:13 +08:00
Wesley Liddick 2e756b71f2 Merge pull request #6198 from xuhaihan/fix/openai-reauth-refresh-token
fix(accounts): reauthorize OpenAI with refresh token
2026-08-31 11:14:12 +08:00
Wesley Liddick fbc69322af Merge pull request #6254 from wucm667/fix/issue-6249-configurable-image-cooldown
feat(openai): configure image tool cooldown
2026-08-31 11:13:52 +08:00
Wesley Liddick 414489d152 Merge pull request #5469 from wucm667/fix/issue-5467-passthrough-http-bridge
fix(openai-ws): bridge oversized passthrough requests
2026-08-31 11:13:35 +08:00
Wesley Liddick 50a9dd7a64 Merge pull request #6386 from wucm667/fix/issue-6383-isolate-passthrough-ws
fix(openai): isolate passthrough websocket sessions
2026-08-31 11:13:16 +08:00
Wesley Liddick a4156eea10 Merge pull request #6385 from wucm667/fix/issue-6381-preserve-quota-limits
fix(admin): preserve quota limits on partial group updates
2026-08-31 11:12:56 +08:00
Wesley Liddick e0ecb55d4b Merge pull request #4872 from wucm667/fix/issue-4845-reset-scheduler-rate-limit
fix(account): clear rate limit on quota reset
2026-08-31 11:12:35 +08:00
wucm667 897faea333 fix(account): reset quota cooldown atomically 2026-08-31 09:12:09 +08:00
feeeei 0e08c39999 fix(openai-ws): remember rejected encrypted content across turns
invalid_encrypted_content 恢复此前只清理当次请求体:同一失效密文随客户
端维护的会话历史在后续每一轮重新出现,反复触发"整包被拒→剥离→重试/
重连"(#6382 生产观测 2h 内 251 次 recovery、273 次 WS 重连),既是
QoS 放大器也是内存压力放大器。

按会话记录被上游拒绝过的 encrypted_content SHA-256 摘要(lineage):
- OpenAIWSStateStore 新增会话级摘要绑定,TTL 复用 session sticky,
  单会话 512 摘要为存储自保护,超出仅退化为既有单次 recovery
- 写点覆盖 forward WSv2 recovery、forward 非 WS 400 重试、ingress
  error 事件、bridge 400/流内 error;读点在 forward 进场与 ingress/
  bridge turn 循环入口按摘要预剥离,新密文摘要不同不受影响
- 只收剥离端可处理的项类型(reasoning/compaction/compaction_summary),
  避免记入永远剥不掉的摘要后每轮空转解码
- replay 历史序列同步剥离,避免与已剥离的当前 input 项错位导致
  prefix 复用失配

含端到端集成测试:turn1 上游拒绝即记录,turn2 上游收到的请求不再携带
失效密文且 reasoning 骨架保留。
2026-08-31 01:02:13 +08:00
feeeei ca9b4d73f2 fix(openai-ws): share immutable replay bodies to stop O(T²) copying
OpenAI Responses WS/HTTP-bridge 的 replay 历史此前每轮对全部正文做多次
深拷贝(构建、双份序列、保存点各一遍),长会话累计分配 O(T²),实测
128 turn x 10KiB 达 160MB+,是 #6382 内存持续增长直至 OOM 的主因。

改为共享不可变正文所有权:
- extract 经零拷贝 string 视图 + gjson Index 直接切 payload 子切片,
  并以 json.Valid 保持对断裂数组的既有错误语义
- 序列构建/sanitize/collector/保存点全部只传引用;跨持有者保存经
  combineOpenAIWSReplayItems 新建头数组,正文不复制
- bridge 每轮一次解析,正常 replay 与 account-failover 序列共享正文
- 前缀比较增加字节级快路径,避免逐轮全历史 decode+encode
- strict 状态可用时不再整包保留上一轮 payload

新增 128 turn 分配回归测试锚定上界(实测 2.5MB,深拷贝回归会击穿),
以及正文共享的所有权断言。
2026-08-31 01:01:20 +08:00
lylelllll 94edcd5d81 chore: remove second misplaced component copy 2026-08-30 23:31:59 +08:00
lylelllll 3673702af5 chore: remove misplaced uploaded component copy 2026-08-30 23:27:08 +08:00
lylelllll 5778739cdb fix(frontend): use strict local expiry parsing for redeem codes 2026-08-30 23:20:59 +08:00
lylelllll 81e461f651 fix(frontend): parse local expiry datetimes strictly 2026-08-30 23:19:45 +08:00
lylelllll b7aca87fd0 test(frontend): cover local datetime parsing across timezones 2026-08-30 23:17:11 +08:00
lylelllll ae1bcdc25d fix(frontend): clarify expiry timezone in Chinese labels 2026-08-30 23:15:54 +08:00
lylelllll 263605779c fix(frontend): clarify expiry timezone in English labels 2026-08-30 23:13:52 +08:00
qiujunandClaude Opus 5 9eabd2a5b7 fix(billing): apply model pricing policies to account stats cost
resolveAccountStatsCost 的优先级 3 维护了第二份"单价 × token 数"实现,每加一个
定价特性都要手工镜像:service tier 与长上下文分别由 9261dd77、25e6688a 补齐,
而 b5827cfd 引入的 DeepSeek 官方峰谷一直没有生效,高峰时段账号成本最多低估一半。

改为复用 CalculateCostWithServiceTier。channelPricing 仍为 nil,优先级 3 的语义
不变:只取模型定价文件、不引入渠道自定义定价;低谷时段结果与原算式完全一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:57:53 +08:00
lylelllll d66bc88e68 fix(frontend): apply expiry input changes in source paths 2026-08-30 22:11:22 +08:00
lylelllll 8177f27aa3 fix(frontend): clarify local timezone for expiry inputs 2026-08-30 22:10:15 +08:00
wucm667 d5a0124637 fix(openai): isolate passthrough websocket sessions 2026-08-30 16:49:01 +08:00
alfadb 30b29e51e8 feat(ollama): 用量窗口支持挂在国产三家平台下的 Ollama Cloud 账号
Ollama Cloud 用量窗口(浏览器会话、5h/7d 快照、自动刷新、同 key 组共享)此前
只认 platform ∈ {openai, anthropic} 的 apikey 账号。但网关对 kimi/zhipu/deepseek
的 apikey 账号同样按 credentials.base_url 转发,用户把 Ollama Cloud 的 key 挂在
这三个平台下跑 Ollama Cloud 托管的 glm/kimi/deepseek 模型时,请求转发正常,用量
窗口却完全不可用。

- 平台白名单放开到 {openai, anthropic, kimi, zhipu, deepseek};type='apikey'
  与官方 ollama.com base_url 的限制不变
- Go 侧收敛为 isOllamaCloudUsagePlatform,SQL 侧收敛为
  ollamaCloudUsagePlatformsSQL,6 处白名单只引用单一来源,并补两侧互为镜像的
  漂移守卫测试
- CN 周期余额/额度探测跳过 ollama 用量账号:其探测端点由 base_url 衍生,对
  ollama.com 会被出站 URL 白名单拒绝,每轮都白跑并产生噪声
- 前端账号列表 CN 分支在 eligible 时改渲染 Ollama 用量窗口,并跳过 CN 额度/
  余额子单元格(同上,渲染出来只会是一行探测报错)
2026-08-30 16:38:11 +08:00
wucm667 9f1effd71c fix(admin): preserve quota limits on partial group updates 2026-08-30 16:36:13 +08:00
alfadb 6532d5b61e fix(frontend): unify usage window rendering and tighten alert thresholds
CN provider accounts (Kimi/Zhipu/DeepSeek) and the monitor quota view each
hand-rolled their own usage bar, so the same account column rendered three
different label styles, bar widths and threshold palettes. Both now reuse
UsageProgressBar, the component the other five platforms already share.

Also move the default utilization thresholds from >=100 red / >=80 amber to
>=90 / >=75. Turning red only once the quota is exhausted is an after-the-fact
notice rather than a warning. The remainingCapacity mode keeps its 20/50 pair,
which is already a warning semantic.

- UsageProgressBar: add labelWidth 'fixed' | 'auto' so the monitor view can
  render composite labels ("Pro/7d") without breaking column alignment; the
  fixed branch is byte-identical, so the existing platforms are untouched
- drop the superseded utilizationColor/utilizationTextColor/formatReset copies
- shorten CN window labels to 5h / 7d and remove the now-dead resetSoon keys
2026-08-30 15:34:21 +08:00
Long Li 57c76584a6 fix(codex): advertise priority tier for fast models 2026-08-30 14:21:08 +09:00
Ritel-T 82105f260a fix(billing): resolve shadow credential tier contract 2026-08-30 13:40:37 +09:00
Ritel-T 50ad6e2e50 fix(billing): narrow Codex OAuth default exception 2026-08-30 13:40:19 +09:00
Ritel-T f323d84644 test(billing): cover Codex OAuth usage pricing 2026-08-30 13:07:33 +09:00
Ritel-T 3a9070359b fix(billing): scope Codex OAuth tier trust 2026-08-30 13:07:21 +09:00
Ritel-T d39fc491ec test(openai): cover websocket tier separation 2026-08-30 13:07:07 +09:00
Ritel-T 2b8cb628b6 fix(openai): preserve tiers in messages chat fallback 2026-08-30 13:06:45 +09:00
Ritel-T 8b4b3f4a94 fix(openai): preserve tiers in responses chat fallback 2026-08-30 13:06:32 +09:00
Ritel-T 624e4eef64 fix(openai): separate outbound and observed tiers 2026-08-30 13:06:11 +09:00
wucm667 a3bbf33c05 fix(monitor): scope groups by user access 2026-08-30 09:28:40 +08:00
wucm667 7c616db07a fix(openai-ws): bridge oversized passthrough requests 2026-08-30 08:41:08 +08:00
Clansty 5fea83dcae fix: 修复429未耗尽额度仍触发账号退避
- 关闭429默认回避时不再创建OAuth运行时冷却

- 仅在x-codex窗口达到100%时使用上游重置时间

- 补充未耗尽额度和关闭开关的回归测试
2026-08-30 00:02:14 +08:00
qiujunandClaude Opus 5 0aef702b66 fix(frontend): prevent hidden cache tooltip from causing horizontal scroll
The cache token breakdown tooltip in `UsageStatsCards` was hidden with
`opacity-0`. An element with zero opacity is invisible but still takes part
in layout, so its fixed `w-56` (224px) width kept contributing to the
document's scrollable overflow.

The tooltip is anchored with `left-1/2 -translate-x-1/2`, and on mobile the
stats grid falls back to `grid-cols-2`, which puts the trigger icon close to
the right edge of the viewport. Centering a 224px box on that point pushes it
roughly 26-49px past the right edge, so `/usage` and `/admin/usage` gained a
phantom horizontal scroll with nothing visible to explain it.

Switching the hidden state to `hidden` (`display: none`) takes the tooltip out
of the flow entirely, while `group-hover:block` / `group-focus:block` keep the
existing hover and keyboard-focus behaviour. The 150ms opacity transition is
dropped, since `display` cannot be transitioned.

Verified over Chrome DevTools Protocol at 360/390/414px: `documentElement.scrollWidth`
goes from 424 back to 390 (equal to `clientWidth`) with no other overflowing
elements remaining, and the tooltip still renders at 224x86 on focus. A sweep of
all 39 application routes confirmed these were the only two affected pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:57:36 +08:00
alfadb b5827cfd54 fix(pricing): align DeepSeek billing with official peak/off-peak rates
同步 DeepSeek 官方峰谷价格与模型定价。
2026-08-29 17:30:23 +08:00
m11y ea4291a92c fix(anthropic): scope Fable scheduling threshold to Fable models
让 Fable 7d_oi 调度阈值仅限制 Fable 模型族,不暂停整个 Anthropic 账号。
2026-08-29 17:30:18 +08:00
101Harry 32ac921f24 fix(anthropic): avoid Fable OAuth system prompt refusal
修复 Fable OAuth 请求因系统提示词形态导致空响应的问题。
2026-08-29 17:29:30 +08:00
chensunlai ed12ea7163 fix(frontend): authenticate Codex API key mode inline
让 Codex API Key 模式在配置文件中正确发送 Bearer 鉴权并保留图像工具授权。
2026-08-29 17:29:26 +08:00
Ryan Chou eb4237a2b0 fix(billing): 渠道定价对带后缀的模型名不再被官方兜底价覆盖
修复带后缀模型无法命中渠道自定义定价的问题。
2026-08-29 17:29:21 +08:00
Brisbanehuang 706b5676a6 fix(groups): show API error messages on create and update
保存分组失败时显示标准化 API 错误信息。
2026-08-29 17:28:20 +08:00
wucm667 c31fe2ed9e fix(admin): preserve saved SMTP TLS mode in test endpoints
保留测试邮件端点已保存的 SMTP TLS 配置,区分省略与显式 false。
2026-08-29 17:28:00 +08:00
Wesley Liddick 987588eaa1 Merge pull request #6358 from Wei-Shaw/fix/openai-spark-model-429-rate-limit
fix(openai): scope Spark quota 429 to model
2026-08-29 15:44:37 +08:00
shaw 804679d996 fix(openai): preserve model scope on stream failover 2026-08-29 15:25:10 +08:00
shaw 571d1e1d9b fix(openai): isolate websocket semantic rate limits 2026-08-29 11:14:54 +08:00
shaw 3c22e78af0 fix(openai): preserve Spark quota reset semantics 2026-08-29 11:08:01 +08:00
shaw 5d9c7abed5 fix(openai): scope Spark quota 429 to model 2026-08-29 09:57:08 +08:00
Wesley Liddick ac18c588c8 Merge PR #6165
修复上游倍率探测导致账号列表整页刷新的问题
2026-08-29 09:12:05 +08:00
Wesley Liddick b83284071a Merge PR #5348
fix(subscription): 周/月窗口展示的重置时间与实际推进锚点对齐
2026-08-29 09:10:07 +08:00
Wesley Liddick fd13c72e95 Merge PR #6330
feat(zhipu): support team GLM Coding Plan usage query
2026-08-29 09:08:42 +08:00
Wesley Liddick 3c2e9a43b9 Merge PR #5860
fix(antigravity): preserve mixed built-in chat tools
2026-08-29 09:07:27 +08:00
Wesley Liddick ca319d09f2 Merge PR #6280
fix(update): strip hyphenated pre-release/custom suffix in version comparison
2026-08-29 09:06:10 +08:00
Wesley Liddick 46598dd491 Merge PR #6306
fix(openai): emit pre-output SSE keepalive on the /v1/responses passthrough path
2026-08-29 09:04:46 +08:00
Wesley Liddick 3045b3ade1 Merge PR #6316
fix(openai): honor Claude Code session id for messages sticky routing
2026-08-29 09:03:14 +08:00
Wesley Liddick 604360d1cc Merge PR #6328
fix(frontend): 批量编辑显式提交 codex_fingerprint_mode=off,修复无法关闭指纹收敛
2026-08-29 09:02:25 +08:00
Wesley Liddick 25dc5742ea Merge PR #6332
fix(monitor): 配额抓取在 singleflight 执行体内重查缓存,消除重复上游查询
2026-08-29 09:01:15 +08:00
Wesley Liddick 1136e290fd Merge PR #6334
fix(payment): show selected currency in recharge rate
2026-08-29 09:01:06 +08:00
Wesley Liddick 6506c0ea68 Merge PR #6343
fix(gateway): preserve Anthropic tool arguments
2026-08-29 09:00:56 +08:00
Wesley Liddick 1323d1645d Merge PR #6346
fix(keys): preserve Claude attribution headers
2026-08-29 09:00:46 +08:00
Wesley Liddick f1d845c630 fix(grok): honor prompt cache key for side calls
fix(grok): honor client prompt_cache_key over X-Grok-Conv-Id so grok-build side-calls share the main-turn cache
2026-08-29 09:00:06 +08:00
RayWangX 5688bcba91 fix(openai): honor Claude Code session for messages sticky routing 2026-08-29 08:31:15 +08:00
Jarl 1a61eb7151 test(usage): update compaction API contracts 2026-08-28 23:32:11 +08:00
Jarl 1cc6999ada feat(usage): track native compaction requests 2026-08-28 23:32:11 +08:00
风起 88cb79d8b3 fix(grok): honor client prompt_cache_key over X-Grok-Conv-Id for cache identity
grok-build CLI recap-style side-calls (turn-summary / title-refresh) send
a fresh per-call X-Grok-Conv-Id label but set body prompt_cache_key to
the parent session id, so a direct xAI connection shares the main turn's
server-side prompt cache prefix.

sub2api's explicitGrokCacheSeed preferred X-Grok-Conv-Id over the body
key, which fragmented the cache identity on every side-call and forced a
full-price replay of the whole conversation (~300K+ tokens each, ~2.3%
cache hit rate observed over 44 side-calls / 4.9M input tokens in 7
days — about a third of all grok-4.6 input).

Priority is now: CC session > generic session headers > body
prompt_cache_key > X-Grok-Conv-Id > explicitKey > previous_response_id.
IDE header-stability tests updated; new regression test locks in the
side-call/parent key-sharing contract.
2026-08-28 21:56:22 +08:00
Ryan ChouandClaude Opus 5 32ad1dcdce fix(subscription): 周/月窗口展示的重置时间与实际推进锚点对齐
订阅首窗初始化在开通日零点时,automaticWindowStartAt 会把实际推进锚点修正为
StartsAt,但展示层(calculateProgress / WeeklyResetTime / MonthlyResetTime)
仍按窗口起点+周期计算,导致仪表盘显示的重置时间早于实际重置时间(差值 =
StartsAt 的时分秒)。用户在显示的重置时间过后请求仍会收到 429
WEEKLY_LIMIT_EXCEEDED。

抽出 windowResetAnchor 供推进与展示共用,保证两者恒一致。非 legacy 锚点
(手动重置或已推进过的窗口)保持权威,不做修正。

日窗口不在本 PR 范围内:#5380 已把日额度改为按日历日对齐(DailyResetTime =
窗口起点所在日的次日 0 点),与周/月的期限对齐滚动窗口语义不同,不共用本修正。
附一个守护用例钉死这条边界。

Fixes #5348

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 21:46:44 +08:00
wucm667 c037766040 fix(keys): preserve Claude attribution headers 2026-08-28 20:42:58 +08:00
wucm667 da10822d7d fix(gateway): preserve Anthropic tool arguments 2026-08-28 19:08:26 +08:00
wucm667 5345881b1d Merge remote-tracking branch 'upstream/main' into fix/issue-5843-antigravity-mixed-tools
# Conflicts:
#	backend/internal/repository/group_usage_rollup_trigger_integration_test.go
2026-08-28 17:36:43 +08:00
wucm667 c5ff640dff test(repository): pin rollup trigger session timezone 2026-08-28 17:34:42 +08:00
wucm667 02eee39ddf fix(payment): show selected currency in recharge rate 2026-08-28 16:54:33 +08:00
li e652f6e20a fix(monitor): 配额抓取在 singleflight 执行体内重查缓存,消除重复上游查询
ChannelMonitorQuotaFetcher.Fetch 顶部的 cachedSnapshot 与随后的 flight.DoChan
之间存在一个窗口:goroutine A 判定缓存未命中之后、真正入队之前,goroutine B 的
flight 可能已经跑完、写好缓存,并且它的 singleflight key 也已被摘掉。A 于是不会
并入 B 的那次飞行,而是另起一个新 flight,对同一账号再打一次上游——正是
singleflight 想要消除的重复查询。

多个监控可以关联同一账号,interval 最小 15s,国产配额服务自身无缓存,
这条路径的全部意义就是防止打爆上游配额端点(见文件头注释)。

修复:把 flight 执行体提为具名方法 fetchShared,开头重查一次缓存。
这次重查必然命中,所以合并是确定的而非尽力而为:storeSnapshot 发生在执行体
返回之前,singleflight 删 key 发生在返回之后,因此「能新起一次飞行」必然蕴含
「上一次的快照已经可见」。

该窗口也是主线 CI 上 TestQuotaFetcher_ConcurrentFetchesShareSingleFlight
间歇性失败(expected: 1, actual: 2)的成因,本地 -count=900 复现 3 次(约 0.33%),
带修复后 -count=1700 零失败。

新增两条确定性用例:既有并发用例只能以约 1/300 的概率撞上该窗口,故直接钉住
窗口本身——fetchShared 就是 flight 的执行体,直接调用它等价于「已越过顶层缓存
判定、拿到属于自己的那次飞行」这一状态。另一条守住重查不得退化成无条件短路:
缓存过期后必须照常回源。
2026-08-28 16:49:49 +08:00
wall-wxk c4e46c3be8 feat(zhipu): support team GLM Coding Plan usage query
Team GLM Coding Plan accounts fail the quota probe with
"当前用户不存在coding plan" because the personal-plan query path
does not carry team context.

- quota probe: when credentials contain zhipu_organization, switch the
  Zhipu quota endpoint to the team variant (?type=2 + bigmodel-organization
  / bigmodel-project headers); personal plans keep the existing path
- account create/edit modals: optional organization/project ID inputs for
  zhipu coding-plan accounts, persisted in credentials; edit modal
  backfills them and clears back to personal plan when emptied
- add a click-to-open tooltip tutorial (zh/en) explaining how to grab the
  IDs from the bigmodel.cn web console via the /api/biz/v1/organization
  network request, with a sample URL mapping org-/proj_ segments to fields
2026-08-28 16:36:30 +08:00
li 0756c98102 fix(frontend): 批量编辑显式提交 codex_fingerprint_mode=off,修复无法关闭指纹收敛
批量编辑选「关闭(默认)」时,buildUpdatePayload 先 ensureExtra() 建出 extra
再 delete 掉唯一的键,payload 退化成 {account_ids, extra:{}},被后端
len(req.Extra) > 0 判为空更新,直接 400 "No updates provided";device/session/full
则正常。即便后端放行空 extra 也无济于事:批量更新走 JSONB 顶层合并
(extra = COALESCE(extra,'{}') || payload),删掉 payload 里的键只表示
「本次不更新该键」,清不掉账号上已有的 device/session/full。

根因是把 Create/Edit 的写法直接搬到了 Bulk:那两个表单提交完整 extra 对象、
后端 SetExtra 整体覆盖,删键确实等于清除;批量接口只合并增量键,两种持久化
语义不能共用同一套写法。

改为 off 也显式落键,与本函数里其它「关闭/清除」字段一致——codex_cli_only
直接落 false(EditAccountModal 相邻注释已写明「关闭时显式写 false,避免 extra
为空被后端忽略导致旧值无法清除」)、load_factor 落 0、proxy_id 落 0。

读取侧完全等价,不改变 #5610 定下的语义:codexFingerprintModeFromExtra 对
空值/非法值走 default 回落 off,对 "off" 命中同一分支;
ShouldEnsureCodexFingerprintSeedForExtraUpdates 也只在 device/session/full
时要种子,显式 off 不会触发种子创建。

顺带给「编辑该项」勾选框补 id(同区域 Select 早有 data-testid,兄弟开关
codex_cli_only 也有 id),否则新增用例无法定位该控件。

新增 3 条用例:主复现(默认 off + 勾选编辑 ⇒ 提交 codex_fingerprint_mode=off,
并断言 extra 非空以钉死 400 那个形状)、显式 opt-in 仍原样提交、未勾选时不写入该键。

Fixes #6327
2026-08-28 16:20:14 +08:00
Wesley Liddick 7b693ae429 Merge pull request #6188 from JialinLiu-codedance/feat/usage-requested-reasoning-effort
feat(usage): 使用记录展示映射前的推理强度
2026-08-28 16:18:50 +08:00
Wesley Liddick d4754c211a Merge pull request #6291 from Whisper-stark/fix/grok-codex-responses
fix(grok): sanitize Codex Responses requests
2026-08-28 15:58:39 +08:00
Wesley Liddick 423f895755 Merge pull request #6227 from OG-Wang/codex/promo-code-oauth-signup
fix(auth): pass registration promo codes to OAuth signup
2026-08-28 15:58:09 +08:00
Wesley Liddick c8a6e93f33 Merge pull request #6310 from fengshao1227/fix/nonstreaming-terminal-failure-failover-v2
fix(openai): 非流式路径按流式同一套判定处理 HTTP 200 终止失败事件
2026-08-28 15:57:39 +08:00
Wesley Liddick 446042e518 Merge pull request #6309 from fengshao1227/fix/easypay-resolve-relative-payurl-qrcode
fix(payment): EasyPay 补全 mapi.php 回传的相对 payurl/qrcode
2026-08-28 15:57:10 +08:00
Wesley Liddick 1e6926cb9c Merge pull request #6312 from fengshao1227/fix/openai-ws-client-close-not-account-failure
fix(openai): 入站 WS 的客户端正常关闭与断开不再计为账号故障
2026-08-28 15:56:34 +08:00
251392463 b4b5371640 fix(grok): preserve Responses vision tool output images 2026-08-28 15:38:04 +08:00
spongehah 6d339ec93b fix: honor persisted cooldown when dropping in-process account blocks
- Treat TempUnschedulableUntil/RateLimitResetAt/OverloadUntil as source of truth during account selection
- Drop stale local account-level runtime blocks with generation+deadline CAS
- Leave model-scoped transient and Grok quota blocks unchanged
2026-08-28 14:38:15 +08:00
Wesley Liddick e866ff6ec4 Merge pull request #6303 from akihitohyh/feat/per-user-public-group-access
feat(admin): 支持为单个用户限制可访问的公开分组
2026-08-28 13:19:11 +08:00
li c83dced4ba fix(openai): 入站 WS 的客户端正常关闭与断开不再计为账号故障
ingress WebSocket 收尾处只把 *service.OpenAIWSClientCloseError 且状态码为 1000
的情况认作正常关闭,其余全部落到 shouldReportOpenAIWSProxyAccountFailure ——
而它只排除 model-switch 与 session-preempted 两种。两种完全正常的结束因此被
误判为上游/账号故障:

- 客户端干净关闭:coder/websocket 直接回裸 coderws.CloseError{Code: 1000},
  ReadOpenAIWSClientMessage 把 conn.Read 的错误原样返回,没有任何一层把它包成
  *OpenAIWSClientCloseError,所以 errors.As 对该类型必然为假。
- 客户端中途断开:ReadOpenAIWSClientMessage 在 controlCtx.Done() 分支用
  StatusGoingAway(1001) 收尾并以 context.Canceled 为 cause,「只认 1000」匹配不到。

两者都会走到 ReportOpenAIAccountScheduleResult(success=false),进而喂给
ObserveOpenAIAPIKeyHealthFailure 与 scheduler.ReportResult,累积到阈值即把上游
账号熔断出调度池——客户端按一次 Ctrl-C 就在扣上游账号的健康分。

同文件的 summarizeWSCloseErrorForLog 一直用 coderws.CloseStatus 读关闭码,就在
该判断之后 12 行被调用,于是同一条 WARN 会打印
close_status=1000(StatusNormalClosure) 的同时把账号记为故障。

失败归因的政策仓库早有明文:同包 failover_loop.go 的 failoverClientGone 写着
「客户端断开后 failover 必须静默终止……并被误报成账号耗尽」。本次把 WS ingress
补齐到同一条规矩。

判定收窄:只豁免 1000 与 context.Canceled。1001 不整类放行——网关也会因自身原因
用 GoingAway 收场,客户端取消那一支已由 context.Canceled 覆盖;
context.DeadlineExceeded 同样不放行,空闲超时那条本就被包成 1000 走前一个判据,
其余 deadline 是真实停滞。

Fixes #6105
2026-08-28 13:17:43 +08:00
Wesley Liddick d674a04f2a Merge branch 'main' into feat/per-user-public-group-access 2026-08-28 13:08:41 +08:00
Wesley Liddick 9b61c1bdd9 Merge pull request #6263 from zenor0/fix/openai-reset-usage-sync
fix(openai): refresh usage after quota reset
2026-08-28 13:08:33 +08:00
Wesley Liddick add86cc3bc Merge branch 'main' into fix/openai-reset-usage-sync 2026-08-28 12:57:48 +08:00
Wesley Liddick dc332d141c Merge pull request #6260 from feeeei/fix/anthropic
fix(gateway): Anthropic/Bedrock 传输层错误转 failover + 持久故障临时摘除账号
2026-08-28 12:57:41 +08:00
Wesley Liddick 59bb131df4 Merge branch 'main' into fix/anthropic 2026-08-28 12:47:15 +08:00
Wesley Liddick 443537daae Merge pull request #6293 from specialpointcentral/fix/ws-v2-passthrough-cyber
fix(openai): detect cyber policy in ws v2 passthrough
2026-08-28 12:47:07 +08:00
Wesley Liddick b588c0bc42 Merge branch 'main' into fix/ws-v2-passthrough-cyber 2026-08-28 12:36:22 +08:00
Wesley Liddick 4e9c1d7c8b Merge pull request #6299 from william-drakemond/fix/openai-raw-stream-truncation
fix(openai): fail raw CC streams truncated before any terminal chunk
2026-08-28 12:36:14 +08:00
Wesley Liddick a0b313018a Merge branch 'main' into fix/openai-raw-stream-truncation 2026-08-28 12:26:01 +08:00
Wesley Liddick fafc4d288d Merge pull request #6278 from kk-418/fix/grok46-xhigh
fix(grok): 为 Grok 4.6 广告 xhigh 推理档位
2026-08-28 12:25:52 +08:00
Wesley Liddick de084cdfc2 Merge branch 'main' into fix/grok46-xhigh 2026-08-28 12:15:53 +08:00
Wesley Liddick 7eed2d3b7d Merge pull request #5623 from fengshao1227/fix/responses-synthesized-created-at
fix(responses): 网关合成的 Responses 对象补齐 created_at,修复严格客户端反序列化失败
2026-08-28 12:15:45 +08:00
Wesley Liddick f7dca22ea5 Merge branch 'main' into fix/responses-synthesized-created-at 2026-08-28 12:05:38 +08:00
Wesley Liddick 5eb8628ff2 Merge pull request #5620 from fengshao1227/fix/anthropic-chat-bridge-reasoning-passback
fix(apicompat): Anthropic→Chat 桥回传工具调用的 thinking,修复 DeepSeek 多轮 400
2026-08-28 12:05:31 +08:00
Wesley Liddick 002aaaa3d0 Merge branch 'main' into fix/anthropic-chat-bridge-reasoning-passback 2026-08-28 11:55:10 +08:00
Wesley Liddick 0f6ad105fe Merge pull request #6246 from wucm667/fix/issue-6240-preserve-multimodal-tool-output
fix(openai): preserve multimodal client tool outputs
2026-08-28 11:55:02 +08:00
Wesley Liddick 28fa458dc7 Merge branch 'main' into fix/issue-6240-preserve-multimodal-tool-output 2026-08-28 11:37:40 +08:00
Wesley Liddick eca8d6b9ab Merge pull request #6200 from xuhaihan/fix/openai-reasoning-replay
fix(openai): normalize cross-provider reasoning replay
2026-08-28 11:37:33 +08:00
li 81ac8ccd64 fix(openai): 非流式路径按流式同一套判定处理 HTTP 200 终止失败事件
stream=false 时上游仍可能回 SSE(其他 sub2api 实例、部分 OpenAI 兼容上游),
容量/限流错误经 HTTP 200 的 response.failed / error 终止事件回传。
handleSSEToJSON 与 handlePassthroughSSEToJSON 把所有终止事件塞进固定 502
(writeOpenAINonStreamingProtocolError),而几百行外的流式读取器对同一帧走
openAIStreamFailedEventShouldFailover / openAIStreamErrorEventShouldFailover
判定并返回 UpstreamFailoverError。

同一个上游、同一个事件,仅因请求上的 stream 标志而结果相反:流式换号,
非流式把上游原文直接抛给客户端,池里还有可调度账号也不换。

新增 nonStreamingTerminalFailureFailover,在两条非流式路径上复用流式的同一套
分类,并按 terminalType 分派——裸 error 帧走更保守的那个(仅正向识别为瞬时才
换号),response.failed 走完整的那个,与流式读取器逐字一致。

安全前提:响应体已被 ReadUpstreamResponseBody 完整缓冲,判定发生在写出任何
语义字节之前。能否真正换号仍由 handler 的 openAIForwardMayFailover 用
keepalive 调整后的写出量仲裁(#3887),service 侧只额外拒绝 IsResponseCommitted
与 account 为 nil 的场景,不重复实现心跳判定,避免与 handler 口径分叉。

行为变更:未被分类为不可重试的泛化 response.failed 在非流式路径上由「回写 502」
变为「换号」——这正是与流式对齐的结果。既有用例
TestHandleSSEToJSON_ResponseFailedReturnsProtocolError 传的是 nil account,
无账号则无可换对象,故其断言原样保留,仅更名为 ...WithoutAccountReturnsProtocolError
并注明原因;带真实账号的同一报文由新增用例钉死。

Fixes #5281
2026-08-28 11:36:08 +08:00
Wesley Liddick d24de611f8 Merge branch 'main' into fix/openai-reasoning-replay 2026-08-28 11:27:39 +08:00
Wesley Liddick f83fe6435b Merge pull request #6277 from TYzzt/fix/apikey-namespace-roundtrip
fix(openai): preserve declared API-key namespace calls
2026-08-28 11:27:31 +08:00
Wesley Liddick ee2c8b97bb Merge branch 'main' into fix/apikey-namespace-roundtrip 2026-08-28 11:16:48 +08:00
Wesley Liddick d36f4dd6c6 Merge pull request #6255 from feeeei/main
fix(openai): 避免 pool 两跳重复计算 system 提示词
2026-08-28 11:16:40 +08:00
li 1e8745c887 fix(payment): EasyPay 补全 mapi.php 回传的相对 payurl/qrcode
createAPIPayment 把上游 mapi.php 返回的 payurl/payurl2/qrcode 原样落库。
EasyPay 兼容上游对该字段并不统一:部分实现回站点根相对路径(如
`/api/pay/toapp/<order>`)而非完整地址。而链路上没有任何一层修复它——
sanitizeCreatePaymentResponseDetails 只剥 NUL 字节就写进 pay_url/qr_code,
前端 PaymentQRCodeView.renderQR 又把 qr_code 直接喂给 QRCode.toCanvas。
结果二维码的载荷是一段裸路径:微信扫出来是文本,pay_url 则落到网关自己的
域名下 404,用户无法进入上游收银台。

同文件的 createRedirectPayment 本就自行拼绝对地址(apiBase + "/submit.php?"),
只有 mapi.php 这一支透传;Alipay provider 也早已写明「Setting it as QRCode
would let the frontend render an unscannable image」并拒绝产出不可扫的值。
本提交让 EasyPay 与这两处对齐:按实例配置的 apiBase 解析上游回传的引用。

仅补全以 "/" 开头的引用。带 scheme 的一律原样返回,覆盖绝对 https:// 收银台
地址与 weixin:// / wxp:// / alipays:// 等 app 深链;无前导斜杠的引用同样不动,
因为二维码载荷常是不透明 token,把 "OrderToken123" 改写成
"<apiBase>/OrderToken123" 会毁掉一个当前可用的载荷,比本 bug 更糟。

payurl2 优先于 payurl 的选择语义保持不变。

Fixes #6292
2026-08-28 11:09:34 +08:00
Wesley Liddick 96e9ab8662 Merge branch 'main' into main 2026-08-28 11:06:29 +08:00
Wesley Liddick 0eddfe1cfa Merge pull request #5395 from ryanchou1994/fix/openai-images-capability-loss-cooldown
fix(openai): 图像能力丢失时冷却 image 调度,不再反复选中坏号
2026-08-28 11:06:22 +08:00
Wesley Liddick acd2f09ddd Merge branch 'main' into fix/openai-images-capability-loss-cooldown 2026-08-28 10:54:47 +08:00
Wesley Liddick 5929cdd38c Merge pull request #6270 from fengshao1227/fix/openai-images-tool-cooldown-not-from-text-heuristic
fix(openai): 图片工具冷却不再由「模型回文字」这一请求级判据触发
2026-08-28 10:54:33 +08:00
HuntercodeT 60756c0ca4 fix(openai): emit pre-output SSE keepalive on the /v1/responses passthrough path
The passthrough streaming loop buffers leading events in `pendingLines` until
the first visible output, so a client receives **zero bytes** — not even the
HTTP response headers, since gin's ResponseWriter only commits them on the
first write — for as long as the upstream takes to produce its first visible
event.

The Forward path already recognises this exact hazard and solves it with a
downstream keepalive:

    // Track downstream writes separately from upstream reads: pre-output
    // failover can buffer response.created / response.in_progress, so
    // keepalive must be based on downstream idle time.

The passthrough path has the same buffering but never starts a keepalive.
Reasoning models routinely think for several minutes before emitting a visible
event, so intermediate proxies close the connection on idle timeout.

Production evidence from one deployment (12h window, gpt-5.6 family):

  * 44 `/v1/responses` requests produced their first visible output only after
    600–900 s, each carrying 30k–50k output tokens — the upstream had finished
    the work.
  * Every one of them was killed by an intermediate nginx `proxy_read_timeout`
    (600 s) and returned 504. The user received nothing.
  * Cutting early does not even save resources: the code deliberately keeps
    draining the upstream for usage after the client disconnects.

Fix: start the existing keepalive helper for the passthrough stream as well.

* `StartOpenAICompactSSEKeepalive` keeps its compact-marker gate; the gate-free
  body is extracted as `startOpenAISSEKeepalive` for callers that already know
  they are in an SSE context.
* The passthrough loop starts it when `gateway.stream_keepalive_interval > 0`
  and stops it immediately before its first real write, so the ResponseWriter
  is never shared.

Pre-output failover is unaffected: keepalive bytes are already excluded by
`OpenAICompactKeepaliveAdjustedWrittenSize`, which `openAIStreamClientOutputStarted`
on this very path already consults (#3887). No pendingLines and no
account-specific headers are ever emitted by a keepalive.

Behaviour is unchanged when `gateway.stream_keepalive_interval` is 0.
2026-08-28 10:19:43 +08:00
akihitohyhandClaude Opus 5 b56c61eccd feat(admin): let admins restrict which public groups a user may access
Public groups have always been bindable by every user: CanBindGroup returned
true for any non-exclusive group, and user_allowed_groups only ever carried the
exclusive groups an admin had granted. Admins had no way to hand a single user
a subset of the public groups short of converting a group to exclusive, which
changes it for everyone already using it.

A user now carries restrict_public_groups. While it is false, which is the
default and what every existing row migrates to, nothing changes: every public
group stays bindable. Once an admin turns it on for a user, that user's public
groups are narrowed to the ones listed in user_allowed_groups, the same table
that already gates exclusive groups.

The flag is an administrative control, so it rides on the admin user DTO only
and leaves the shape of the end-user endpoints alone.

The model plaza filter honours the flag as well, so a restricted user is not
shown groups they would be refused when binding a key. Anonymous visitors have
no user record and keep the previous view.

Enforcement rides on the existing CanBindGroup choke point, so it covers key
creation, key updates, and per-request authorization together. An API key bound
to a group that is later withdrawn stops working at request time rather than
lingering as a key that can be listed but not used.

The admin dialog gains a toggle over the public group list. Turning it off
re-checks every public group, so an admin cannot save a list that reads as
restrictive while the restriction itself is disabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 02:40:17 +08:00
Qi HU f4e3eb1c5a fix(openai): detect cyber policy in ws v2 passthrough 2026-08-27 21:50:53 +08:00
Whisper-stark fd872550d5 fix(grok): handle typed invalid tool unions 2026-08-27 21:03:23 +08:00
Whisper-stark f4820c00dc fix(grok): simplify invalid tool union roots 2026-08-27 19:24:50 +08:00
Whisper-stark de6ef7134d fix(grok): sanitize Codex Responses requests 2026-08-27 19:24:50 +08:00
Ryan ChouandClaude Opus 5 e6ea7b9af0 fix(openai): 图像能力丢失时冷却 image 调度,不再反复选中坏号
OAuth 账号被上游收走 image_generation 能力后,/v1/images/* 稳定返回同一句 400
(Tool choice 'image_generation' not found in 'tools' parameter),但网关不做
任何处置,坏号持续被排程选中(报告者实测连续 17 次同 body 400、窗口内 0 成功、
持续 3.5 小时,直到人工停调度)。

RateLimitService 的 400 分支是白名单式,只认 organization has been disabled /
credit balance / identity verification is required 三句,其余 400 明确不处理;
唯一能兜任意 400 的 tryTempUnschedulable 又要求账号开启 temp-unschedulable
开关,OAuth 账号默认没开。两条路都不命中,shouldDisable 恒为 false,也没有任何
model 作用域冷却。

修复收窄到 sub2api 自建 body 的 images 管线:buildOpenAIImagesResponsesRequest
产出的 body 必定同时带 tool_choice 与匹配的 image_generation tool,这条路上出现
该 400 只可能是上游收了账号能力。forwardOpenAIImagesOAuth 在 upstreamCtx 上打
自建标记(覆盖其下游全部三个错误分支),共用错误入口读到标记才把该 400 判为能力
丢失,对 openai:image_generation 作用域冷却 30 分钟(对齐 upstreamModelNotFound
Cooldown),不禁用整颗账号——账号对文本请求依然健康。

未采用 issue 建议①的全局签名匹配:handleOpenAIAccountUpstreamError 同时服务
passthrough 路径,客户端可自带「tool_choice 指 image_generation 但 tools 不带」
的请求触发一字不差的同一句 400,全局匹配等于任何 API 用户一句话即可禁用健康
账号。附 5 个测试,其中防毒杀用例在撤掉该闸门后会实测失败。建议②(连续 N 次
400 熔断)涉及跨请求状态与配置项,另议。

Fixes #4466

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:36:01 +08:00
WUJI-Labs 9e7aff59df fix(update): strip hyphenated suffix from version string during comparison
When running a custom build or pre-release version (e.g. v0.1.183-custom),
parseVersion failed to parse the patch segment because of the non-numeric suffix,
causing compareVersions to incorrectly report an available update.

This commit strips any hyphenated suffix before splitting SemVer components.

Signed-off-by: WUJI-Labs <zoujincool@gmail.com>
2026-08-27 15:54:22 +08:00
kk 00efee4303 🐛 fix(grok): 为 Grok 4.6 广告 xhigh 推理档位
Grok Build 的 /v1/models 原先给所有可配 reasoning 的 Grok 模型写死三档,
导致 grok-4.6 无法在 /model 里选出官方支持的 xhigh。转发与 Codex catalog
已按模型判断,现与同一条件对齐。
2026-08-27 15:50:20 +08:00
zhaotao1 00b7c855cc fix(openai): preserve declared API-key namespace calls 2026-08-27 15:22:43 +08:00
li 4a02e54171 Merge remote-tracking branch 'origin/main' into fix/anthropic-chat-bridge-reasoning-passback 2026-08-27 13:40:29 +08:00
li 9192426d2e merge: 合入 origin/main,与新增的 service_tier 字段并存
主线在 ResponsesResponse 与两处流式事件上新增了 service_tier;本分支新增的
created_at 与之并列,无语义冲突。同时跟进 ChatCompletionsResponseToResponses
新增的 functionTools 形参。
2026-08-27 13:40:07 +08:00
li d077002eba fix(openai): 图片工具冷却不再由「模型回文字」这一请求级判据触发
v0.1.181 起,/v1/images/generations 只要上游回了文字而没回图,就给账号写 30 分钟
openai:image_generation 模型级冷却。该判据由 openAIImagesTextFallbackErrorForText
从模型的自然语言输出**推断**而来,只说明这个 prompt 这一轮模型选择了说话——
仓库自己的既有用例 fixture 就是一句「Here's a polished image prompt for your request.」。

更糟的是同一个错误被判为可重试(502 → IsOpenAIImagesRetryableUpstreamError)并驱动
newOpenAIAccountFailoverError:一次闲聊回复会被逐个账号重试,沿途把每个账号都冷却掉,
表现为「开启后没多久账号就全部被限制」。

改为只在上游 error 帧点名该状态时才写冷却;网关自行推断的判据打上
SynthesizedFromModelText 标记并跳过。换号行为一字未动。

依据是仓库既有政策(alpha/search 路径原话):工具端点的单次失败
「仍允许本次请求换号,但不修改任何账号状态」。

Fixes #6171
2026-08-27 13:36:50 +08:00
zenor0 5f09442fce fix(openai): refresh usage after quota reset 2026-08-27 10:09:42 +08:00
feeeei 44003d7f6b fix(gateway): Anthropic/Bedrock 传输层错误转 failover + 持久故障临时摘除账号
上游 Do/DoWithTLS 返回非 HTTP 错误(代理/DNS/TCP/TLS)时,Anthropic 五条转发路径
(forward/apikey 透传/Bedrock/ForwardAsResponses/ForwardAsChatCompletions)原先
直接向客户端写 502 且不换号,单账号网络故障期间该账号仍持续被调度,请求全量失败。

对齐 OpenAI 侧 handleOpenAIUpstreamTransportError 的既有语义:
- 传输层错误统一返回 *UpstreamFailoverError(502),由 handler failover 循环换号,
  耗尽后才向客户端渲染错误;service 不再写响应
- 持久性故障(connection refused/no route/DNS not found/代理认证失败)额外临时
  摘除账号 10 分钟(仅写库,Anthropic 调度以持久化状态为准,无内存快路径)
- context.Canceled 保持原样返回:不换号也不摘账号
- 分类器改为平台无关命名(classifyUpstreamTransportError)供两侧共用
- Ops 错误事件保留各路径原有字段(UpstreamURL/DurationMs/Passthrough)
2026-08-27 09:56:41 +08:00
feeeei d881bfc0de fix(openai): 避免 pool 两跳重复计算 system 提示词 2026-08-26 19:55:49 +08:00
wucm667 6ff771d3d3 feat(openai): configure image tool cooldown 2026-08-26 19:07:39 +08:00
Wesley Liddick efb46db0a9 Merge pull request #5926 from baryon/contrib/routed-codex-model-catalog
feat(gateway): generate complete routed Codex model catalogs
2026-08-26 15:49:38 +08:00
shaw 8ba81615e0 merge: resolve conflict with main (upstream endpoint observability)
Merge origin/main into contrib/routed-codex-model-catalog.

Single conflict in openai_gateway_forward.go: main added
ClearActualOpenAIUpstreamEndpoint + SetActualOpenAIUpstreamEndpoint
(commit 4795650d2) at the same insertion point where the PR added
filterOpenAIResponsesNoneReasoningEffortForAccount. Both changes
are independent — endpoint observability first, then body filtering.
2026-08-26 15:29:01 +08:00
wucm667 50ba146298 fix(openai): preserve multimodal client tool outputs 2026-08-26 15:04:17 +08:00
Wesley Liddick b9083fc7ad Merge pull request #6211 from Monika-shipship/codex/ws-v2-tool-id-regression
test(openai): 补充 OAuth Responses WS v2 历史 ID 清洗回归测试
2026-08-26 15:03:24 +08:00
Wesley Liddick 1bf76d26d7 Merge pull request #6229 from Wei-Shaw/fix/openai-upstream-endpoint-observability
fix(openai): 记录真实上游端点避免错误误报
2026-08-26 15:03:11 +08:00
Long Li 2abce65031 fix(codex): harden routed catalog capability sync 2026-08-26 12:46:31 +09:00
haruka 1ee929e4b9 fix: broadcast channel cache invalidations 2026-08-26 11:16:32 +08:00
shaw 6ca1e15b0a chore: update sponsors 2026-08-26 09:58:16 +08:00
Long LiandCursor 195b219707 fix(codex): isolate API-key catalog cache and DeepSeek Codex defaults
Copy cached API-key manifests before group-specific mutation, use DeepSeek
model IDs for Codex fallbacks, omit unsupported config.toml effort, and
drop wildcard mapping keys from generated catalogs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 10:56:17 +09:00
shaw 66d664ff08 chore: update sponsors 2026-08-26 09:53:47 +08:00
shaw 4795650d21 fix(openai): record actual upstream endpoint on errors 2026-08-26 09:42:28 +08:00
Rick d522aed65e fix: preserve promo codes for OAuth signup 2026-08-26 08:58:22 +08:00
github-actions[bot] 7634e3c23b chore: sync VERSION to 0.1.183 [skip ci] 2026-08-25 13:53:14 +00:00
Monika d8694f03bc test(openai): cover WSv2 cleanup of stale native tool IDs 2026-08-25 21:45:24 +08:00
Wesley Liddick e8cb019fab Merge pull request #6078 from YogaSakti/fix/responses-custom-tool-call-id
fix(openai): keep restored tool-call item IDs typed
2026-08-25 21:32:34 +08:00
Wesley Liddick 7a7bd3729c Merge pull request #6166 from jyking/fix/email-bind-alias-dedup
fix(auth): 邮箱换绑增加别名与并发守卫
2026-08-25 21:31:39 +08:00
Wesley Liddick 49ad7021d7 Merge pull request #6164 from jimbirthday/fix/openai-sticky-capacity-spillover
fix(openai): preserve sticky binding on capacity spillover
2026-08-25 21:28:38 +08:00
Wesley Liddick 4952b919a4 Merge pull request #6204 from wucm667/fix/issue-6203-kimi-concurrency-403
fix(kimi): keep concurrency-limit 403 recoverable
2026-08-25 21:28:08 +08:00
Wesley Liddick fa0685a4a3 Merge pull request #6175 from wucm667/feat/issue-6172-antigravity-token-clamp
fix(antigravity): clamp compatible token limits
2026-08-25 21:27:57 +08:00
Wesley Liddick 8040428716 Merge pull request #6201 from xuhaihan/fix/codex-session-id-affinity
fix(openai): honor Codex session-id header
2026-08-25 21:27:24 +08:00
Wesley Liddick 4ff136cfd9 Merge pull request #6193 from antTing/codex/fix-channel-monitor-v2-nullif
fix(channel-monitor-v2): fix composite platform aggregation SQL
2026-08-25 21:26:32 +08:00
Wesley Liddick 832cf4df65 Merge pull request #6208 from Wei-Shaw/fix/openai-oauth-429-quota-scheduling
fix(openai): pause OAuth accounts on exhausted quota 429
2026-08-25 20:41:41 +08:00
Long LiandCursor 77b6c5bb0c merge: sync contrib/routed-codex-model-catalog with upstream v0.1.182
Keep catalog membership on schedulable accounts and intersect advertised
capabilities across all active group members that map an alias. Preserve
upstream plugin, service-tier, and models-list body-limit changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 21:28:57 +09:00
shaw f1aadd48d5 fix(openai): pause OAuth accounts on exhausted quota 429 2026-08-25 20:25:22 +08:00
Long LiandCursor 5934981e22 test(codex): cover unschedulable catalog capability intersection
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 21:02:47 +09:00
Long LiandCursor db01fb98f8 fix(codex): keep catalog capabilities stable when accounts are unschedulable
Intersect advertised alias capabilities across all active group members
that map the model, not only currently schedulable accounts, so rate-limits
cannot widen image input or context windows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 21:01:58 +09:00
wucm667 3802268e2d fix(kimi): keep concurrency 403 recoverable 2026-08-25 19:40:39 +08:00
hank 8e60d57475 fix(openai): honor Codex session-id header 2026-08-25 17:08:27 +08:00
hank 32064d39e7 fix(openai): normalize cross-provider reasoning replay 2026-08-25 17:04:38 +08:00
hank 92a550973a fix(accounts): reauthorize OpenAI with refresh token 2026-08-25 16:52:29 +08:00
暴走的小笼包 b20f29d115 fix(channel-monitor-v2): fix composite platform aggregation SQL 2026-08-25 16:36:04 +08:00
刘佳林 a8cfe746b3 test(usage): cover user and admin reasoning effort page display
Mount the real usage tables and assert users only see the requested
effort, while admins can reveal mapped effort via column settings.
2026-08-25 15:49:04 +08:00
刘佳林 5705f4a4aa fix(usage): hide mapped reasoning effort from users
Users only see the reasoning effort they requested. Admins still see
the requested value plus the forwarded mapped value, matching the
model column's requested vs upstream split.
2026-08-25 15:36:22 +08:00
刘佳林 11ada80d58 feat(usage): 使用记录展示映射前的推理强度
用量行新增 requested_reasoning_effort,记录分组策略与模型族改写前的客户端请求值;推理强度列与模型列一致,主行显示请求值,有映射时用 ↳ 展示实际转发值。
2026-08-25 15:10:52 +08:00
wucm667 1a9898a6a4 fix(antigravity): clamp compatible token limits 2026-08-25 13:13:08 +08:00
github-actions[bot] aa2c4e8d13 chore: sync VERSION to 0.1.182 [skip ci] 2026-08-25 04:47:52 +00:00
Wesley Liddick 5a7d469622 Merge pull request #6157 from LiPu-jpg/fix/issue-6147-responses-lite-parallel
fix(openai): enforce Responses Lite parallel tool calls across account types
2026-08-25 12:31:32 +08:00
shaw 095b525360 fix(openai): pin Responses Lite parallel tool calls 2026-08-25 12:14:52 +08:00
Wesley Liddick 027d442f9b Merge pull request #6101 from jianjianai/fix/composite-group-channel-monitor-v2
fix(monitor-v2): 修复Composite分组错误事实未解析到真实账号平台导致渠道监控V2无效
2026-08-25 11:27:03 +08:00
Wesley Liddick 810d50a00f Merge pull request #6155 from HypoxanthineOvO/fix/composite-kimi-k3-routing
fix(composite): route Kimi Code K3 model IDs
2026-08-25 11:13:59 +08:00
Wesley Liddick 41d712be09 Merge pull request #5658 from william-drakemond/fix/opencode-go-usage-limit-reset
fix(openai): honor OpenCode Go usage reset durations
2026-08-25 11:05:15 +08:00
Wesley Liddick 3c714d8736 Merge pull request #6152 from ranxi2001/fix/payment-result-balance-refresh
fix(payment): refresh balance after fulfillment
2026-08-25 11:04:10 +08:00
Wesley Liddick 636d7debf9 Merge pull request #6132 from wucm667/fix/issue-6125-anthropic-cache-ttl
fix: prevent duplicate Anthropic cache TTL billing
2026-08-25 11:02:29 +08:00
Wesley Liddick 3dd717ab0d Merge pull request #5920 from wucm667/fix/issue-5884-antigravity-sonnet46
fix(antigravity): define Sonnet 4.5 to 4.6 compatibility routing
2026-08-25 11:01:34 +08:00
Wesley Liddick 1dc1b44268 Merge pull request #6149 from SipengXie2024/fix/oauth-image-verbatim-prompt
fix(openai): preserve OAuth image prompts verbatim
2026-08-25 11:01:20 +08:00
jyking 4ca86c52e3 fix(auth): 邮箱换绑增加别名与并发守卫 2026-08-25 10:40:02 +08:00
yardbirds0 3f1581b2d3 修复上游倍率探测导致账号列表整页刷新的问题 2026-08-25 10:34:46 +08:00
超级英雄吉姆 e55727d4c8 fix(openai): preserve sticky binding on capacity spillover 2026-08-25 10:27:48 +08:00
William DrakeandClaude Opus 5 b7ec3cdadf fix(openai): fail raw CC streams truncated before any terminal chunk
The raw Chat Completions passthrough returned a nil error whenever the upstream
body ended, so an upstream that cut the SSE stream mid-generation surfaced as
HTTP 200 with usage 0/0: the client kept a half-finished answer, the request was
not counted as an SLA failure, and ops saw nothing. Observed against
opencode.ai/zen/go (deepseek-v4-pro): 11 of 1346 streams in 24h recorded a first
token but no final usage, alongside a concurrent `connection reset by peer`
against the upstream edge.

Track whether the upstream ever emitted a terminal signal — [DONE], a usage
chunk, or a finish_reason — and treat a stream that ends without one as an
upstream failure. Matching only [DONE] would misfire on compatible upstreams
that EOF right after their last chunk and would drop billing for tokens that
were actually delivered, so all three count.

  - nothing written to the client yet: UpstreamFailoverError, retried on another
    account with no client-visible half stream
  - bytes already on the wire: typed openAIUpstreamStreamReadError, so the
    handler appends an SSE error frame and marks the request an SLA failure

Client cancel/disconnect keeps the previous semantics — there the upstream read
error is indistinguishable from a truncation, so partial usage is still billed
rather than blamed on the account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 18:29:37 -07:00
shaw 3b7753a8e4 chore: update sponsors 2026-08-25 08:51:58 +08:00
Jiao Ziang d5e43ef7d1 fix(openai): normalize Lite requests in WS HTTP bridge 2026-08-25 03:46:46 +08:00
Jiao Ziang d6012b0b35 fix(openai): preserve numeric precision in Lite payloads 2026-08-25 01:05:28 +08:00
Jiao Ziang 53d76ad800 fix(openai): enforce Responses Lite tool call mode 2026-08-25 00:21:29 +08:00
HypoxanthineOvO 4347e5555b fix(composite): route Kimi Code K3 model IDs 2026-08-24 23:09:30 +08:00
SipengXie2024 329b92ef04 fix(openai): preserve OAuth image prompts verbatim 2026-08-24 14:37:48 +00:00
github-actions[bot] e2d9b823f6 chore: sync VERSION to 0.1.181 [skip ci] 2026-08-24 14:35:57 +00:00
ranxi2001 eb594eefc9 fix(payment): refresh balance after fulfillment 2026-08-24 22:30:24 +08:00
Wesley Liddick 3af5443b22 Merge pull request #6116 from wucm667/fix/issue-6110-gemini-tool-schema
fix(gemini): sanitize unsupported tool schema fields
2026-08-24 22:19:07 +08:00
Wesley Liddick 7ba3e1ac52 Merge pull request #6150 from Wei-Shaw/fix/grok-upstream-user-agent
fix(grok): use official CLI user agent
2026-08-24 22:18:02 +08:00
shaw 9fb260439f fix(grok): use official CLI user agent 2026-08-24 22:14:45 +08:00
Wesley Liddick 07931bbb18 Merge pull request #6143 from akihitohyh/fix/rejected-status-strip-all
fix(openai): clear the rejected input status for the whole item type
2026-08-24 22:01:14 +08:00
Wesley Liddick 2307aa5ca7 Merge pull request #6148 from 759502416/fix/responses-lite-parallel-tool-calls
fix(openai): keep parallel_tool_calls for Responses Lite additional_tools
2026-08-24 21:49:01 +08:00
759502416andClaude Opus 5 1563db3f82 fix(openai): keep parallel_tool_calls for Responses Lite additional_tools
normalizeOpenAIParallelToolCallsWithoutTools only looked at the top-level
"tools" array, but normalizeOpenAIResponsesLiteTools moves namespace tools
into an input item of type "additional_tools" and deletes the top-level key.
A Responses Lite request that carries tools therefore looks like it has none,
and the parallel_tool_calls:false that ensureOpenAIResponsesLiteParallelToolCalls
had just pinned gets deleted on the way out.

OpenAI then applies its default of true and rejects the request:

  400 unsupported_value: "X-OpenAI-Internal-Codex-Responses-Lite requires
  `parallel_tool_calls` to be false."

Note the field cannot simply be pinned to false unconditionally: without tools
OpenAI rejects it with "'parallel_tool_calls' is only allowed when 'tools' are
specified", so the two constraints have to be honoured together.

Reuse the same tool-detection standard the Lite path already uses by adding
openAIRequestBodyHasTools, the []byte counterpart of openAIResponsesLiteHasTools,
so both sides of the repo agree on what "has tools" means.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:15:39 +08:00
akihitohyhandClaude Opus 5 e440ac48c7 fix(openai): clear the rejected input status for the whole item type
The upstream names one offending index per response, but a replayed
conversation routinely carries dozens of items of the same type, each with a
status the upstream schema does not accept. Clearing a single index per round
trip needs one retry per item, so a conversation with more than
maxOpenAIResponsesRejectedFieldRetries such items exhausts the bounded budget
and the 400 reaches the client. Reported against tool_search_output items,
where the rejection surfaced as "Unknown parameter: 'input[60].status'".

Clear the status of every input item sharing the rejected item's type in the
same pass. Items of other types keep theirs: the rejection only proves that
the rejected item's type has no status field. When the rejected item carries
no type to match on, fall back to clearing the named index alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:03:44 +08:00
wucm667 71aa6e3574 fix(antigravity): preserve explicit Sonnet 4.5 routing 2026-08-24 16:07:27 +08:00
wucm667 99ec347eaf fix(antigravity): migrate legacy Sonnet tests to 4.6 2026-08-24 15:58:47 +08:00
github-actions[bot] 03e8ab4134 chore: sync VERSION to 0.1.180 [skip ci] 2026-08-24 07:30:34 +00:00
Wesley Liddick c40edb4070 Merge pull request #6139 from xz-dev/fix/configurable-model-list-read-limit
feat(gateway): configure model list read limit
2026-08-24 15:04:50 +08:00
Wesley Liddick 7bb9c0ed7d Merge pull request #6079 from okbexx/fix/codex-analytics-account-affinity
fix(openai): scope Codex identity to OAuth account
2026-08-24 14:54:05 +08:00
Wesley Liddick 5f43696a9a Merge pull request #6121 from creamtea47/codex/feat-openai-auto-reset-credit
feat: OpenAI 重置卡按用量阈值自动使用
2026-08-24 14:39:59 +08:00
Xiangzhe 847c0c4526 feat(gateway): configure model list read limit
Add gateway.models_list_read_max_bytes with the existing 8 MiB behavior as its default, and apply it consistently to generic, Codex, and Antigravity model-list reads.

Read one sentinel byte for Codex manifests so oversized responses return an explicit bounded upstream error instead of malformed JSON.
2026-08-24 14:20:18 +08:00
Wesley Liddick 4a02d80543 Merge pull request #6136 from alfadb/fix/flaky-tool-schema-alloc-guard
fix(openai): harden flaky alloc guard in tool schema sanitize test
2026-08-24 14:17:15 +08:00
Wesley Liddick bd17411d0e Merge pull request #6129 from alfadb/feature/openai-fast-service-tier
feat(openai): Fast 档位请求校验、响应透传与按上游实际档位计费
2026-08-24 14:16:47 +08:00
Wesley Liddick c4ae3550dd Merge pull request #6119 from feeeei/feat/go1.27.0
feat(go1.27.0): 升级 Go 1.27.0,默认启用 jsonv2 ,并同步 CI/Dockerfile
2026-08-24 14:11:03 +08:00
Wesley Liddick 2f43e72bb9 Merge pull request #6109 from feeeei/main
feat(model-plaza): 模型广场增加长上下文阶梯计价显示 & 分时段计价显示
2026-08-24 14:09:56 +08:00
Wesley Liddick b8651947c3 Merge pull request #6137 from yan9651688/codex/fix-cn-anthropic-usage-billing
fix(billing): normalize CN Anthropic usage tokens
2026-08-24 14:09:45 +08:00
NellPoi 96b160d9a0 fix: 修复重置工作流共享告警码检查 2026-08-24 13:46:19 +08:00
NellPoi 6f972145b7 feat: 支持 OpenAI 重置卡按用量阈值自动使用 2026-08-24 13:28:33 +08:00
yan9651688 695ebede70 fix(billing): normalize CN Anthropic usage tokens 2026-08-24 13:05:12 +08:00
alfadb 269a409241 fix(openai): harden flaky alloc guard in tool schema sanitize test 2026-08-24 12:37:53 +08:00
feeeei 73aabc861c build: 取消 gosec G703/G704 全局排除,生产代码逐点 nolint、测试文件按路径豁免;DEV_GUIDE 同步 golangci-lint v2.13 2026-08-24 12:21:06 +08:00
feeeei 3b81776429 fix(test): grok QueryQuota 用例排除后台 /v1/models 同步请求,消除请求计数竞态
QueryQuota 返回前经 scheduleGrokObservedModelsSync 异步拉取 GET /v1/models,
该请求是否早于 upstream.snapshot() 落到 mock 取决于调度时序,三个精确断言
请求数的用例约 0.5% 偶发多出一条。新增 quotaSnapshot 只返回配额探测链路的
请求,三处断言改用它。
2026-08-24 12:02:53 +08:00
feeeei cbe258fd12 build: 升级 Go 1.27.0,同步 CI/Dockerfile 并适配 jsonv2 与 golangci-lint v2.13
- go.mod 1.26.6 → 1.27.0;backend-ci/release/security-scan 的 go version 断言、
  三个 Dockerfile 的 golang 镜像、README 徽章与 DEV_GUIDE 同步
- golangci-lint-action v2.9 → v2.13(v2.9 由 go1.26 构建,拒绝 go.mod 1.27 目标);
  新规则按最小方式处理:排除 G703/G704 污点分析(网关按配置转发/写文件,
  与既有 G304 排除策略一致)、reflect.Ptr → reflect.Pointer、
  ResetQuota 恒返回错误的 SA4023 与 OIDC EC JWK 的 SA1019 加 nolint
- ent 生成代码按 Go 1.27 默认 jsonv2 引擎重新生成:json.RawMessage 字段
  生成为同类型别名 jsontext.Value(group.model_pricing / usage_cleanup_task.filters)
- x/net v0.56 在 go1.27 下包装标准库 HTTP/2:ConfigureTransports 经
  RegisterProtocol("http/2") 打开 Protocols.HTTP2 而不再写 TLSNextProto,
  ReadIdleTimeout/PingTimeout 建连时映射为 HTTP2Config.SendPingTimeout/PingTimeout;
  keepalive 测试改断言 Protocols.HTTP2(),并补真实 HTTP/2 协商用例
2026-08-24 12:02:53 +08:00
alfadb 1591477a3e test(apicompat): adapt ChatCompletionsResponseToResponses call to upstream functionTools signature 2026-08-24 11:58:01 +08:00
alfadb e457f0fa22 fix(openai): adapt service tier observation to upstream constraints
- cc_pipeline: observe Chat Completions chunks/bodies as untyped payloads
  (empty event type) so the upstream-echoed service_tier is trusted, matching
  the upstream constraint that only terminal events and untyped bodies report
  the actual processing tier.
- tests: add model field to terminal SSE frames (observation only triggers on
  model-bearing frames) and assert response.created tier echo is ignored.
2026-08-24 11:52:48 +08:00
alfadb c0c3e1cb47 fix(openai): wire local observer service tier in WS ingress; bound handler tests
- openai_ws_forwarder_ingress: resolve billing tier from the local
  upstreamResponseModelObserver (upstream echo first) instead of the raw
  request payload, matching the HTTP->WS bridge and WS v2 forwarder.
- openai_ws_http_bridge_test: add fast-alias + upstream default case
  proving the local observer's echoed tier wins.
- handler tests: keep only invalid service_tier -> 400 (short-circuits at
  validation); valid/omitted semantics covered by the pure service-level
  validation tests, avoiding real account selection in tests.
2026-08-24 11:52:48 +08:00
alfadb f06bf181d2 feat(openai): support Fast mode service_tier across responses/chat/WS paths
- Accept fast|priority (canonical priority), flex|auto|default|scale on
  /v1/responses and /v1/chat/completions; reject unknown/empty/non-string
  with HTTP 400; omitted and null stay compatible.
- Propagate service_tier through JSON/SSE, Responses<->Chat conversions,
  fallback paths and HTTP->upstream WebSocket bridge.
- Billing prefers the upstream terminal tier; the outbound (policy-
  transformed) tier is used only when upstream omits the field.
  Explicit upstream default bills Standard even when Fast was requested.
- Pricing: Fast premium 2x Standard for gpt-5.6-sol/terra/luna and
  gpt-5.4; 2.5x for gpt-5.5; channel FastMultiplier stays authoritative.
- Live verification (official Codex 0.149.0 + gateway, HTTP & WS):
  upstream ChatGPT backend may return terminal default even when the
  account catalog advertises priority; billing follows the actual tier.
2026-08-24 11:52:48 +08:00
Wesley Liddick 7075ae0d82 Merge pull request #6133 from spongehah/feat-ops-error-detail-back-to-list-pr
feat: 运维监控错误详情支持返回列表并保留筛选状态
2026-08-24 11:40:31 +08:00
Wesley Liddick a177b88e52 Merge pull request #6122 from aeonframework/security/bump-dompurify-xss-fixes
fix(deps): bump dompurify to patch sanitizer-bypass XSS advisories
2026-08-24 11:39:25 +08:00
spongehah cfecc8d113 feat: 运维监控错误详情支持返回列表并保留筛选状态
进入单条错误详情后新增"返回列表"按钮,可回到来源明细列表并保留
筛选/分页状态,避免只能退出到运维监控总览后重新筛选。记录来源列表
类型,返回时跳过列表重开时的筛选重置。
2026-08-24 11:25:43 +08:00
Wesley Liddick c416467882 Merge pull request #6084 from wucm667/fix/issue-6057-responses-lite-parallel-tools
fix(openai): enforce serial tool calls for Responses Lite
2026-08-24 11:24:17 +08:00
feeeei f19095f96d 模型广场:分时时段行明确不含高峰倍率口径并披露叠加
- 实扣倍率为 基础 × 高峰 × 分时;时段行价格与整表一致,按不含高峰的口径展示
- 分组启用高峰时,时段行 tooltip 披露与高峰窗口重叠的部分实付再乘高峰倍率
- PlazaGroupSection 把高峰窗口描述与倍率传入价格表
2026-08-24 11:23:58 +08:00
Wesley Liddick 625f1693cb Merge pull request #6118 from akihitohyh/fix/terminal-output-item-preservation
fix(openai): rebuild streaming terminal output from the reported items
2026-08-24 11:23:51 +08:00
Wesley Liddick f25f399be0 Merge pull request #5905 from wucm667/fix/issue-5883-restore-custom-tool-alias
fix(openai): restore namespaced custom tool aliases
2026-08-24 11:23:37 +08:00
Wesley Liddick 748b84a15a Merge pull request #6081 from wucm667/fix/issue-5942-deferred-tools
fix(responses): remove orphan deferred tool flags
2026-08-24 11:23:25 +08:00
Wesley Liddick fa42c3d706 Merge pull request #6080 from alfadb/fix/cc-stream-empty-tool-call-identity
fix(openai): 剔除流式 tool_call 后续 delta 中的空 id/name
2026-08-24 11:23:12 +08:00
Wesley Liddick fb01f5df2c Merge pull request #6060 from anguobao123/codex/document-openai-force-http-fallback
fix(deploy): forward documented Gateway settings
2026-08-24 11:22:38 +08:00
Wesley Liddick 8238956799 Merge pull request #6095 from xiaxiaxaia/fix/openai-oauth-upstream-model-sync
fix(openai): sync models for OAuth accounts
2026-08-24 11:21:58 +08:00
Wesley Liddick e00a8abdd5 Merge pull request #6124 from anguobao123/codex/diagnose-openai-load-batch-exclusions
fix(scheduler): diagnose load-batch OpenAI exclusions
2026-08-24 11:21:30 +08:00
Wesley Liddick a52665d079 Merge pull request #6061 from shunwang-crypto/fix/ops-mixing-cgroup-host-memory
fix(ops): avoid mixing cgroup and host memory metrics
2026-08-24 11:20:51 +08:00
Wesley Liddick ba5b861ec0 Merge pull request #6073 from lbyxiaolizi/fix/proxy-ipv6-batch-parse
fix(proxy): support bracketed IPv6 hosts in batch proxy URL parsing
2026-08-24 11:20:34 +08:00
Wesley Liddick 817fd1214c Merge pull request #6075 from YogaSakti/fix/user-edit-allow-zero-concurrency
fix(frontend): accept unlimited (0) user concurrency in the edit dialog
2026-08-24 11:20:09 +08:00
Wesley Liddick 41f6e63799 Merge pull request #6117 from wucm667/feat/issue-6114-account-priority-column
fix(admin): show account priority by default
2026-08-24 11:19:53 +08:00
feeeei b07d85c497 模型广场:分时计价同步渠道仅工作日规则
- 阶梯表分时倍率透传渠道 weekdays_only;探针锚点显式固定在工作日
  (原 2026-01-01 恰为周四是巧合,锚点落周末会把仅工作日时段整组剔除)
- 前端时段徽章加「工作日」前缀,tooltip 说明周末全天按标准价计费
2026-08-24 11:16:00 +08:00
wucm667 bc4a9ae432 fix: prevent duplicate Anthropic cache TTL billing 2026-08-24 11:05:57 +08:00
alfadb cc894ef578 fix(openai): strip empty streamed tool-call id/name
DashScope/DeepSeek later tool_call deltas send empty id and
function.name. Clients that merge with !== undefined overwrite
the first delta's identity and dispatch unknown tool "". Drop
those empty fields on the raw Chat Completions SSE path.
2026-08-24 10:59:52 +08:00
feeeei 83d4eb6a43 模型广场:增加渠道分时段计价展示
- 阶梯表查询附带分时倍率时段:时段取自计费解析到的渠道定价,
  每个时段的倍率由计费的 resolvedChannelTimeMultiplier 在时段内取值,
  分组价卡覆盖或配置非法时自然不出现;倍率为 1 的时段不列
- 广场模型条目新增 time_pricing(时区 + 时段 + 倍率)
- 前端把分时时段展开为独立行:模型名旁标注时段,价格按时段倍率折算,
  倍率列显示生效倍率;时区与计算口径放在提示中
2026-08-24 10:50:52 +08:00
feeeei ecce0769c0 模型广场:上下文档位统一标签形态并保证升序
- 阶梯表标签由计费层统一生成:有上限的档为「≤上限」、末档为「>下限」
  (达到阈值即进高档时用 < / ≥),不再沿用渠道区间的自定义 tier_label;
  合并同价段只看单价
- 前端档位按下限升序兜底展示,无标签时按同一形态生成
2026-08-24 10:50:52 +08:00
feeeei 377d1230fc 模型广场:按计费阶梯单价表展示长上下文档位
- 新建 ModelPlazaService(持计费服务与定价解析器)承接广场聚合,
  token 模型的单价与档位全部取自 ResolveContextPricingSchedule,
  渠道选择与计费同源;图片/按次模型沿用原档位合成
- 官方参考价改走计费目录(LiteLLM → 内置兜底 → 模型策略),带官方阶梯
- DTO 增加 long_context_pricing_enabled / long_context_basis /
  official_pricing.intervals
- 前端实付与官方三列按档分行(标签只在首列,其余列按行对齐),
  缓存列按档展示写/读价,边际计价以徽章与 tooltip 标注,
  分组关闭阶梯时在头部说明
2026-08-24 10:50:52 +08:00
feeeei 6466978d2f 计费:统一 token 计费路径选择并提供上下文阶梯单价表查询
- BillingService.CalculateTokenCostForRequest 承接网关的路径选择
  (分组/渠道定价 → 平台旧长上下文规则 → 内置目录),网关改为调用该入口
- Gemini /v1beta 的 200K 边际翻倍常量从 handler 移入
  BillingService.LegacyLongContextRule,入口只声明适用
- 新增 ResolveContextPricingSchedule:沿用 Resolve 解析链收集断点
  (渠道区间边界、目录阶梯阈值、旧规则阈值),每档单价由真实计费函数
  探针差商得出,倍率/策略变更无需同步;附阶梯表 vs 计费函数的对账测试
2026-08-24 10:50:52 +08:00
Wesley Liddick 3b8a148bcf Merge pull request #6111 from feeeei/fix/request_billing
fix(billing): bill fast mode by the tier upstream actually served
2026-08-24 10:43:17 +08:00
Wesley Liddick 3e45d4e030 Merge pull request #6089 from lyen1688/feat/channel-time-pricing-weekdays
新增渠道时间段定价工作日生效规则
2026-08-24 10:21:47 +08:00
Wesley Liddick f82d32207f Merge pull request #6127 from Wei-Shaw/feat/oauth-transport-plugin-system
feat: add OAuth outbound transport plugin system
2026-08-24 10:16:05 +08:00
shaw 40aaf7b3ae fix: handle plugin route health update errors 2026-08-24 10:02:06 +08:00
shaw 391d69e086 fix: preserve initial plugin bridge requests 2026-08-24 09:51:31 +08:00
shaw 684d9efb1f fix: harden plugin runtime and UI bridge 2026-08-24 09:50:46 +08:00
shaw 26ac0498f2 test: update plugin management settings contract 2026-08-24 09:16:16 +08:00
shaw 40ea3aebad feat: add OAuth outbound transport plugin system 2026-08-24 09:03:37 +08:00
anguobao123 3fd66a33be fix(scheduler): diagnose load-batch OpenAI exclusions 2026-08-24 01:25:54 +08:00
aeonframework 4a1da29509 fix(deps): bump dompurify to patch multiple sanitizer-bypass XSS advisories
DOMPurify <=3.3.1 (and the mermaid-transitive 3.3.3) carry ~18 disclosed
sanitizer-bypass/XSS advisories, including GHSA-cj63-jhhr-wcxv
(CVE-2026-65913): with USE_PROFILES enabled, ALLOWED_ATTR is rebuilt as a
plain array and looked up via ALLOWED_ATTR[lcName], so a polluted
Array.prototype property (e.g. onclick) is treated as an allow-listed
attribute and survives sanitization -- this app calls
DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } })
in src/utils/sanitize.ts, whose output is rendered via v-html in
ImageUpload.vue's SVG upload preview.

Bumped to 3.4.14 (latest, OSV-clean) and pinned via pnpm.overrides so the
mermaid-transitive copy dedupes to the same patched version instead of
staying pinned at 3.3.3. Lockfile-only regen via pnpm 9, no other package
changes.
2026-08-23 15:43:27 +00:00
akihitohyhandClaude Opus 5 243921dc0a fix(openai): rebuild streaming terminal output from the reported items
A terminal event that arrives with an empty output was rebuilt from delta
accumulation. BufferedResponseAccumulator models only one reasoning item, one
message, and N function calls, and records no item id, status, or phase, so a
turn carrying several items collapsed into a single fabricated message: the
reasoning item disappeared, the real message id was replaced, and phase was
lost.

reconstructResponseOutputFromSSE already prefers the raw output_item.done
items over accumulation for buffered responses. The streaming path had no
equivalent because it never sees the whole body at once. Collect the raw item
of each output_item.done keyed by output_index and rebuild from those, falling
back to accumulation only when the stream reported no done item at all.

Items are stored as raw JSON, so vendor extensions and item types this gateway
does not model survive the rebuild verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:49:36 +08:00
wucm667 616df479e8 fix(admin): show account priority by default 2026-08-23 19:03:59 +08:00
wucm667 19da0f240e fix(gemini): sanitize unsupported tool schema fields 2026-08-23 18:38:11 +08:00
feeeei 75faedda95 计费:fast/priority 按上游响应实际档位只降不升计费
此前 OpenAI service_tier 与 Anthropic speed 的计费档位只看请求侧,上游
因容量把 priority/fast 静默降为 default/standard 时仍按 2x 收费。现在:

- upstreamResponseModelObserver 同时观察响应侧档位:OpenAI 只信终止事件
  与无类型 body 里的 service_tier(response.created 为请求回显,忽略),
  Anthropic 读 usage.speed;查找仅在带 model 的帧上触发
- ResolveBillingServiceTier 只降不升:响应档位比请求便宜才采纳,更贵、
  未知或缺失一律沿用请求侧,与图片尺寸归并、response_model 计费的
  "不得更贵"不变式一致
- usage_logs.service_tier 记录实际计费档位,降级写
  billing.service_tier_downgraded 审计日志;HTTP、WS 及 ws_v2 relay 全覆盖
2026-08-23 16:53:28 +08:00
Long Li fc589bce17 fix(codex): address routed catalog review feedback 2026-08-23 15:43:57 +09:00
wucm667 4eadee1074 [verified] test(openai): update responses bridge signature 2026-08-23 08:45:04 +08:00
wucm667 31d5b67baa fix(openai): restore namespaced custom tool aliases 2026-08-23 08:11:20 +08:00
jjaw 49752060fc fix(monitor-v2): resolve composite group error facts to concrete account platform
Composite groups are a routing layer. The usage-side aggregation already
maps their traffic to the concrete account platform via
usageLogEffectivePlatformExpr, but the error aggregation read
ops_error_logs.platform verbatim. Pre-routing failures on composite groups
fall back to the group platform 'composite', which is never an enabled
monitor v2 config platform, so those error facts were filtered out of
every channel monitor v2 query and composite groups looked permanently
healthy.

Join groups/accounts in the error dedup CTE and resolve the effective
platform the same way as the usage side: account platform first, then a
real recorded platform, else 'unknown'. The window delete+rewrite
recompute rewrites historical facts with the corrected key.
2026-08-23 02:33:09 +08:00
xiaxiaxaia 913ec5d74b fix(openai): sync models for OAuth accounts 2026-08-23 02:07:59 +08:00
William Drake a6b11ccced fix(openai): honor OpenCode Go usage reset durations
Recognize GoUsageLimitError responses and parse both single and composite reset durations from their human-readable messages. Keep API-key accounts rate-limited until the parsed reset instead of falling back to the short default cooldown.
2026-08-22 10:26:56 -07:00
lyen1688 77e0409f7c 新增渠道时间段定价工作日规则 2026-08-23 00:30:27 +08:00
anguobao123 6a1efda0cc fix(deploy): preserve gateway defaults in compose 2026-08-22 21:02:48 +08:00
wucm667 7498d8fdc8 fix(openai): enforce serial tool calls for Responses Lite 2026-08-22 19:40:19 +08:00
shunwang-cryptoandClaude Opus 4.8 cd05772e91 fix(ops): avoid mixing cgroup and host memory metrics
In Docker + cgroup v2 with no memory limit set, /sys/fs/cgroup/memory.current
returns a small container number while /sys/fs/cgroup/memory.max is "max".
readCgroupMemoryBytes then returned (used=<container>, total=0, ok=true).

collectSystemStats used that container "used" but, being unable to derive a
cgroup total, filled the total from the host via gopsutil. The dashboard then
computed container_used / host_total, e.g. ~60MB / 23GB ≈ 0.3% — wildly
understating real usage.

Fix: introduce resolveMemoryStats, which picks a single self-consistent
(used, total, percent) trio from ONE source. cgroup metrics are used only when
the cgroup exposes both a current usage AND a concrete limit (memory.max != max,
so total > 0); otherwise used/total/percent all fall back to the host reading.
The two sources are never mixed.

- memory.current valid + memory.max = "max"  -> all host metrics
- memory.current = 512MiB + memory.max = 2GiB -> ~25% from cgroup
- no cgroup (bare metal)                       -> all host metrics

CPU metric behavior is unchanged (cgroup attempt then host fallback).

Adds ops_metrics_collector_memory_test.go covering all branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 19:35:31 +08:00
wucm667 7a09a2eaf7 fix(responses): remove orphan deferred tool flags 2026-08-22 16:42:56 +08:00
Jarl d493ce0bb2 fix(openai): scope codex identity to oauth account 2026-08-22 16:06:04 +08:00
Yoga Sakti e0e5e45cd2 fix(openai): keep restored tool-call item IDs typed
The Responses client-tool restore raised an upstream function_call back to
custom_tool_call and tool_search_call without touching its item ID, so the
client received a custom_tool_call carrying the upstream's fc_ ID. That
combination is invalid: once the item lands in the client's history, any
later replay to an upstream that validates item IDs fails the whole request
with "Invalid 'input[N].id': 'fc_...'. Expected an ID that begins with
'ctc'", and every following message re-submits the same bad item.

Re-prefix the ID to match the type it is raised to, keeping the suffix so
the ID stays stable and unique per upstream item. IDs without a known
tool-call prefix are left alone rather than guessed at.

The lowering direction has to reverse this, because the WebSocket HTTP
bridge replays restored items back to the upstream and would otherwise lose
the upstream's own ID. normalizeLoweredFunctionItemID now maps a ctc_/tsc_
call ID back to fc_ instead of dropping it, which makes the round trip
lossless; output item IDs (ctco_/tso_) still have no function-protocol
counterpart and are still dropped. The helper was renamed from
dropInvalidLoweredFunctionItemID to match what it now does.

The stream restorer keeps the upstream ID for its own bookkeeping so later
upstream events still match the call, and emits the client-facing ID on
output_item.added/done and the custom_tool_call_input events.

Closes #6071
2026-08-22 14:03:25 +07:00
Long Li e39fce270d fix(codex): sync routed capabilities from upstream
Account model mappings decide availability, but generated Codex catalogs previously inferred capabilities from local model-name tables. Compatible upstreams can expose unknown models such as OpenCode x-preview-f-free, so reasoning levels, image input, and context limits were missing or wrong.

Sync capability metadata from the account model endpoint, enrich incomplete lists from the Models.dev provider matching the account base URL, and persist only complete snapshots. Mixed groups consume the safe intersection across schedulable accounts. When IDs sync without complete metadata, return an explicit warning and preserve the previous snapshot.

Third-party Responses providers often omit /models. If that endpoint returns 404 or 405 and the account already has concrete model mappings, use those configured upstream IDs for capability enrichment. Authentication, rate-limit, server, and network failures remain hard errors, and metadata is never guessed across providers by model name alone.

Advertise a single none choice for non-reasoning models so current Codex can select them, then omit that catalog placeholder when forwarding to compatible Responses upstreams while preserving official OpenAI request semantics.
2026-08-22 15:33:12 +09:00
Yoga Sakti 5dfad32b87 fix(frontend): accept unlimited (0) user concurrency in the edit dialog
The admin user edit modal rejected concurrency < 1, so a user whose
concurrency is already 0 could not be saved at all — the guard runs
before the request, blocking notes, password, role and RPM edits on that
user too.

Everywhere else already treats 0 as unlimited: the gateway skips slot
limiting when maxConcurrency <= 0 (ConcurrencyService.AcquireUserSlot),
the batch limits endpoint binds concurrency with min=0, and the bulk edit
modal only rejects negative values.

Reject negative and non-integer values instead, mirror the RPM field with
min/step and a "0 = unlimited" placeholder and hint, and rename the error
key to match its new meaning. Account concurrency is unchanged.
2026-08-22 13:22:21 +07:00
lbyxiaolizi ee62dfbaf1 fix(proxy): support bracketed IPv6 hosts in batch proxy URL parsing
The quick-add parser rejected every IPv6 proxy: the host group [^:]+
cannot match IPv6 literals (colons) and the pattern had no bracketed
form, so lines like socks5://[2001:db8::1]:1080 were reported invalid.

Add a bracketed-IPv6 host alternative and strip the brackets before
storing; the backend re-brackets via net.JoinHostPort when building the
proxy URL. Bare (unbracketed) IPv6 stays rejected because it is
ambiguous with host:port. Also add a regression test.
2026-08-22 14:10:01 +08:00
Long Li 5a2f542ab0 fix(codex): prefer configured models over discovery
OpenAI groups can mix OAuth accounts with Responses-compatible API key accounts whose upstreams do not implement /models. The manifest handler previously selected one account and fetched its catalog before merging model_mapping aliases, so a permanent 404 returned 502 and hid models the administrator had already configured.\n\nTreat explicit model mappings from schedulable accounts as the authoritative Codex catalog. Build that catalog locally before account selection, preserve group filtering and final-body ETags, and retain the existing upstream discovery and failover path only when no mappings exist. Composite groups keep their existing account-first catalog behavior.
2026-08-22 14:43:40 +09:00
Long Li b16ed03cad fix(codex): align routed catalogs with actual routes
Anthropic fallbacks previously reused Antigravity defaults, leaking Gemini models into Claude-only groups. Composite aliases were also emitted with generic metadata and could be scheduled to accounts that did not own the exact mapping, while generated configs could default to a model absent from the downloaded catalog.

Keep provider defaults separated, derive alias capabilities from unique upstream targets with conservative fallback, require exact mapping ownership during scheduling, filter media targets, and choose generated config defaults from the fetched catalog.
2026-08-22 14:43:40 +09:00
Long Li 3e98a5a1ad fix(composite): route exact account model aliases
Composite model catalogs expose exact account model_mapping aliases, but request routing only considered explicit routes or model-name detection. Unprefixed aliases were selectable but unroutable.

Resolve unique exact account ownership before the detector, keep explicit routes authoritative, and fail closed when an alias is claimed across platforms.
2026-08-22 14:43:40 +09:00
Long Li e471be7302 feat(codex): complete routed model catalogs
Codex treats the downloaded model catalog as a capability contract. Slug-only entries hide supported reasoning levels, can advertise invalid image inputs, and expose automatic or media-only models in the model picker. Generate complete route-aware metadata, preserve official manifests, filter non-agent models, and compute validators from the final catalog.
2026-08-22 14:43:40 +09:00
Long Li 22e1b8144a feat(gateway): expose routed Codex model catalogs
Codex model discovery expects a top-level models manifest, but Composite and other non-OpenAI groups either reached the OpenAI live-manifest handler or had no Codex-specific response at all. API key users also had no supported way to fetch that manifest and reference it from config.toml.

Generate minimal manifests from each group's effective model list while preserving the official OpenAI live path and the ordinary /models response. Add the Use Key flow for authenticated catalog download and model_catalog_json configuration without writing the API key into the downloaded file.
2026-08-22 14:43:40 +09:00
Wesley Liddick d45135d87d Merge pull request #6068 from okbexx/fix/codex-guardian-parent-affinity
fix(openai): keep auto-review on parent account
2026-08-22 13:41:42 +08:00
Wesley Liddick d29d7f8cbc Merge pull request #6065 from chinnsenn/fix/image-generation-flows
fix(openai): stabilize oauth image generation
2026-08-22 13:35:26 +08:00
Wesley Liddick fd6cd474d6 Merge pull request #5846 from lbyxiaolizi/fix/responses-chat-malformed-tool-arguments
fix(apicompat): reject malformed tool-call arguments
2026-08-22 13:35:02 +08:00
Wesley Liddick 73f6a590bf Merge pull request #5912 from xuhaihan/fix/deepseek-responses-client-tools
fix(deepseek): adapt Codex client tools for Responses
2026-08-22 13:34:49 +08:00
Wesley Liddick ffc01f9c66 Merge pull request #5864 from wucm667/fix/issue-5850-http-bridge-replay
fix(openai): avoid duplicate HTTP bridge replay
2026-08-22 13:34:32 +08:00
Wesley Liddick 6244090c1c Merge pull request #5487 from an-epiphany/fix/file-part-min
fix(apicompat): chat/completions 的 file part 转换为 Responses input_file,不再静默丢弃
2026-08-22 13:34:18 +08:00
Wesley Liddick 844b118785 Merge pull request #5938 from Hakunm/fix/google-one-model-catalog
fix(gemini): 限制 Google One OAuth 模型目录 / constrain Google One model catalog
2026-08-22 13:34:05 +08:00
Wesley Liddick 7c64a48dc2 Merge pull request #6067 from alfadb/fix/ollama-cloud-cc-reasoning-content
fix(ollama): 补齐 Ollama Cloud Chat Completions 兼容(思维字段对齐 + max_tokens 上限 clamp)
2026-08-22 13:33:53 +08:00
alfadb 86470628df feat(ollama): 对 Ollama Cloud 账号 clamp max_tokens 上限 2026-08-22 12:38:05 +08:00
Jarl fa4587041c fix(openai): keep auto-review on parent account 2026-08-22 12:14:19 +08:00
chinnsenn cb8dabc12f fix(openai): stabilize oauth image generation 2026-08-22 11:21:39 +09:00
alfadb b30651a0ad fix(ollama): 对齐 Cloud Chat Completions 思维字段为 reasoning_content
Ollama Cloud 的 raw /v1/chat/completions 把思维放在 reasoning/thinking,
DeepSeek 客户端只认 reasoning_content。仅对 openai+apikey+force_chat_completions
且具备 Ollama Cloud 信号的账号,在 raw CC 直转路径做 wire JSON 双向补齐。
2026-08-22 09:26:23 +08:00
anguobao123 e2263d2565 fix(deploy): forward documented gateway settings 2026-08-22 01:07:29 +08:00
xuhaihan cef18b4ad3 fix(deepseek): route client tools through native responses 2026-08-22 01:04:11 +08:00
xuhaihan 30ae15268f Merge remote-tracking branch 'origin2/main' into fix/deepseek-responses-client-tools 2026-08-22 00:52:33 +08:00
anguobao123 10081a8127 fix(deploy): pass force HTTP setting to containers 2026-08-22 00:39:12 +08:00
anguobao123 9f2f2738fd docs(openai): document force HTTP fallback 2026-08-21 23:43:09 +08:00
Wesley Liddick 67380eafd5 Merge pull request #5549 from zcxads666/fix/openai-capabilities-empty-set
fix(openai): 允许空 openai_capabilities 的 OAuth 账号加入文本调度
2026-08-21 21:53:27 +08:00
Wesley Liddick a53150a7cf Merge pull request #5708 from yan9651688/fix/issue-5524-model-plaza-home
fix(frontend): show model plaza entry on /home
2026-08-21 21:53:17 +08:00
Wesley Liddick 2ddda67354 Merge pull request #6049 from MokoYee/fix/openai-sticky-prefix-system
修复 OpenAI Chat 动态系统消息导致账号亲和漂移
2026-08-21 21:34:57 +08:00
zcxads666 40c26f343a fix(openai): 空 openai_capabilities 不再排除 OAuth 账号的文本调度(#5530) 2026-08-21 21:30:40 +08:00
yan9651688 d9d2854d27 Make enabled model plaza discoverable from /home
The model plaza route already supports public access, but both built-in /home headers omit its entry. Add the link to compact and default headers while keeping the existing feature and authentication settings authoritative, then cover the visibility matrix with focused component tests.

Constraint: Keep the change frontend-only and preserve router-owned access control

Rejected: Add the link to AppHeader only | /home renders its own headers and never mounts AppHeader

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep the model plaza entry gated by the existing opt-in flag and require-auth setting

Tested: HomeView focused Vitest, full frontend Vitest (223 files / 1554 tests), ESLint, vue-tsc, production build

Not-tested: Manual browser click-through against a running backend

Related: #5524
2026-08-21 21:29:39 +08:00
Wesley Liddick 219368ec6d Merge pull request #5654 from zninggo/fix/composite-video-generation
fix(gateway): Composite 分组放行视频生成端点
2026-08-21 21:27:17 +08:00
Wesley Liddick fb56bcbd07 Merge pull request #5913 from xuhaihan/fix/deepseek-responses-account-test
fix(deepseek): route Responses account tests through OpenAI probe
2026-08-21 21:27:01 +08:00
Wesley Liddick beaeaaed06 Merge pull request #6009 from HypoxanthineOvO/fix/cn-quota-refresh-affordance
fix(frontend): make CN provider quota/balance refresh affordance explicit
2026-08-21 21:25:05 +08:00
Wesley Liddick ccfced36ae Merge pull request #6011 from HypoxanthineOvO/fix/cn-anthropic-protocol-account-test
fix(accounts): route CN provider anthropic-protocol tests to the native endpoint
2026-08-21 21:24:38 +08:00
Wesley Liddick 77d8516e87 Merge pull request #5911 from xuhaihan/fix/deepseek-relay-balance-validation
fix(deepseek): reject invalid relay balance payloads
2026-08-21 21:24:21 +08:00
Wesley Liddick afa21336a5 Merge pull request #6048 from wucm667/fix/issue-5886-composite-messages-dispatch
fix: allow messages dispatch for composite groups
2026-08-21 21:24:06 +08:00
Wesley Liddick 5fc9778468 Merge pull request #6053 from wucm667/fix/issue-5899-token-refresh-lock-cpu
fix(frontend): prevent token refresh lock loop
2026-08-21 21:23:45 +08:00
wucm667 3445485ebc fix(frontend): prevent token refresh lock loop 2026-08-21 19:14:04 +08:00
MokoYee e45490a360 fix(openai): stabilize chat sticky hash across dynamic system messages 2026-08-21 18:11:18 +08:00
wucm667 68653fb2cc fix: allow messages dispatch for composite groups 2026-08-21 18:01:16 +08:00
Wesley Liddick f646a1f974 Merge pull request #5632 from 3219378872/fix/apicompat-streaming-tool-name-empty
fix(apicompat): omit empty tool name on streamed arguments deltas
2026-08-21 17:49:55 +08:00
Wesley Liddick fd24923f61 Merge pull request #5685 from Monster-DP/main
fix(gateway): 修复池模式同账号错误重试
2026-08-21 17:49:07 +08:00
Wesley Liddick b2d5ce0395 Merge pull request #5662 from yzxcj797/fix/self-referential-paths
docs: fix self-referential URLs after file moved into docs/
2026-08-21 17:47:51 +08:00
Wesley Liddick f96035462f Merge pull request #5612 from wucm667/fix/issue-5611-antigravity-paid-tier-endpoint
fix(antigravity): route paid accounts to daily endpoint
2026-08-21 17:40:39 +08:00
Wesley Liddick 4eb7630ab5 Merge pull request #5625 from sweetcornna/fix/antigravity-official-daily-endpoint
fix(antigravity): use official daily endpoint
2026-08-21 17:40:29 +08:00
Wesley Liddick 6816c63883 Merge pull request #5906 from feeeei/main
fix(test): CN 供应商额度探测 fake 加锁消除并发 append 竞态
2026-08-21 17:39:03 +08:00
Wesley Liddick 9f74eb57f4 Merge pull request #5919 from clearmann/fix/cn-native-anthropic-reasoning-effort
fix(gateway): 记录国产厂商原生 Anthropic 直通路径的 reasoning_effort
2026-08-21 17:38:52 +08:00
Wesley Liddick 98d60931c1 Merge pull request #5954 from StarryKira/codex/fix-5945-upstream
fix: preserve native max reasoning effort
2026-08-21 17:38:39 +08:00
Wesley Liddick c2572535d8 Merge pull request #6016 from YogaSakti/fix/prompt-guard-config-loaded-log-spam
fix(securityaudit): log prompt_guard.config_loaded only on change
2026-08-21 17:38:27 +08:00
Wesley Liddick 9d5171c5d1 Merge pull request #5888 from IanShaw027/fix/openai-responses-compatibility
修复 OpenAI Responses/Codex 与 Grok 多路径兼容问题
2026-08-21 17:06:00 +08:00
Yoga Sakti f6aa9dc3c8 fix(securityaudit): log prompt_guard.config_loaded only on change
ConfigManager.refreshLoop reloads the Prompt Guard config every 5s and
Reload logged config_loaded on every successful load, so an unchanged
config produced up to ~17k identical lines per instance per day and
buried real configuration changes.

Log the event only when the reload carries news: the first snapshot, a
new config version (every admin save bumps it under the advisory lock in
UpdateConfig), a flip of the global risk control gate (a separate setting
that leaves the version untouched), or a recovery from a failed reload so
the degraded-to-healthy transition stays visible.

This mirrors logInvalidTokenEndpoints, which already warns once per
change rather than on every refresh.
2026-08-21 15:51:11 +07:00
HypoxanthineOvO a749673de3 fix(accounts): route CN provider anthropic-protocol tests to the native endpoint
CN-provider accounts explicitly configured with api_protocol=anthropic
(the frontend offers per-provider presets such as GLM Anthropic) fell
through the account-test router to the generic Claude tester, which:

- appended ?beta=true, which the native passthrough deliberately avoids
  for third-party Anthropic endpoints, and
- defaulted a missing base_url to https://api.anthropic.com — sending
  the provider's API key to Anthropic instead of the provider's own
  Anthropic-compatible endpoint (guaranteed 401 plus cross-provider
  credential exposure).

Route them to a dedicated probe that mirrors the adaptive Anthropic
check: resolve the endpoint via GetAnthropicProtocolBaseURL (same
resolution as real /v1/messages forwarding, including per-platform
defaults), use the shared API-key auth header, and mark 401/403 as the
account error state.

Also fail fast with an actionable hint when the anthropic-protocol
base_url still points at an OpenAI-compatible endpoint (paas path,
version segment, or chat/completions suffix): the naive {base}/v1/messages
join would 404 (e.g. /api/paas/v4/v1/messages) without any pointer to
the actual misconfiguration.
2026-08-21 16:37:25 +08:00
heyx 2e279c81d2 fix(frontend): make CN provider quota/balance refresh affordance explicit
The quota cell used the noun label "5-hour window/weekly window" and the
balance cell the balance value itself as the click target for a manual
refresh. Both read as passive captions, so users could not discover the
manual probe and reported the feature as missing ("only OpenAI has a
refresh button").

- Split data display from the refresh action: bars/balance render as
  static rows (snapshot already paints on mount), with a dedicated
  action button below, aligned with the OpenAI "Query" and Grok
  "Probe" buttons (same icon, blue action styling).
- Label the control with a verb (cnProviders.probe: 查询 / Query) and
  give the balance cell a tooltip (it previously had none).
- Keep the tier/label layout classes and data-test hooks intact.
- Extend the quota cell spec and add a balance cell spec covering
  snapshot rendering, explicit action labeling and failure passthrough.
2026-08-21 16:23:11 +08:00
haruka d5824f6a5b fix: preserve native max reasoning effort 2026-08-21 14:24:32 +08:00
IanShaw 1429e8f714 修复 PR 5888 剩余审计问题 2026-08-20 22:01:20 -07:00
IanShaw b2b2adcf8d 修复 PR 5888 审查发现的兼容性与竞态问题 2026-08-20 21:06:48 -07:00
shaw 3548256745 chore: update gitignore 2026-08-21 10:01:29 +08:00
IanShaw 16b15e870d 修复 5888 与 5925 的同号重试语义冲突 2026-08-20 18:16:38 -07:00
IanShaw027 7e9af4c100 将 compact fallback 的流式重试改为循环,避免递归重置单次重试标志 2026-08-21 08:55:04 +08:00
IanShaw027 7c53a842e4 合并上游 main,保留 5925 的 Grok 重试上限与 5888 的协议兼容修复
同账号重试采用次数上限加 deadline;畸形 tools 在出站前删除;compaction 422 与结构化错误扫描一并保留。
2026-08-21 08:49:44 +08:00
Wesley Liddick 4033387fd4 Merge pull request #5925 from IanShaw027/fix/grok-compatibility
fix(grok): 对齐官方目录与计费,并修复容量重试、Realtime 握手与媒体兼容
2026-08-21 08:34:38 +08:00
Hakunm f98a056f75 fix(gemini): constrain Google One model catalog 2026-08-21 02:33:00 +08:00
IanShaw 1bff06ea50 修复 Grok Realtime 关闭检查与 rollup 时区断言
golangci 要求检查 Realtime 上游 Close 返回值,并删除已无调用的 heavy-model 冷却函数。分组日汇总集成测试在 UTC 晚上跨上海零点时会把水位写成 UTC 日期,将会话时区钉到 Asia/Shanghai。
2026-08-20 09:50:23 -07:00
IanShaw 2e68b10aad 完善 Grok 内容拒绝计费与媒体兼容 2026-08-20 09:12:52 -07:00
IanShaw 39aaf2fea3 收紧 Grok 容量重试与兼容性分类 2026-08-20 08:07:04 -07:00
IanShaw d78e366db5 补齐 Grok Realtime 握手失败账号冷却 2026-08-20 07:31:19 -07:00
IanShaw cca235365b 修正 Grok 默认模型测试断言 2026-08-20 07:19:51 -07:00
IanShaw c628b3eea7 让 Grok stream idle 重试上限作用于主路径 2026-08-20 07:07:55 -07:00
IanShaw f7bc1970ef 同步 Grok 默认模型回归断言 2026-08-20 07:00:42 -07:00
IanShaw 0b1f79c83c 同步 Grok 429 bridge 回归断言 2026-08-20 06:43:54 -07:00
IanShaw 2ab24a1e77 修正 Grok 429 边界与 stream idle 重试上限 2026-08-20 06:43:31 -07:00
IanShaw ab9cb69e7e Revert "修复 Grok WebSearch SSE action 兼容"
This reverts commit 726de30101.
2026-08-20 06:36:42 -07:00
IanShaw 5ae254f771 补齐 Grok CC bridge 同号重试 2026-08-20 06:25:57 -07:00
IanShaw 17c0ee3859 支持 Grok compaction 422 重试 2026-08-20 06:08:34 -07:00
IanShaw ad87ddee12 补齐 Grok CC 重试与 compaction 恢复 2026-08-20 06:08:09 -07:00
IanShaw 8db8791a73 为 Grok 普通 429 增加有限同号重试 2026-08-20 05:53:55 -07:00
IanShaw e62ec2c42f Revert "feat(429): add configurable cooldown and retry strategies"
This reverts commit 6c3edc0956.
2026-08-20 05:44:21 -07:00
IanShaw 6c3edc0956 feat(429): add configurable cooldown and retry strategies 2026-08-20 05:43:43 -07:00
IanShaw 726de30101 修复 Grok WebSearch SSE action 兼容 2026-08-20 05:41:26 -07:00
clearmann.liu 2074fe3ba8 fix(gateway): 记录国产厂商原生 Anthropic 直通路径的 reasoning_effort
kimi/zhipu/deepseek 平台分组的 /v1/messages 请求在账号 api_protocol=anthropic
时走零转换直通路径(forwardAnthropicViaNativeAnthropicEndpoint)。该路径的
两个结果构造函数(非流式/流式)从不设置 ReasoningEffort,导致客户端显式
携带 output_config.effort 或启用 thinking 时,usage_log.reasoning_effort
仍恒为 NULL,用量页推理强度列长期显示 '-'。

修复:转发前从入站 body 提取推理强度——优先 Claude 协议 output_config.effort
(NormalizeClaudeOutputEffort),缺失且 thinking 已启用时经
ApplyThinkingEnabledFallback 按 passback-required 白名单兜底为 high,与
Anthropic 网关(gateway_handler)的记录语义对齐;两个结果构造函数补上
ReasoningEffort 字段透传。

测试:新增 4 个单元测试覆盖显式 effort(非流式/流式)、thinking 兜底、
无字段保持 nil 的守卫场景。
2026-08-20 08:16:17 -04:00
wucm667 66808413df fix(openai): drop orphan replay tool calls 2026-08-20 20:10:57 +08:00
wucm667 25da02dddd fix(openai): avoid duplicate HTTP bridge replay 2026-08-20 20:10:57 +08:00
IanShaw 3243983b72 完善 Grok Realtime 与默认映射测试 2026-08-20 04:26:24 -07:00
IanShaw f7145c7508 迁移 Grok 默认模型到 4.6 2026-08-20 04:26:15 -07:00
IanShaw 0e05c61d36 修复 Grok 容量冷却与用量计费 2026-08-20 04:26:02 -07:00
xuhaihan 01a0083948 fix(deepseek): route responses account tests to OpenAI probe 2026-08-20 18:40:05 +08:00
xuhaihan b27cd76a85 fix(deepseek): adapt Codex custom tools for Responses 2026-08-20 18:18:45 +08:00
xuhaihan b0b2734b0b fix(deepseek): ignore invalid relay balance payloads 2026-08-20 18:18:45 +08:00
xuhaihan f75c4161f2 fix(deepseek): make account test links platform-aware 2026-08-20 18:18:44 +08:00
IanShaw e85348be80 调整 Grok 媒体超时与重试语义 2026-08-20 02:39:13 -07:00
IanShaw 61c2f5ad28 复用 Grok Realtime 预握手连接 2026-08-20 02:38:51 -07:00
IanShaw 611a7c8ed3 修复 Grok Realtime 预接入切号 2026-08-20 02:32:51 -07:00
IanShaw ad26172b83 完善 Grok 限流冷却与用量兼容 2026-08-20 02:32:43 -07:00
IanShaw 39485f2e28 更新 Grok 默认模型与官方计费目录 2026-08-20 02:32:22 -07:00
IanShaw027 787f875dd6 修复 Grok 孤儿控件、thinking 断言与流式 failover 的 CI 回归
畸形 tools 继续剥离孤儿 tool_choice;bare error 在临时上游失败时保持可换号;已转发的官方失败帧不再重复写入。
2026-08-20 16:21:29 +08:00
IanShaw ed4207a16f 校正 Grok 模型目录计费与工具出站 2026-08-20 01:03:29 -07:00
IanShaw 5ade094318 优化 Grok 传输超时与 Realtime 握手 2026-08-20 01:02:49 -07:00
IanShaw 953028718d 修复 Grok 错误分类与容量重试 2026-08-20 01:02:40 -07:00
IanShaw027 2ab41b92b6 修复 Grok 兼容测试与 errcheck 导致的 CI 失败
对齐 Grok ModelInput 对 function_call.status 的剥离,恢复畸形 tools 不改写 tool_choice 的行为,并检查测试中的类型断言。
2026-08-20 15:46:53 +08:00
feeeei 0117452551 fix(test): CN 供应商额度探测 fake 加锁消除并发 append 竞态 2026-08-20 15:31:35 +08:00
IanShaw 48615d5d1f Merge remote-tracking branch 'upstream/main' into fix/openai-responses-compatibility
# Conflicts:
#	backend/internal/handler/ops_error_logger.go
2026-08-20 00:31:12 -07:00
IanShaw ccb20ace88 修复 OpenAI 兼容性 PR 的 CI 回归 2026-08-20 00:19:59 -07:00
github-actions[bot] 2bc139ab52 chore: sync VERSION to 0.1.179 [skip ci] 2026-08-20 07:06:50 +00:00
shaw c0e073a798 chore: update sponsors 2026-08-20 14:56:47 +08:00
Wesley Liddick 75f88be5f7 Merge pull request #5876 from wucm667/fix/issue-5872-exclude-model-not-found-sla
fix(ops): exclude model configuration errors from SLA
2026-08-20 14:45:08 +08:00
Wesley Liddick 740f580801 Merge pull request #5842 from SavitarC/feat/adaptive-api-protocol
feat(accounts): 添加账号支持自适应API 协议
2026-08-20 14:43:41 +08:00
IanShaw d1c6456d08 合并上游最新主分支兼容修复 2026-08-19 23:15:50 -07:00
IanShaw e4f869e0c7 完善运维错误详情兼容展示 2026-08-19 23:13:47 -07:00
IanShaw c374ff2951 完善 OpenAI 网关切换与运维错误语义 2026-08-19 23:13:40 -07:00
IanShaw acce29af27 补齐 OpenAI 与 Grok 协议兼容处理 2026-08-19 23:13:29 -07:00
Wesley Liddick 752b3d8574 Merge pull request #5881 from X-T-E-R/codex/promote-grok-tool-search-discoveries
fix(grok): promote Codex tool-search discoveries into callable tools
2026-08-20 14:00:17 +08:00
SavitarC b3092145db fix(accounts): harden adaptive protocol compatibility 2026-08-20 13:57:25 +08:00
Wesley Liddick fb94fd352b Merge pull request #5834 from xuhaihan/feat/configurable-proxy-probe-targets
feat(proxy): allow configurable probe targets
2026-08-20 13:40:35 +08:00
Wesley Liddick cb7fef14c0 Merge pull request #5838 from xuhaihan/fix/admin-user-role-select-styling
fix(frontend): align admin role selector styling
2026-08-20 13:40:21 +08:00
Nai Long 7th 06fc0055cd Merge remote-tracking branch 'origin/main' into codex/promote-grok-tool-search-discoveries 2026-08-20 12:37:45 +08:00
Nai Long 7th 9ede0f7165 fix(grok): promote tool-search discoveries into callable tools 2026-08-20 12:37:39 +08:00
SavitarC 85051616f9 feat(accounts): add adaptive API protocol routing 2026-08-20 11:52:05 +08:00
wucm667 6b0ec50f24 fix(ops): exclude model configuration errors from SLA
404 model_not_found remains unchanged while ops attribution becomes routing/business-limited.
2026-08-20 10:52:29 +08:00
Wesley Liddick 1b5dc676a9 Merge pull request #5851 from IanShaw027/feat/channel-pricing-tier-multipliers
fix(billing): restore Fast tier pricing under channel overrides, add configurable multipliers
2026-08-20 10:13:17 +08:00
Wesley Liddick 394b12afdc Merge pull request #5875 from hansnow/fix/admin-platform-filter-options
fix(admin): 补全平台筛选选项
2026-08-20 10:07:46 +08:00
Wesley Liddick 2d03e40fd6 Merge pull request #5868 from X-T-E-R/codex/fix-grok-tool-search-output
fix(grok): lower Codex tool-search discovery outputs
2026-08-20 10:07:11 +08:00
hansnow 1f2a87adb0 fix(admin): 补全平台筛选选项 2026-08-20 10:00:09 +08:00
Nai Long 7th 5b2089c5a3 fix(grok): lower Codex tool-search discovery outputs 2026-08-20 06:55:47 +08:00
wucm667 b1737cc841 fix(antigravity): preserve mixed built-in chat tools 2026-08-20 00:29:33 +08:00
IanShaw cf3577a3c5 fix(openai): harden Responses compatibility 2026-08-19 09:03:37 -07:00
IanShaw d4d2c746c5 前端:修正账号长上下文开关门控 2026-08-19 06:35:11 -07:00
IanShaw d536795e93 测试:同步长上下文计费断言 2026-08-19 06:35:11 -07:00
IanShaw 26be82cc85 前端:配置渠道倍率并精简长上下文开关 2026-08-19 06:35:11 -07:00
IanShaw 7dae055f2a 计费:识别并记录 Anthropic Fast 请求 2026-08-19 06:35:11 -07:00
IanShaw 5b2a386ed7 计费:应用渠道倍率与上下文区间价格 2026-08-19 06:35:11 -07:00
IanShaw fce90ecf89 渠道定价:持久化服务层级与区间倍率 2026-08-19 06:35:11 -07:00
xuhaihan 1ab3256783 fix(proxy): format validated probe targets 2026-08-19 21:31:11 +08:00
xuhaihan ec5a345935 fix(proxy): validate configurable probe targets 2026-08-19 21:10:08 +08:00
Wesley Liddick 32a0d9ba2d Merge pull request #5837 from xuhaihan/fix/cn-provider-quota-cell-layout
fix(frontend): prevent CN quota labels overlapping bars
2026-08-19 21:05:26 +08:00
lbyxiaolizi fbc9ee626d fix(apicompat): narrow malformed tool-call handling 2026-08-19 20:49:31 +08:00
Wesley Liddick 32ecd5cc62 Merge pull request #5847 from wucm667/feat/issue-5826-cn-header-overrides
feat(accounts): support header overrides for CN providers
2026-08-19 20:28:36 +08:00
Wesley Liddick 38a5f266e9 Merge pull request #5839 from xuhaihan/test/fix-grok-api-key-placeholder
test(frontend): align Grok API key placeholder assertion
2026-08-19 20:28:19 +08:00
Wesley Liddick b7fe8ebaad Merge pull request #5762 from jaxxjj/codex/perf-usage-stats-grouping-sets
perf(usage): aggregate admin stats in one scan
2026-08-19 20:27:42 +08:00
xuhaihan d5484866fa fix(config): register proxy probe URLs default 2026-08-19 19:52:10 +08:00
Wesley Liddick 23dc9377bc Merge pull request #5844 from lyen1688/fix/grok-inline-image-view-image
fix(grok): 避免内联图片与 view_image 工具冲突
2026-08-19 19:22:08 +08:00
Wesley Liddick 68666e1f86 Merge pull request #5845 from vincenthcui/fix/openai-ws-later-turn-429-failover
fix(openai): resume later websocket turns after 429
2026-08-19 19:21:49 +08:00
lyen1688 b0cdea303a 补全 Grok 多入口内联图片工具适配
raw Chat 绕过 Responses 公共补丁,需按 Chat schema 单独移除当前轮内联图片对应的冗余 view_image;同时补充 Chat、raw Chat 与 Messages 真实出站回归测试。
2026-08-19 19:04:53 +08:00
wucm667 1b30a2d747 feat(accounts): support header overrides for CN providers 2026-08-19 19:03:39 +08:00
Vincent CuiandKinso 82cbe6aff7 fix(openai): resume later websocket turns after 429
Port the safe current-turn replay from #4622 onto current main.

Co-authored-by: Kinso <kinsolee@users.noreply.github.com>
2026-08-19 18:51:21 +08:00
lbyxiaolizi e2d9ce0cad fix(apicompat): reject malformed tool-call arguments 2026-08-19 18:48:35 +08:00
lyen1688 99a8b84701 修复 Grok 内联图片与 view_image 冲突 2026-08-19 18:36:53 +08:00
hank 63839f193a fix(frontend): align admin role selector styling 2026-08-19 16:47:51 +08:00
hank 994fbfeddb fix(frontend): prevent CN quota labels overlapping bars 2026-08-19 16:47:51 +08:00
hank b0464a9863 feat(proxy): allow configurable probe targets 2026-08-19 16:47:50 +08:00
hank f917d19d3e test(frontend): align Grok API key placeholder assertion 2026-08-19 16:47:11 +08:00
Wesley Liddick ae62854abc Merge pull request #5815 from 771373073/fix/grok46-xhigh
fix(grok): preserve xhigh effort for grok-4.6
2026-08-19 16:42:52 +08:00
Wesley Liddick 89d28037f2 Merge pull request #5822 from hansnow/fix/ws-http-bridge-followup-client-tools
fix(openai): 修复 WS HTTP bridge 多轮客户端工具回放
2026-08-19 16:42:28 +08:00
Wesley Liddick e8b53c9195 Merge pull request #5801 from MokoYee/fix/openai-chat-buffered-stream-failover
fix(openai): 修复 Chat 非流式缓冲读取错误未触发故障转移
2026-08-19 16:41:33 +08:00
Wesley Liddick 6a945b3cad Merge pull request #5817 from wucm667/fix/issue-5796-composite-new-platforms
fix(composite): support Kimi, GLM, and DeepSeek
2026-08-19 15:36:38 +08:00
hansnow fefd0d5145 test(apicompat): avoid unchecked tool history assertions 2026-08-19 15:31:18 +08:00
hansnow e4896c41d2 test(apicompat): satisfy client tool type assertions 2026-08-19 15:26:19 +08:00
shaw 499a8ee423 fix(composite): exempt resolved grok/CN targets from messages dispatch gate
sanitizeGroupMessagesDispatchFields forces AllowMessagesDispatch=false for
every non-openai platform including composite, and the gate checked only the
group platform — so composite requests resolved to grok/CN targets were
rejected 403 on /v1/messages and /v1/messages/count_tokens before reaching
the composite target whitelist, leaving the CN rollout unusable for
Claude-protocol clients.

- allowOpenAICompatibleMessagesDispatch: composite groups resolved to a
  grok/CN target get the same exemption as the standalone platforms;
  openai-resolved and unresolved targets keep requiring the switch
- resolveOpenAIMessagesDispatchMappedModel: skip the group-level dispatch
  model mapping (openai-specific gpt defaults) for composite grok/CN
  targets — model rewriting stays with account-level model_mapping
2026-08-19 15:24:26 +08:00
hansnow b94e484e23 fix(openai): preserve client tools across WS bridge turns 2026-08-19 15:19:33 +08:00
shaw aa673062e2 fix(composite): keep CN rollout on fully supported paths
- Responses WebSocket composite whitelist stays openai+grok: CN accounts
  cannot pass the WSv2 ingress transport filter and the WS HTTP bridge
  has no Responses conversion for them, so admitting CN targets only
  turns a clear policy rejection into a misleading no-available-account
- widen the responses/input_tokens and messages/count_tokens composite
  gates to the shared openai-compatible text whitelist so CN targets get
  the same local-estimate token counting as generation
- restore the Claude default-model fallback for standalone CN groups
  (admin candidates and custom models list) while keeping composite
  listings limited to CN account mapping keys
- refresh the scheduler bulk-rebuild comment and bucket capacity hints
  for the 8-platform canonical set
2026-08-19 14:58:32 +08:00
shaw bd1ccd9736 Merge remote-tracking branch 'origin/main' into fix/issue-5796-composite-new-platforms 2026-08-19 14:47:34 +08:00
Wesley Liddick 7d9c958482 Merge pull request #5810 from Pluviobyte/codex/fix-responses-input-tokens
fix(codex): handle Responses input token preflight
2026-08-19 14:43:33 +08:00
Wesley Liddick 2a5ae2b2dc Merge pull request #5816 from kingsleydon/fix/composite-codex-support
feat(composite): support Codex endpoints
2026-08-19 14:19:26 +08:00
wucm667 4d3b300a25 test(scheduler): update CN platform expectations 2026-08-19 14:05:52 +08:00
Wesley Liddick e943f817b1 Merge pull request #5729 from lbyxiaolizi/fix/responses-chat-reasoning-content-passback
fix(openai-compat): Responses→Chat 桥接按 reasoning item id 缓存回注 reasoning_content (#5520)
2026-08-19 14:03:03 +08:00
wucm667 b171bb0e4a fix(composite): support CN providers
Extend composite routing, pricing, migrations, and admin options for Kimi, GLM, and DeepSeek.
2026-08-19 13:01:25 +08:00
Kingsley 58e147fba6 feat(composite): support Codex endpoints 2026-08-19 04:40:32 +00:00
boooot 892787723f fix(grok): preserve xhigh effort for grok-4.6 2026-08-19 03:38:11 +00:00
Rain bfac49fef9 fix(codex): handle responses input token preflight 2026-08-19 11:34:18 +08:00
Wesley Liddick 82f7dd14f7 Merge pull request #5794 from Dessalines39394/fix/star-history-chart
docs: fix broken star history chart in README
2026-08-19 11:23:33 +08:00
Wesley Liddick e61595fb35 Merge pull request #5780 from Randark-JMT/fix/channel-monitor-p2
fix(monitor): P2 audit follow-ups — credential/balance semantics, capability checks, single account load, quota placeholder UI
2026-08-19 11:21:35 +08:00
Wesley Liddick c6f4fbde49 Merge pull request #5676 from Perfecto23/agent/openai-capacity-failover
fix(openai): recover message-only capacity failures before output
2026-08-19 11:11:23 +08:00
MokoYee b228b93e9c fix(openai): 修复 Chat 非流式缓冲读取错误未触发故障转移 2026-08-19 10:41:47 +08:00
wucm667 b410c39133 fix(security): add nanoid audit exception for GHSA-2v37-7h3g-55p8 2026-08-19 10:17:52 +08:00
Wesley Liddick 359fd12b2e Merge pull request #5749 from Randark-JMT/chore/remove-sora-leftovers
chore: remove leftover Sora references after platform removal
2026-08-19 09:41:25 +08:00
Wesley Liddick 214210e1b2 Merge pull request #5782 from UnlastingR/fix/cn-provider-account-test-routing
fix(accounts): route CN provider chat tests correctly
2026-08-19 09:37:45 +08:00
Dessalines39394 85cb732cdb docs: fix broken star history chart in README
The star history chart was broken due to GitHub stargazer API restrictions. Update the chart URLs in README.md, README_CN.md and README_JA.md so the chart renders again.
2026-08-18 18:47:23 +00:00
UnlastingR ac6208de16 fix(accounts): route CN provider chat tests correctly 2026-08-18 08:46:28 -04:00
Randark 2c250bfd73 fix(monitor-ui): localize the "quota" placeholder model across monitor views
纯配额模式的主模型存储值是占位符 "quota"(数据源是关联账号而非模型),
此前在管理列表主模型列、用户端监控卡片、用户端详情弹窗、运行结果弹窗
四处被当作真实模型名直接渲染,读起来像假模型。

新增 useChannelMonitorFormat.formatMonitorModel:占位符替换为本地化
「配额」标签(复用既有 monitorCommon.checkMode.quota,零新增 i18n 键),
quota_probe/probe 的真实模型名不受影响;纯匹配逻辑(如 15d/30d 可用率
按模型名回查 detail)仍用原始值。

问题定位备注:另一反馈「设置页看不到 channel_monitor_show_quota 开关」
经查为部署镜像过旧(开关随 #5761 于 08-18 合入,反馈环境容器构建于
08-15),上游代码中开关完整存在,更新镜像即可,无需改码。
2026-08-18 10:35:57 +00:00
Randark c9effc4560 fix(frontend): monitor form check-mode restore, account unbinding and mode badge
审计 P2-7 三处前端收尾:

- 切离 antigravity 时对称还原 check_mode:antigravity 强制 quota,但反向
  切走没有还原——编辑存量 antigravity 监控换平台后停留在 quota,目标平台
  未必支持。现切走时还原为 probe 并清掉占位模型 'quota'(回填值),否则
  probe 模式拿 'quota' 当探活模型(与离开 grok 清 DEFAULT_GROK_MODEL
  同一先例)。
- update 切回 probe 显式发 account_id=0:后端语义 0=解绑、null=不动,
  此前 probe 模式 payload 带 null,存量关联永远清不掉。仅 update——
  create 发 0 会落 &0 触发外键违约(API 类型注释同步写明)。
- 管理列表 provider 徽章旁并列 check_mode 徽章(checkModeLabel 此前
  定义了却无处使用):quota 系=蓝、probe=中性灰,三种模式一眼可分;
  i18n 复用既有 monitorCommon.checkMode.*,零新增键。
2026-08-18 10:28:28 +00:00
Randark e2dfb3b8cb refactor(monitor): pass loaded account through quota sources (single load per fetch)
配额 fetcher 缓存未命中时账号被加载两次:fetchUncached GetByID 一次,
下游数据源(GetUsage / CN QueryUsage / CN QueryBalance)按 ID 再各自
GetByID 一次——每次 GetByID 含 proxies/groups 联查,纯属重复劳动。

统一改为「路由前加载一次、指针直传」:
- fetcher 三个数据源接口签名改收 *Account:
  GetUsageForAccount / QueryUsageForAccount / QueryBalanceForAccount
- AccountUsageService 暴露 GetUsageForAccount 直通(getUsageForAccount
  既有逻辑零改动)
- CN 两侧服务抽 validateCodingPlanAccount / validatePayGAccount
  (加载后校验原样),新 ForAccount 入口 = NOT_CONFIGURED 守卫 → 校验 →
  singleflight → 探测;ID 入口 = load + 委托,语义不变
- singleflight key 保持 "cn_quota:<id>" / "cn_balance:<id>":ForAccount
  与 admin handler 的 ID 入口并发探测仍按账号合并
- cn_provider_balance_check_service.checkOne 刻意留在 ID 入口:调度器
  路径按 ID 取的是最新凭据,不复用可能已过期的已加载账号

校验移到 flight 之外且 ForAccount 复用同一套校验,直传无法绕过
平台/模式检查;专项测试固化(无效账号零出站请求、单次加载指针直传
require.Same)。
2026-08-18 10:28:28 +00:00
Randark c41ae19e52 fix(monitor): reject unusable quota data sources and invalid mode combos at write time
P2-2/P2-4/P2-5 from review:

- validateLinkedAccount/revalidateLinkedAccount now check
  monitorAccountQuotaCapability after the platform match, blocking combos
  that would permanently error at runtime: deepseek coding and
  custom-domain kimi coding (no quota endpoint), zhipu payg (no balance
  endpoint), anthropic/openai API-Key accounts (usage query requires
  oauth; anthropic setup-token still allowed via local estimation).
  gemini/grok/antigravity stay permissive. Quota mode surfaces
  CHANNEL_MONITOR_ACCOUNT_NOT_SUPPORTABLE on edit; probe mode silently
  unbinds (same as platform mismatch).
- applyMonitorUpdate re-runs validateCheckMode on the effective
  provider+check_mode whenever either field is patched, so a
  provider-only update can no longer persist antigravity+probe (the
  check is conditioned on provider/check_mode presence so legacy illegal
  rows can still be renamed/disabled).
- normalizeMonitorPrimaryModel only substitutes the "quota" placeholder
  for pure quota mode; quota_probe with an empty model now fails with
  MissingPrimaryModel instead of probing model="quota". The quota branch
  also moves ahead of the grok default, so grok+quota now gets "quota"
  rather than "grok-4.5" (review L-item).

Tests: capability matrix (15 cases incl. anti-over-blocking rows),
linked-account wiring, revalidate quota/probe split, provider-only
update bypass + legacy-row rename, quota_probe missing-model create,
normalize matrix.
2026-08-18 10:28:28 +00:00
Randark 1128df2592 fix(monitor): align quota-fetcher credential/balance semantics with scheduler
P2-1/P2-3 from review:

- fetchCNQuota: credential-invalid now judged by StatusCode 401/403
  (aligned with fetchCNBalance) instead of `!Success && !CredentialValid` —
  CN quota service only sets CredentialValid=true on the success path, so
  500/429/zhipu business errors were all misclassified as failed instead
  of error.
- fetchCNBalance: snapshot carries new BalanceLow flag computed with the
  scheduler's exact criterion (`!Available || allCNBalancesBelowThreshold`)
  against Gateway.CNProviders.BalanceThreshold (ctor now takes cfg; wire
  regenerated). quotaDegradedHint reports "balance low" instead of the old
  `<=0` check, so an account already paused by the scheduler (balance 5 /
  threshold 10) no longer shows green in the monitor.
- threshold helper falls back to viper default 0.5 for nil/<=0 config to
  avoid a zero-threshold regression where balance=0 stops alerting.

Tests: CN quota status-code matrix (rewrites the test that cemented the
old behavior), balance-low matrix (below-threshold / unavailable /
multi-currency healthy), threshold-from-config; PayG stubs now set
Available explicitly (zero-value trap).
2026-08-18 10:28:28 +00:00
lbyxiaolizi 3d21d61609 chore: retrigger CI(上游 flaky 测试 TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant 毫秒边界误报,与本 PR 无关) 2026-08-18 18:20:09 +08:00
github-actions[bot] 49504adc98 chore: sync VERSION to 0.1.178 [skip ci] 2026-08-18 10:03:19 +00:00
Wesley Liddick e0c48a19ed Merge pull request #5761 from Randark-JMT/feat/channel-monitor-quota-mode
feat(monitor): 渠道监控配额模式——关联账号展示用量/余额(重启 #5387)
2026-08-18 17:36:27 +08:00
Wesley Liddick dd04503e1f Merge pull request #5773 from Wei-Shaw/fix/channel-pricing-cn-platforms
fix(channels): 渠道定价支持 Kimi/智谱/DeepSeek 平台
2026-08-18 17:27:43 +08:00
Randark 5cbd0c96a2 fix(monitor-ui): 关联账号选择器改服务端搜索+回填,OpenAI 配额模式加消耗提示
- 选择器改为 /admin/accounts?platform=&search= 服务端搜索(50 条/页,
  300ms 防抖由 Select 承担),AbortController + 序列守卫防乱序覆盖
- 已绑定账号不在结果页时用 getById 回填为固定选项,删除「不在列表即
  静默清空绑定」;绑定仅在显式切换 provider 或账号确实失效时清空(带提示)
- OpenAI 平台配额模式显示 Codex 探测额度消耗提示;checkModeQuotaHint
  修正为「不发送探活请求」不再宣称「不产生 LLM 请求」
- grok spec 补齐缺失的 accounts mock;新增账号选择器 7 用例
  (加载/防抖搜索/回填保留/失效清空/提示门控/固定选项/切平台清空)
2026-08-18 09:14:44 +00:00
Randark 03c3f3b6fc feat(ui): Select 组件支持可选远程搜索(remote/loading props + search 事件)
- remote 模式始终显示搜索框且不做本地过滤,输入 300ms 防抖后 emit('search')
- 关闭/卸载时取消排队中的防抖定时器,避免尾随空 query
- loading 时空态显示加载文案;未设 remote 时行为不变(回归用例保障)
2026-08-18 09:14:31 +00:00
Randark 22df600d09 fix(channel-monitor): 配额快照识别值通道失败并加 60s 负缓存与 singleflight
- fetchUsage 显式识别 UsageInfo.Error/ErrorCode/NeedsReauth/IsBanned/IsForbidden
  (antigravity/grok 等平台失败不走 Go error 通道,此前会被误判为 operational)
- 凭据失效语义(401/403)标记 CredentialInvalid → failed 状态;
  grok quota_unknown 已知未知态豁免不判失败
- 失败快照进 60s 负缓存,避免故障/凭据失效期间以最小 15s 间隔打上游
- 同账号并发抓取由 singleflight 合并(脱离调用方 ctx,45s 总超时兜底)
2026-08-18 09:14:20 +00:00
shaw 8f6f459835 fix(channels): support kimi/zhipu/deepseek platforms in channel pricing
- ChannelsView platformOrder now includes the three CN provider
  platforms so channel pricing can be configured for them; composite
  group expansion/attachment stays limited to the five main platforms
  (matches backend isConcreteRequestPlatform and composite-routes
  target_platform validation)
- SyncPricingModels maps kimi->moonshot, zhipu->zhipu,
  deepseek->deepseek; also fixes gemini mapping to "google" which
  matched zero catalog entries (provider key is "gemini")
- Add CN platform colors to channel pricing model tag classes
2026-08-18 17:12:01 +08:00
Wesley Liddick 58ccea4eaa Merge pull request #5767 from hansnow/fix/ws-http-bridge-custom-tools
fix(openai): 补齐客户端工具终止事件恢复
2026-08-18 16:21:50 +08:00
hansnow c253bd2c72 fix(openai): restore client tools in terminal events 2026-08-18 15:48:02 +08:00
Wesley Liddick 26cb59df05 Merge pull request #5764 from hansnow/fix/ws-http-bridge-custom-tools
fix(openai): 补齐 WS HTTP bridge 的客户端工具适配
2026-08-18 15:47:59 +08:00
Wesley Liddick 58ea46e894 Merge pull request #5661 from wucm667/fix/issue-5659-openai-custom-tools
fix(openai): restore API-key custom tool calls
2026-08-18 15:47:50 +08:00
Wesley Liddick 685455985b Merge pull request #5765 from IanShaw027/feat/grok-from-personal-main
feat(grok): 用量条补齐本站 24h/7d/30d 聚合,并隐藏空预付与 0 限额已用
2026-08-18 15:35:42 +08:00
Wesley Liddick 1ed3b6aef6 Merge pull request #5760 from spongehah/feature/unify-codex-outbound-identity
fix(Fingerprint): 将 Codex 非推理出站身份统一到推理解析链
2026-08-18 15:35:34 +08:00
Wesley Liddick f211a630c8 Merge pull request #5720 from tamseno/fix/invitation-code-toctou-race
fix(auth): make invitation code consumption atomic with user creation
2026-08-18 15:35:16 +08:00
Wesley Liddick 37732dcd34 Merge pull request #5725 from tamseno/fix/gemini-include-server-side-tool-invocations
fix(gemini): support includeServerSideToolInvocations in GeminiToolConfig
2026-08-18 15:35:01 +08:00
yaxin a341239596 fix(fingerprint): align credential-face identity with the real client and de-drift models version
- Replace ApplyCodexCanonicalIdentity with CodexCanonicalAuthIdentity /
  ApplyCodexCanonicalAuthIdentity: the credential face (auth.openai.com
  token exchange / refresh / PAT whoami) now sends the originator +
  canonical User-Agent pair and no version header, matching codex-rs
  default_headers(); the version gate (#3901) only exists on the
  /backend-api/codex inference face. whoami keeps its original header
  shape (originator + UA) with the canonical UA source.
- Token exchange and refresh send the full pair instead of a bare UA,
  eliminating the half-identity (UA without originator) combination no
  real client ever emits.
- Codex models manifest: the Version header now follows the client's
  own client_version when it is valid and >= the upstream floor (same
  source as the query param, restoring the pre-refactor consistency),
  falling back to the canonical version otherwise; the query param
  keeps its verbatim passthrough contract.
- Drop the now-unreferenced openAICodexProbeVersion constant and its
  vacuous consistency assertions; probes resolve their version through
  resolveCodexOutboundIdentity at runtime.
2026-08-18 15:20:08 +08:00
yaxin 1ba92449c7 fix(gemini): wire includeServerSideToolInvocations into the typed transform path
The struct field alone never reached the wire: the raw passthrough
pipeline is covered by enableMixedGeminiToolInvocations (#5711), but
TransformClaudeToGeminiWithOptions builds GeminiToolConfig from scratch
and never set the flag, so gemini-* models entering through the Claude
format gateway could still hit the upstream 400 from issue #5709.

- Set IncludeServerSideToolInvocations=true when the built tool
  declarations mix functionDeclarations with googleSearch, matching the
  raw-path injection semantics.
- Replace the marshal-roundtrip-only test with behavior tests that
  drive TransformClaudeToGeminiWithOptions: mixed tools set the flag,
  function-only and web-search-only requests leave it unset.
2026-08-18 15:05:54 +08:00
yaxin 9617775f9a fix(repo): tolerate ErrTxStarted for tx-bound clients and harden test stubs
- user_repo.create(): keep the TxFromContext fast path, but restore
  tolerance for dbent.ErrTxStarted in the self-owned-transaction branch.
  ent's Client.Tx only inspects the driver type, so a repository built
  from a tx-bound client (client-injected transactions, e.g. the
  integration fixture testEntTx + tx.Client()) hits ErrTxStarted; reuse
  that client instead of failing. Fixes the two red integration tests in
  allowed_groups_contract_integration_test.go.
- createUserAndClaimInvitation: roll back via defer (matching the OAuth
  registration precedent) so a panic inside the transaction cannot leak
  the connection.
- settingRepoStub: guard call counters and state with a mutex; the new
  concurrency regression test exercises it from multiple goroutines and
  the unsynchronized counters were flagged by -race.
2026-08-18 15:02:25 +08:00
IanShaw027 fd42d3722d fix: hide Grok prepaid and used/limit when they are empty
Skip the prepaid chip without a positive balance, and skip used/limit when monthly limit is 0.
2026-08-18 14:55:08 +08:00
Wesley Liddick 1870b58c1d Merge pull request #5721 from lyy0709/codex/bulk-openai-settings
fix(openai): complete bulk account settings
2026-08-18 14:53:55 +08:00
IanShaw027 269fbcac0e feat: Grok 用量条补齐本站 24h/7d/30d 聚合
把已有的本地窗口统计接到 Free 24h 与付费 7d/30d 进度条,避免只显示官方百分比。
2026-08-18 14:50:57 +08:00
hansnow 7e579cb28d fix(openai): adapt client tools in WS HTTP bridge 2026-08-18 14:42:30 +08:00
Randark ebcae03afd fix(lint): 补齐 setting_public.go 与配额 fetcher 测试的 gofmt 对齐 2026-08-18 06:29:43 +00:00
jaxxjj a9514a68d2 perf(usage): aggregate stats in one scan
Reduce filtered admin usage statistics from four scans to one GROUPING SETS query so every breakdown shares the exact same filters. Add concurrent expression indexes for requested and upstream model filters on large usage_logs tables.
2026-08-18 14:26:19 +08:00
Wesley Liddick 938f1868ae Merge pull request #5714 from wucm667/fix/issue-2733-current-main
fix(ops): avoid single-insert fallback after batch failure
2026-08-18 14:11:42 +08:00
Wesley Liddick 4d19836189 Merge pull request #5716 from wucm667/fix/issue-2695-current-main
fix: skip expiry reminders without SMTP config
2026-08-18 14:06:32 +08:00
Wesley Liddick c792b44bb2 Merge pull request #5712 from xiaobo121388/main
feat: add Ollama Cloud usage query action
2026-08-18 14:05:27 +08:00
Randark 7376f4a48c fix(lint): gofmt 对齐 + 移除未使用的 isSupportedProvider
- 三个文件补 gofmt 规范对齐(结构体尾部注释、const 块字段对齐)
- isSupportedProvider 在 validateProvider 改用能力集合后无引用,删除
2026-08-18 05:57:41 +00:00
Wesley Liddick 1ea4150bf0 Merge pull request #5581 from wucm667/fix/issue-5574-passthrough-model-discovery
fix(gateway): align passthrough model discovery
2026-08-18 13:56:02 +08:00
Wesley Liddick ed2da82396 Merge pull request #5711 from wucm667/fix/issue-5709-antigravity-tool-config
fix(antigravity): preserve mixed Gemini tool config
2026-08-18 13:55:24 +08:00
Wesley Liddick 6259940ef2 Merge pull request #5669 from feeeei/main
feat(openai): OpenAI Team 联动熔断
2026-08-18 13:54:32 +08:00
Wesley Liddick baaf59d417 Merge pull request #5755 from feeeei/feat/gemini
fix(gemini): Skipped 错误策略对齐 OpenAI,上游 4xx 不再硬改 500
2026-08-18 13:54:06 +08:00
Wesley Liddick c0325d24f9 Merge pull request #5759 from o2e/codex/fix-codex-usage-probe-model-clean
[codex] 修复部分账号 Codex 额度查询 400
2026-08-18 13:53:49 +08:00
Wesley Liddick c76ff55997 Merge pull request #2148 from feitianbubu/pr/ops-error-detail-support-custom-time
feat: ops 错误详情弹窗支持自定义时间区间
2026-08-18 13:52:34 +08:00
Wesley Liddick 5c12960285 Merge pull request #4005 from feitianbubu/fix/header-role-i18n
fix(ui): localize user role label in app header
2026-08-18 13:52:15 +08:00
Wesley Liddick f6d6613ec6 Merge pull request #4006 from feitianbubu/fix/native-controls-dark-mode
fix(ui): adapt native form controls to dark mode via color-scheme
2026-08-18 13:51:57 +08:00
Wesley Liddick 5dfac8bffa Merge pull request #4049 from feitianbubu/fix/dashboard-token-cache-breakdown
fix(dashboard): include cache tokens in token card breakdown
2026-08-18 13:51:42 +08:00
Wesley Liddick 632b4f4faa Merge pull request #4053 from feitianbubu/fix/announcements-empty-state-copy
fix(announcements): use proper empty-state copy instead of error message
2026-08-18 13:51:21 +08:00
Wesley Liddick 3d9a0a71bd Merge pull request #4057 from feitianbubu/fix/ops-sla-zero-window
fix(ops): show neutral SLA card when window has no requests
2026-08-18 13:51:09 +08:00
Wesley Liddick 1a3ecd2b94 Merge pull request #5004 from wucm667/fix/issue-4990-deferred-tool-cache-control
fix(claude): strip cache control from deferred tools
2026-08-18 13:50:57 +08:00
Wesley Liddick 8532344745 Merge pull request #5715 from wucm667/fix/issue-3917-current-main
fix(frontend): isolate AccountsView helper data loading
2026-08-18 13:50:39 +08:00
Wesley Liddick cddb03c0f1 Merge pull request #5609 from wucm667/fix/issue-5607-auth-pricing-snapshot
fix: preserve group pricing in auth snapshots
2026-08-18 13:50:13 +08:00
Wesley Liddick a7a321232f Merge pull request #5567 from wucm667/fix/issue-5563-anthropic-sse-overload
fix(gateway): handle Anthropic SSE overload errors
2026-08-18 13:49:59 +08:00
Randark 302a10b886 test(monitor-ui): 配额视图渲染与开关门控用例
- 新增 MonitorQuotaView.spec:tier window/label 的 i18n 映射与未知
  token 透出、阈值配色(≥90 红/≥75 黄/其余绿)、进度条宽度钳制、
  多币种余额与单余额回退、plan_level 徽章、错误截断与失败态不渲染 tiers
- 新增 MonitorCard.quota.spec:latest_quota 受系统开关门控,
  无快照或开关关闭时不渲染
- 既有工厂/断言补 check_mode、account_id;provider 网格断言 4→8
2026-08-18 05:10:10 +00:00
Randark a20e1c00cc feat(monitor-ui): 配额模式表单、用量快照视图与 8 平台支持
管理端:
- MonitorFormDialog:新增检查方式三态分段控件(探活/配额/探活+配额),
  provider 网格扩展至 8 平台(+antigravity/kimi/zhipu/deepseek),
  国产平台默认 endpoint 预填、antigravity 强制 quota-only;配额模式
  显示关联账号选择器(复用 adminAPI.accounts.list 按平台过滤),
  隐藏 endpoint/api_key/模型等探活专属字段
- 新建共用 MonitorQuotaView:用量窗口进度条(阈值/样式对齐账号页
  CNProviderQuotaCell)、多币种余额、plan_level 徽章与错误态;
  window/label 机器 token 走 i18n,未知 token 原样透出(前向兼容)
- 列表主模型单元格内联 latest_quota;运行结果弹窗逐行展示配额;
  筛选栏/模板管理/格式化 composable 同步 8 平台(配色对齐
  utils/platformColors.ts);ProviderIcon 搬运 4 家官方 SVG

用户端:
- MonitorCard 支持 latest_quota,渲染受 channel_monitor_show_quota
  系统开关(isChannelMonitorQuotaVisible)控制,服务端剥离为第一道防线

设置与 i18n:
- 管理设置页新增「向用户展示渠道用量/余额」开关(默认关闭);
  zh/en 全量补充 providers/checkMode/quota 文案
2026-08-18 05:09:58 +00:00
o2e 16e4f7ecc3 修复 Codex 额度探针模型兼容性 2026-08-18 13:08:28 +08:00
spongehah bb6c3b4f6a fix: unify Codex OAuth outbound identity onto the inference resolver
Token exchange, PAT whoami, models, probes, and pre-writes now follow
the same UA/version chain as Codex inference instead of hardcoded
codex-cli/0.91.0 or compile-time constants.
2026-08-18 13:03:22 +08:00
Randark c51fd7d0b3 test(channel-monitor): adapt checker body test to 3-arg normalizeMonitorPrimaryModel 2026-08-18 04:51:06 +00:00
Randark 7ab6d3db66 test(channel-monitor): quota mode unit/integration/migration coverage
- fetcher:海外/CN coding/CN payg 分派、账号缺失、凭据失效标记、
  TTL 命中与失败不缓存、UsageInfo→tiers 全窗口归一、状态推导矩阵
- quota mode:RunCheck 三分派(quota 单行 / quota_probe 仅挂主行 /
  probe 不变)、配额失败不翻探活状态、校验矩阵、关联账号复核
  (quota 报错 probe 自动解绑)、quota→probe 切换强制重填 key、
  Duplicate 空明文重加密
- settings:channel_monitor_show_quota 缺省关闭 + 仅 "true" 开启
- 迁移内容断言(仿 176 grok 迁移测试)
- repo 集成测试:check_mode/account_id 往返、quota JSONB 回读、
  探活旧行 NULL 兼容(testcontainers PG 实跑迁移 226)
- 修复 Fetch 在 nil receiver 上的 panic(缓存查询先于原 nil 守卫)
2026-08-18 04:51:02 +00:00
Randark 41344c20ff feat(monitor): wire quota fetcher & expose check_mode in handlers
- handler DTO: create/update 接收 check_mode/account_id,provider oneof 扩至 8 家,
  endpoint/api_key 改为 omitempty(条件必填下沉 service 校验);
  monitor/checkResult/historyItem 响应透传 check_mode/account_id/quota
- 用户端 latest_quota 由 channel_monitor_show_quota 控制,关闭时服务端剥离
- wire: NewChannelMonitorQuotaFetcher 以具体服务类型收参(窄接口包内保留供
  stub),ProvideChannelMonitorRunner 注入后 SetQuotaFetcher
2026-08-18 04:14:15 +00:00
Randark 6a6fd304f6 feat(settings): channel_monitor_show_quota public setting (default off)
- 新增公开设置 key(迁移 226 已插入默认 false),控制用户端监控页
  是否展示配额/余额;管理端不受影响
- 解析 fail-closed:仅字面 "true" 视为开启(对齐 available_channels_enabled
  语义,而非 hide_throughput 的 fail-open)
- 全链路贯通:domain key 常量 → setting_public(公开读取 + ChannelMonitorRuntime
  .ShowQuota + 注入 payload)→ settings_view/parse/update → handler DTO
  (admin/user 响应 + admin 更新请求)→ api_contract_test 两个 wantJSON 块
2026-08-18 04:06:18 +00:00
Randark c44711ac98 feat(channel-monitor): quota mode service layer (fetcher + dispatch + repo)
- 常量:MonitorProvider{Antigravity,Kimi,Zhipu,Deepseek}、MonitorCheckMode 三态、
  配额缓存/告警阈值参数;新增 check_mode 相关业务错误
- 校验:monitorProviders(8)/probeCapableProviders(7) 集合替代 adapter 表判定,
  validateCheckMode 矩阵(antigravity 仅 quota);quota 模式 primary_model
  默认占位 "quota",endpoint/api_key 条件必填
- checker:kimi/deepseek 复用 /v1/chat/completions、zhipu 走
  /api/paas/v4/chat/completions;merge 黑名单与 replace 校验同步扩容
- ChannelMonitorQuotaFetcher:窄接口聚合账号侧三个用量服务 + 账号加载,
  归一为 domain.MonitorQuotaSnapshot;成功快照 5min TTL 缓存防配额端点风暴;
  Fetch 永不返回 error(失败降级 Success=false 快照);状态推导
  operational/degraded(≥90% 或余额耗尽/账号未关联)/failed(401/403)/error
- RunCheck 按 check_mode 分派:quota 单行结果、quota_probe 探活+配额挂主行、
  probe 不变;Update 组合校验 + 关联账号复核(probe 自动解绑失效账号);
  Duplicate 拷贝新字段且 quota 模式空明文重加密(防 runner 误停摆)
- repo:Create/Update/entToServiceMonitor 透传 check_mode/account_id;
  InsertHistoryBatch 写 quota;ListLatestForMonitorIDs 裸 SQL 加 quota 列 +
  scanMonitorQuota JSONB 解包(NULL 安全);ListHistory 透传 quota
2026-08-18 04:03:13 +00:00
Randark 615e6901ea feat(channel-monitor): add quota mode schema (migration 226 + ent)
- 迁移 226:provider CHECK 扩到 8 平台(monitors + request_templates)、
  channel_monitors 加 check_mode/account_id(FK ON DELETE SET NULL)、
  channel_monitor_histories 加 quota JSONB、插入公开设置
  channel_monitor_show_quota(默认 false)
- ent schema 同步:provider 枚举 +4、endpoint 去 NotEmpty(quota 模式存空串)、
  新增 check_mode/account_id 字段、history 加 quota JSON 快照
- domain 新增归一化配额快照类型 MonitorQuotaSnapshot/Tier/Balance
  (放 domain 是因为 ent schema 需要引用,service 会造成 import cycle)
2026-08-18 03:50:01 +00:00
feeeei ab0fcd1a0e fix(gemini): Skipped 错误策略对齐 OpenAI,上游 4xx 不再硬改 500
ErrorPolicySkipped(池模式、或自定义错误码未命中)原来在响应写出上
自成一派:v1beta 原生把上游 4xx 硬改 500 后原文透传,/v1/messages
硬传 500 进映射(客户端拿到 502)。下游网关据此把请求级错误当可重
试的服务端故障反复换号,耗尽后改写成 All available accounts
exhausted(2026-08-17 gemini 生产事故链)。现对齐 OpenAI 路径语义:

- Skipped 只豁免账号状态标记,不豁免换号:可 failover 状态码一律
  返回 UpstreamFailoverError(poolModeSkippedFailoverError 泛化为
  skippedErrorPolicyFailoverError,同账号重试标记仍仅池模式携带)
- 池模式的不可 failover 4xx 保真:v1beta 原码+原文透传(新
  writeGeminiNativeUpstreamError 与 ErrorPolicyNone 共用同一写出,
  并补记 ops 事件),/v1/messages 与 chat completions 按真实状态
  码映射
- 自定义错误码未命中且不可 failover:三路径统一 500 + "Upstream
  gateway error" 固定文案,上游细节仅记 ops 错误日志
- 400 属确定性请求错误:mapped 写出回传脱敏后的上游 message,客户
  端可据此定位非法字段
2026-08-18 11:24:39 +08:00
Wesley Liddick 8869775ed3 Merge pull request #5636 from feitianbubu/fix/proxy-status-expired-i18n-key
fix(i18n): add missing expired key to account status block used by proxy list
2026-08-18 10:06:56 +08:00
Wesley Liddick f66a42c40e Merge pull request #5697 from feitianbubu/fix/ops-error-distribution-legend-labels
fix(admin): show category labels in ops error distribution legend
2026-08-18 10:05:39 +08:00
Wesley Liddick 7d633f5fc3 Merge pull request #5742 from heathermhuang/codex/fix-grok-response-model-audit
fix: normalize Grok response model audit aliases
2026-08-18 10:03:26 +08:00
Wesley Liddick b2d1c3859a Merge pull request #5738 from okbexx/fix/codex-identity-snapshot
fix(openai): make Codex convergence identity consistent
2026-08-18 09:57:05 +08:00
Randark 7e45634df9 chore: remove leftover Sora references after platform removal
PR #1463 removed the Sora platform, but some references survived:

- README/README_CN/README_JA kept the 'Sora status (temporarily
  unavailable)' sections and gateway.sora_* docs that the removal PR
  never touched.
- deploy/config.example.yaml still documented ~130 lines of sora_*
  gateway keys, the top-level sora: direct-client/storage block, and
  token_refresh.sync_linked_sora_accounts - none of which map to any
  field in the config structs anymore.
- The OIDC login PR (02a66a01c, branched off pre-removal main and
  merged 4 days after #1463) re-added the dead
  PublicSettings.SoraClientEnabled field, which no code ever sets.
- A release sync (748a84d87) re-introduced sora i18n keys that the
  later i18n split (d9e514f98) faithfully carried into
  locales/{zh,en}/admin/{overview,settings}.ts. No component references
  any of these keys.

This drops all of the above. Pure deletions, no behavior change.
2026-08-18 00:51:30 +00:00
Wesley Liddick 5253bb72b0 Merge pull request #5737 from lyen1688/feat/channel-time-pricing
功能:支持渠道模型分时倍率定价(支持峰谷定价)
2026-08-17 22:38:41 +08:00
shaw 6bf335965a merge main 并修复与 #5730 的语义冲突
main 侧 #5730 新增的 openai_gateway_cn_fixes_test.go 按旧 11 参签名调用
calculateOpenAIRecordUsageCost;本分支为该函数新增了第 12 个参数
pricingAt。文本无冲突但 test build 会失败,此处按本分支对同类测试
调用点的既有处理方式补传 time.Time{}。
2026-08-17 22:27:22 +08:00
Heatherm Huang c46d07ca07 fix: normalize Grok response model audit aliases 2026-08-17 21:15:12 +08:00
Wesley Liddick ab28a2d10b Merge pull request #5730 from Wei-Shaw/fix/cn-provider-group-entry-and-correctness
fix: 打通 CN 平台分组入口并修复五项功能缺陷(调度闸门 403/计费误计/断开漏记/count_tokens/403 处置)
2026-08-17 20:29:54 +08:00
Jarl 6793d5ac85 fix(openai): make Codex convergence identity consistent 2026-08-17 20:25:28 +08:00
lyen1688 9f24a55305 功能:支持渠道模型分时倍率定价 2026-08-17 19:45:07 +08:00
lbyxiaolizi 401dd43b4b fix(apicompat): 链式工具调用回放本轮 reasoning_content
codex 0.147.0 真实 resume 历史复现:DeepSeek 每轮只在开头流式产生一次
reasoning,reasoning → call A → output A → call B 的链式调用中 call B 前
没有 reasoning item,其 assistant 消息缺 reasoning_content,DeepSeek
thinking mode 400 整个历史。

buildChatMessagesFromItems 新增 lastTurnReasoning:记录本轮最近一次
reasoning,跨 tool output 存活,仅被 user 侧 item 清除;assistant 消息
(文本或 tool_calls)在 pendingReasoning 为空时回放本轮 reasoning。
2026-08-17 18:51:17 +08:00
shaw c38c5beef8 fix(i18n): CN 余额单元格误引 grokBalance 键致渲染原始 key
CNProviderBalanceCell 无快照占位标签引用 admin.accounts.grokBalance,
但该键实际嵌套在 admin.accounts.usageWindow 下,未命中时 DeepSeek/Kimi
payg 账号的用量窗口列直接显示原始键名。

- cnProviders 块新增自有占位键 balance(zh: '余额 --' / en: 'Balance --'),
  单元格改引该键
- 用 esbuild 实际加载模块对 zh/en 全部 admin 语言包做键路径深度比对:
  除本处外 CN 相关键(静态 14 个 + 动态拼接 accountMode/apiProtocol 系列)
  双语全部对齐;唯一存量漂移是 claudeMaxSimulation 块(zh 嵌套于
  modelRouting 下/en 为 groups 直属),全源码零消费方属死键,不在本次
  范围内处理
2026-08-17 17:44:33 +08:00
shaw 10c8b70203 fix(cn-providers): 修复 CN 分组五项功能缺陷(调度闸门/计费/断开漏记/count_tokens/403)
对已合并 PR #5666 + 分组入口放行后的全量功能审计发现的 P0/P1 修复,
全部先经代码与厂商文档实证再实施:

1. /v1/messages 调度闸门(P0):sanitizeGroupMessagesDispatchFields 对非
   openai 平台恒置 AllowMessagesDispatch=false,而闸门豁免名单只有 grok,
   CN 分组经正常途径创建后恒 403——原生 Anthropic 直通(Claude Code 主用例)
   完全不可达。修复:闸门对 CN 与 grok 同语义豁免;count_tokens 处的内联
   裸检查统一走同一 helper;ResolveMessagesDispatchModel 对 CN 早退,避免
   openai 专属的 gpt-5.x 默认映射发给 CN 上游。

2. 计费候选链(P0):候选链兜底含客户端原始模型名,配合 getFallbackPricing
   的 claude→Sonnet 统一兜底,映射的 CN 模型无价时 CN 流量会按 Claude 原价
   (数倍~数十倍)静默误计,且 usage 日志显示 claude-* 名无从察觉。修复:
   CN 账号的 claude-* 候选仅在显式分组/渠道定价时放行;候选全滤空时按
   ErrModelPricingUnavailable 走零成本+告警落账(顺带修复原空候选错误会
   丢弃整条 usage 记录的次生问题)。

3. 断开/中断漏记(P0,#5148 对齐,惠及 openai 平台):messages/responses/
   chat_completions 三个 handler 的错误路径此前在 err!=nil 时丢弃携带的
   部分 result——客户端断开排水后的完整 usage 被丢,payg 上游照常计费而
   平台漏记(anthropic 网关早有同修复,openai 网关缺失)。修复:错误路径
   result 非空时照常提交 usage;failover 错误恒 result=nil 无重复计费。
   Responses×anthropic 流式转换器同时改为断开后继续排水至流自然结束
   (末尾 message_delta 的 output_tokens 不再丢),finalize 帧补工具名反转
   与客户端工具还原、仅在客户端仍连接时写出。

4. count_tokens(P1,证据修正):经实证三家 Anthropic 兼容层均无
   /v1/messages/count_tokens(DeepSeek 官方文档无此端点且注明
   anthropic-version 被忽略;OpenModel 标注该端点 Anthropic only),
   anthropic 协议转发上游=常态 404,且错误处置缺模型上下文会把不计费的
   探测放大成整账号停调。修复:CN 全协议一律本地 tiktoken 估算(与 Grok
   同方案),删除上游转发死代码。

5. 403 处置(P1):CN 此前落入通用 handleAuthError,单次 HTML 403(CDN/
   代理拦截页)即永久禁用,且 403 在 failover 集里会逐账号重放连环禁用
   整组。修复:CN 与 openai 同口径——HTML 豁免 + 3 次累计 + 临时冷却。

新增回归测试 8 项:闸门豁免(含 openai 仍受控断言)、CN 调度映射空返回、
候选过滤三态、空候选零成本落账、断开排水 usage 完整性、HTML-403 零处罚、
结构化 403 首次临时停调。handler/service 全包测试通过。
2026-08-17 17:28:53 +08:00
shaw 7cdca9e495 feat(groups): 放行 kimi/zhipu/deepseek 平台分组创建入口
PR #5666 引入 CN 平台后,路由/调度/前端类型均已支持 CN 平台分组,但分组
创建入口两头缺失:后端 Create/UpdateGroupRequest 的 platform oneof 白名单
与前端 GroupsView 平台选项都没有三平台,导致 CN 账号「无可用分组」、整条
流量链路不通(composite 不能作为替代:CN 不可为 composite 路由目标)。

- group_handler.go: 两处 oneof 加 kimi/zhipu/deepseek;composite 路由目标
  白名单有意不动(DetectModelPlatform/isConcreteRequestPlatform 均无 CN 分支)
- GroupsView: platformOptions/platformFilterOptions 补三项;两处徽章配色链
  按 platformColors.ts 色系补 CN 分支
- i18n: admin.groups.platforms 补 kimi/zhipu/deepseek 键(zh/en),缺键时
  分组徽章/GroupRPM/RateMultipliers 弹窗/ChannelsView 会渲染原始 key
- GroupBadge: badgeClass/labelClass 补 CN 配色
- 新增表驱动测试:9 平台 Create/Update 全放行、非法值(别名/大小写/空格)
  全拒绝、composite target 对 CN 保持拒绝的守卫
2026-08-17 17:28:51 +08:00
wucm667 971544570d test(antigravity): check tool config assertions 2026-08-17 17:11:33 +08:00
lbyxiaolizi 612436a5a7 fix(openai-compat): Responses→Chat 桥接按 reasoning item id 缓存回注 reasoning_content
修复 #5520:Codex 经 force_chat_completions 桥接到 DeepSeek thinking 上游时,
历史中的 encrypted-only reasoning item(summary 为空 + 不透明 encrypted_content,
远程 compaction / 跨会话恢复后常见)取不出明文,后续 assistant 消息缺
reasoning_content,DeepSeek 400 "The reasoning_content in the thinking mode
must be passed back to the API",客户端仅看到通用 502。

reasoning item 的 id 一定会被客户端回传,以其为 key 做服务端缓存:

- GatewayCache 新增 Set/GetReasoningContent(Redis,默认 TTL 7 天)
- 响应侧:流式扫 response.output_item.done、非流式扫 output,把 reasoning
  全文按 item id 写缓存;客户端断连后 drain 期间用 detached ctx 仍会写完
- 请求侧:apicompat 新增 ResponsesToChatCompletionsRequestWithOptions 与
  ReasoningContentByID 钩子,encrypted-only item 查缓存补回 pendingReasoning;
  缓存 miss/出错一律 fail-open 维持原行为
- 自愈:历史里带明文 summary 的 reasoning item 顺手刷新缓存,覆盖 Redis
  flush / 跨实例漂移

测试:apicompat(命中恢复/miss 保持原样/明文优先)、service 端到端(流式
写缓存、请求侧回注+自愈)、repository miniredis 存取。
2026-08-17 16:48:36 +08:00
Tamseno 3c3bb2fa19 fix(gemini): support includeServerSideToolInvocations in GeminiToolConfig
- Add IncludeServerSideToolInvocations field to GeminiToolConfig to prevent dropping client tool settings.
- Fix HTTP 400 error when mixing built-in tools (e.g. Google Search) with function calling on Gemini 3.6/3.7 models.
- Add serialization/deserialization unit test TestGeminiToolConfig_IncludeServerSideToolInvocations.

Fixes #5709
2026-08-17 15:23:49 +08:00
Wesley Liddick e330c243a8 Merge pull request #5666 from Randark-JMT/feat/cn-providers-kimi-zhipu-deepseek
feat: 国产供应商多协议支持(Kimi/Zhipu/DeepSeek 原生 Anthropic 直通 + DeepSeek Responses)与配额/余额监控
2026-08-17 14:55:20 +08:00
lyy0709 8d82bb069c fix(openai): expose bulk account settings 2026-08-17 13:56:16 +08:00
Tamseno b8642ef674 fix(auth): make invitation code consumption atomic with user creation
RegisterWithVerification checked CanUse() and then marked the code used in
two separate, non-transactional steps; the second step's failure was
swallowed ("invitation code mark failure does not affect registration").
Concurrent registrations with the same invitation code could all pass the
check and each create an account, turning a one-time invitation code into
an unlimited account factory (TOCTOU race).

Fix:
- AuthService: create user and claim the invitation code inside one DB
  transaction (createUserAndClaimInvitation). The claim reuses
  redeemRepo.Use's conditional UPDATE (WHERE status='unused'); losers are
  rejected with INVITATION_CODE_INVALID and their transaction (including
  the user insert) is rolled back. No-code registration path unchanged.
- userRepository.create: explicitly join an outer ent transaction via
  TxFromContext instead of relying on Client.Tx returning ErrTxStarted
  (ent's Tx never inspects the context, so the old reuse branch was dead
  code and user inserts always committed in their own transaction,
  leaving orphan users behind when the outer transaction rolled back).

Regression tests:
- unit: concurrent register with one invitation code must succeed exactly
  once (8 goroutines -> 1 success, 7 x INVITATION_CODE_INVALID)
- integration: outer-tx rollback removes user and releases the claim;
  commit persists both atomically
2026-08-17 13:49:46 +08:00
lyy0709 76b70b1685 fix(openai): validate bulk account settings 2026-08-17 13:34:41 +08:00
xiaobo 8a0a76aeb5 Merge pull request #1 from xiaobo121388/codex/add-ollama-usage-query
feat: add Ollama Cloud usage query action
2026-08-16 21:40:44 -07:00
wucm667 cb5e03a720 fix(antigravity): preserve mixed Gemini tool config 2026-08-17 12:36:58 +08:00
xiaobo 9aac3b73f7 add Ollama usage query action 2026-08-16 21:33:53 -07:00
Randark e728545382 style: gofmt 迁移测试注释(CI golangci-lint gofmt) 2026-08-17 04:26:22 +00:00
Randark 4b667ccd45 fix(review): 处理 PR #5666 四个阻断项(B1-B4)
- B1: 移除根目录 docker-compose.yml(个人镜像测试产物混入)
- B2: 新增迁移 224 放宽 user_platform_quotas CHECK 至 8 平台,补
  BulkInsertInitial 国产平台集成测试与迁移内容断言测试
- B3: CC/Responses×anthropic 四个上游读循环接入间隔超时泵
  (gateway.stream_data_interval_timeout,默认 180s):上游挂住 SSE
  不发数据也不断连时结束排水/组装、关闭 resp.Body 归还连接池位,
  排水超时仍按已累计 usage 返回(与 messages 主路径同语义)
- B4: 配额/余额探测发起前过 cnValidateProbeURL(与网关转发同一套
  security.url_allowlist 策略),被拒时零出站、API key 不离开本机
2026-08-17 04:07:06 +00:00
wucm667 1977810cf2 fix(frontend): isolate account helper data loading 2026-08-17 11:04:50 +08:00
wucm667 79c2eb5020 fix: skip expiry reminders without SMTP config 2026-08-17 10:59:43 +08:00
wucm667 5f19433103 fix(ops): avoid single-insert fallback after batch failure 2026-08-17 10:52:57 +08:00
Wesley Liddick 396a9d1130 Merge pull request #5703 from InCerryGit/fix/clarify-openai-fast-policy-ui
fix(frontend): clarify OpenAI Fast/Flex policy rules
2026-08-17 09:40:53 +08:00
Wesley Liddick 51016bb03a Merge pull request #5690 from luckydududu/fix/dockerfile-golang-image-follows-gomod
fix(docker): Go 构建镜像跟上 go.mod 的 1.26.6(否则任一 Dockerfile 构建都直接失败)
2026-08-17 09:30:48 +08:00
InCerryGit 22fc0cdbf5 fix(frontend): clarify OpenAI Fast/Flex policy rules 2026-08-16 23:49:21 +08:00
feitianbubu cb7841d85c fix(i18n): add missing expired key to account status block used by proxy list 2026-08-16 18:48:49 +08:00
feitianbubu e8ff2017c0 fix(admin): show category labels in ops error distribution legend 2026-08-16 18:04:52 +08:00
Lucky 11e1e22882 fix(docker): bump Go builder image to 1.26.6 to match go.mod
89d826be2 raised backend/go.mod to `go 1.26.6` and updated the three CI
workflows' version assertions, but left the Go builder image in all three
Dockerfiles pinned at 1.26.5. Since the official golang images set
GOTOOLCHAIN=local, the toolchain is not auto-downloaded and any image build
fails hard at `go mod download`.

CI does not catch this: the workflows build with actions/setup-go, not with
these Dockerfiles.

Also extend the Go-upgrade checklist in DEV_GUIDE.md, which listed only the
CI files -- that omission is why the Dockerfiles were missed.
2026-08-16 06:17:43 +00:00
myy_321 b1e60ba453 fix(gateway): 修复池模式同账号错误重试
- 透传上游错误处理结果,保留池模式重试判定\n- 对可重试状态码设置同账号重试标记\n- 补充 Chat Completions 与 Responses 的 429 回归测试
2026-08-16 08:29:12 +08:00
Perfecto a600dd1c0a Merge remote-tracking branch 'origin/main' into agent/openai-capacity-failover 2026-08-15 22:54:27 +08:00
Perfecto 5390647989 fix(openai): complete request-scoped capacity recovery 2026-08-15 22:54:21 +08:00
github-actions[bot] baeac1f3de chore: sync VERSION to 0.1.177 [skip ci] 2026-08-15 13:40:21 +00:00
Perfecto c3063e01a4 fix(openai): recover message-only capacity failures 2026-08-15 21:30:56 +08:00
Randark 901a0439f1 feat: 国产供应商一等支持(Kimi/Zhipu/DeepSeek 多协议 + 配额/余额监控)
后端:
- 协议凭证维度 credentials[api_protocol] ∈ chat_completions(默认)/anthropic/responses(deepseek)
- /v1/messages 零转换直通原生 Anthropic 端点(kimi/zhipu/deepseek),CC/Responses
  入站交叉组合走 apicompat 双向转换链(responses/chat_completions anthropic-native 转发器)
- count_tokens:anthropic 协议透传原生端点;其余 CN 协议本地 tiktoken 估算
- Coding Plan 额度探测(5h/weekly 滚动窗口)+ payg 余额探测(kimi/deepseek),
  deepseek 双币种 CNY+USD 明细,任一币种达标不停调
- 周期任务 [CNBalance] 并发探测 + 预算随工作量放大;响应式 429 冷却到最早窗口
  重置点;余额不足可恢复临时停调;智谱 CREDIT_LIMIT 不污染窗口解析
- CC→anthropic 流式客户端断开后继续排水上游保住 usage 计量

前端:
- 创建/编辑弹窗 account_mode + api_protocol + base_url 联动预设(含 watcher 竞态防护)
- 用量单元格:kimi/zhipu coding 显示 5h/weekly 窗口,kimi/deepseek payg 显示余额,
  多币种并列展示;探测失败保留快照;挂载自动探测 5min 去抖
- 调度阈值设置面板补 kimi/zhipu 平台(对齐后端 AllowedSchedulingThresholdPlatforms)
2026-08-15 10:37:51 +00:00
Wesley Liddick 073e92d171 Merge pull request #5668 from Wei-Shaw/fix/codex-turn-state-and-fingerprint-optin
fix(openai): Codex 回合状态回传、v2 压缩探测与指纹收敛改为 opt-in
2026-08-15 17:10:18 +08:00
feeeei d677d67dda feat: OpenAI Team 联动熔断
OpenAI OAuth 账户收到 402 deactivated_workspace(ChatGPT Team 工作区被停用)时,
将同一 Team(credentials.chatgpt_account_id 相同)的其余 active 账户一并置为
error 并进程内立即熔断,让调度瞬间切走,避免流量继续打到已死的兄弟账户。

- 触发条件仅限 402 + detail.code == deactivated_workspace;通用 402 与 401 行为不变
- 调用点两处:fastpath 各类早退之前 + HandleUpstreamError 顶部,进程内 60s 按 Team 去重
- 逐账户 SetError(错误信息标注触发账户),单账户失败不中断其余
- 默认生效,无配置开关
2026-08-15 16:48:40 +08:00
shaw 4d9fedee20 fix(test): check type assertions in turn-state provenance tests
errcheck runs with check-type-assertions enabled, so the single-value
assertions on the provenance map values fail lint.
2026-08-15 16:44:30 +08:00
shaw fce41e318f fix(openai): make Codex fingerprint convergence opt-in and cover passthrough
Default codex_fingerprint_mode to off. v0.1.175 treated a missing key as
"session", so upgrading silently rewrote installation/session/thread/turn/
window identifiers for every existing OAuth account that had never configured
this field. The quota regressions in #5555, #5556 and #5582 line up with that
version boundary, with A/B reports that rolling back to v0.1.173 restores
quota. Convergence is now explicit opt-in (#5610).

Only accounts that never set the field change behaviour; explicit off /
device / session / full keep working exactly as configured. That required
flipping the persistence condition in all three account modals from
"!== 'session'" to "!== 'off'": the old rule deleted the key when it equalled
the default, which after the flip would have silently discarded an
administrator's explicit opt-in to session.

Also extend convergence to the passthrough path, which previously left client
identifiers untouched:

- resolve the ids once in forwardOpenAIPassthrough and rewrite
  client_metadata on the raw bytes (gjson extract + sjson splice) because
  passthrough is a hot path that must not fully unmarshal multi-MB bodies;
  a shared core keeps the raw and map variants from drifting
- both request builders apply the staged ids at the same relative position
  (after session isolation, before identity enforcement) so headers and body
  share one id set and turn_id stays consistent
- stage the ids unconditionally, including nil: a failover from a converged
  account to an off account must not leave the previous account's ids behind
2026-08-15 16:35:26 +08:00
shaw 8ae6d8f67e fix(openai): send session-level beta features and probe native compaction v2
OpenAI sunset the legacy unary /responses/compact endpoint (404, #5598,
#5624), so the account "compact probe" in the admin UI kept failing even for
healthy accounts, and the beta-feature negotiation header was only attached
to compaction turns.

Beta features (codex-rs session/mod.rs build_model_client_beta_features_header
+ client.rs build_responses_headers): the header is a session-level constant
attached to every /responses request, the WS handshake and /responses/compact.
Enumerating FEATURES shows no Experimental feature is enabled by default, so a
default install sends exactly "remote_compaction_v2". Mirror that:

- OAuth requests without a client-declared header get the default shape, so we
  no longer produce a "header only on compaction turns" pattern real Codex
  never emits (#5586 chains that strip the header)
- a client-declared header is preserved as-is: non-empty without v2 means the
  user disabled the feature and the gateway must not rewrite that
- native v2 turns (compaction_trigger in body) always ensure v2 is present
- non-OAuth upstreams keep the compaction-turn-only behaviour
- the WS injection sits outside the client-header copy block so prewarm and
  turn handshakes cannot land in different pool compatibility buckets

Compact probe now exercises native v2 (streaming /responses +
compaction_trigger) instead of the dead endpoint. Success requires an actual
compaction output item — scanning output_item.done/added, the terminal
response.output[] and the whole-JSON fallback — so a 2xx that silently drops
the trigger is reported as unsupported (the "got 0 items" class, #5478,
#5648). Probe identity is now UUID-shaped and applies the account's
convergence, matching real traffic on the same endpoint.
2026-08-15 16:35:08 +08:00
shaw 8219dcfc87 fix(openai): relay x-codex-turn-state and guard cross-account echo
Codex captures x-codex-turn-state from /responses SSE, /responses/compact
JSON and the WS handshake (codex-api sse/responses.rs, endpoint/compact.rs),
then echoes it back on later requests of the same turn. The HTTP path dropped
it because the header is not in the generic response allowlist, while the WS
path already relayed it — an inconsistency that broke the protocol chain.

Relay it explicitly at every commit point instead of widening the global
allowlist (which would leak it into Anthropic/Gemini responses):

- streaming, non-streaming and SSE-to-JSON handlers relay it, clearing any
  value left over by a previous failover attempt when upstream sends none
- under the first-output guard the header is only staged; provenance is
  recorded when applyAttemptResponseHeaders actually writes it, because a
  first-output timeout discards the staged headers and the client never
  receives that blob
- record (api key + client session) -> minting account, TTL-bounded with an
  opportunistic sweep, and strip echoes known to come from another account
  before they go upstream. Stripping only: injection is the Claude bridge's
  job. Same-account or unknown provenance passes through unchanged.
2026-08-15 16:34:47 +08:00
Wesley Liddick 1d3b9665c8 Merge pull request #5641 from InCerryGit/fix/issue-5624-remote-compaction-v2
fix(openai): preserve remote compaction v2 responses endpoint
2026-08-15 13:46:31 +08:00
yzxcj797 98c7b0e88e docs: fix self-referential URLs after file moved into docs/ 2026-08-15 11:42:47 +08:00
wucm667 44ef88f659 fix(openai): restore API-key custom tools 2026-08-15 11:02:59 +08:00
Wesley Liddick c204d33b09 Merge pull request #5649 from lyen1688/feat/group-usage-daily-rollups
feat: 优化分组用量统计
2026-08-15 09:00:16 +08:00
zninggo 1e1798d90d fix(gateway): Composite 分组放行视频生成端点
视频状态(/videos/:id)与内容下载(/videos/:id/content)都已放行
PlatformComposite, 唯独创建端点仅放行 PlatformGrok. Composite 分组
用户配好模型路由后可查询可下载, 唯独创建 404.

补齐放行判断, 与另外两个视频端点保持一致; 其他平台行为不变.
2026-08-15 01:20:26 +08:00
lyen1688 45dcce0e49 修复:隔离分组用量仓储测试时区
分组用量仓储测试原先固定恢复上海时区,覆盖集成测试包初始化的 UTC,并导致后续今日统计多算。统一保存并恢复测试进入前的时区,避免 unit 和 integration 用例相互污染。
2026-08-14 23:48:03 +08:00
lyen1688 89d826be29 修复:解决分组用量 PR 的 CI 失败
恢复分组时区测试进入前的全局时区,避免污染高峰计费和支付租约测试。同步升级 Go 1.26.6 与 nanoid 3.3.18,清除后端和前端安全门禁。
2026-08-14 23:25:31 +08:00
lyen1688 cb7b03795d feat: 优化分组用量统计 2026-08-14 22:34:43 +08:00
InCerryGit a8b9ea22b7 fix(openai): separate native and legacy compaction routing 2026-08-14 18:18:53 +08:00
InCerryGit 9662cff2e7 fix(openai): preserve remote compaction v2 responses endpoint 2026-08-14 16:23:52 +08:00
风起 bafd2e293f fix(apicompat): omit empty tool name on streamed arguments deltas
The responses->chat streaming converter re-emits the tool name as an empty
string on every function_call_arguments.delta because ChatFunctionCall.Name
has no omitempty. OpenAI-compatible clients accumulate the name from the
first tool-call delta, so the trailing "name":"" overwrites it and the
call fails dispatch (unknown tool ""). Empty names also break the next
round-trip upstream (missing field 'name').

Fixes #5631
2026-08-14 06:32:38 +00:00
Cornna 21c07e8351 fix(antigravity): use official daily endpoint
Align the daily Cloud Code host with the current Antigravity client so requests do not use the legacy sandbox hostname.
2026-08-13 21:31:42 -07:00
li 3c5553e253 fix(responses): 网关合成的 Responses 对象补齐 created_at,修复严格客户端反序列化失败
严格的 Responses 客户端(Rust serde 系,如 Codex / Grok CLI)把 created_at 声明为
必填字段,缺失即 `missing field 'created_at'` 直接读不出响应。

apicompat.ResponsesResponse 此前根本没有这个字段,六个构造点全部不写:
Chat→Responses 与 Anthropic→Responses 两座桥的非流式结果、response.created、
终止事件,以及两处手写的 response.failed 帧。

两个流式 state 早就在 New*State() 里采集了 Created 时间戳,只是没有出口——
反方向(Responses→Chat)每个 chunk 都老老实实发 created,正方向直接丢在地上。

原生 Responses 透传走 gjson/sjson 字节级改写,保留上游原样,不受影响;
本次只补合成路径。

Fixes #5601
2026-08-14 11:05:29 +08:00
li 901a77cfb1 fix(apicompat): Anthropic→Chat 桥回传工具调用的 thinking,修复 DeepSeek 多轮 400
/v1/messages 打到只支持 Chat Completions 的 OpenAI 兼容上游时,
anthropicAssistantToChatMessages 把历史 assistant 消息里的 thinking 块整块丢弃。
DeepSeek 的 thinking mode 要求产生工具调用的 reasoning_content 随该 assistant
消息回传,缺失即 400 —— 表现为「单轮正常,一进多轮工具对话必现失败」。

这些 thinking 块本来就是同一个文件的出站方向
(chatMessageToAnthropicBlocks) 用上游 reasoning_content 生成的,客户端只是
原样回传:桥自己造出来的东西,回来时被自己丢掉。

兄弟路径 Responses→Chat (buildChatMessagesFromItems 的 pendingReasoning) 早已
处理同一约束。本次把同一策略补到漏掉的这条桥上,作用域严格对齐:reasoning 只挂
到带 tool_calls 的 assistant 消息,纯文本轮次维持现状。

Fixes #5528
2026-08-14 10:33:47 +08:00
wucm667 e7a3c1202e fix(antigravity): route paid accounts to daily endpoint 2026-08-14 02:40:18 +08:00
wucm667 674570ca17 fix: preserve group pricing in auth snapshots 2026-08-14 00:25:55 +08:00
wucm667 a288bab73a fix(gateway): align passthrough model discovery 2026-08-13 12:40:06 +08:00
Wesley Liddick fbfdcef818 Merge pull request #5573 from IanShaw027/fix/grok-long-context-and-media-fallback
fix(grok): 长上下文阶梯不再被 OpenAI 账号开关否决,媒体 ID 不继承文本价
2026-08-13 10:32:07 +08:00
github-actions[bot] 0e82efe489 chore: sync VERSION to 0.1.176 [skip ci] 2026-08-13 01:46:47 +00:00
IanShaw027 e215c98c2c fix: 账号页自动刷新偏好改为模块初始化时恢复
onMounted 再读一次会覆盖已在 setup 阶段恢复的偏好。
2026-08-13 09:22:41 +08:00
IanShaw027 e29b93a1fb fix: 未知 Grok 文本兜底排除 image/video/audio 等媒体族
带版本号的媒体 ID(grok-2-image-1212、grok-5-video)会被
grok-N* 白名单当成文本,错吃 4.5 token 价。vision 多模态
仍按 token 计;HasIdentifiedTokenPricing 也不放行这些 ID。
2026-08-13 09:22:38 +08:00
IanShaw027 fd82dfd52d fix: Grok 长上下文只跟分组开关,不受 OpenAI 账号开关否决
openai_long_context_billing_enabled 是 OpenAI 账号设置,Grok
账号无法打开。按账号硬传 false 会让分组开关失效,官方 200k
阶梯和渠道多档区间全部塌到第一档。非 OpenAI 不再传账号门闩。
2026-08-13 09:22:34 +08:00
wucm667 76a13a5a8d fix(gateway): handle Anthropic SSE overload errors 2026-08-13 02:43:03 +08:00
wucm667 c91fbeb0c0 chore: remove unrelated test refactors 2026-08-11 16:57:39 +08:00
wucm667 c3fc4331c1 fix(lint): make compatibility test assertions fatal 2026-08-11 16:57:39 +08:00
wucm667 e087f1b736 fix(lint): use fatal pointer assertions in usage tests 2026-08-11 16:57:39 +08:00
wucm667 145b0ac35e fix(lint): make remaining pointer assertions explicit 2026-08-11 16:57:39 +08:00
wucm667 5ea03c178d fix(lint): resolve remaining nil dereference warnings 2026-08-11 16:57:39 +08:00
wucm667 1afb8264e5 fix(lint): use require.NotNil for staticcheck SA5011
Replace if-then-t.Fatal nil checks with testify require.NotNil so
staticcheck can prove the pointer is non-nil before field access.
Fixes SA5011 in payment_handler_test, parse_test, and
openai_images_incomplete_test.
2026-08-11 16:57:38 +08:00
wucm667 2321c0e771 chore: retry CI checks 2026-08-11 16:57:38 +08:00
wucm667 0b35370a7a fix(claude): support top-level deferred tools 2026-08-11 16:57:38 +08:00
wucm667 9c36b75a7d fix(claude): strip cache control from deferred tools 2026-08-11 16:57:38 +08:00
Pengap 4d4a0be1ad fix(apicompat): chat/completions file part 不再被静默丢弃,转换为 Responses input_file
/v1/chat/completions → Responses 转换层此前只处理 text 和 image_url 两种
content part,type:"file"(PDF 附件)被静默丢弃:请求返回 200、模型正常
回答,但 prompt 里没有文件(prompt_tokens 只剩纯文字)。

现将 file part 映射为 Responses API 的 input_file(filename/file_data/
file_id 透传),与同网关 /v1/responses + input_file 实测可用的格式一致。
无 file_data 且无 file_id 的空 file part 跳过,与空 image URL 行为一致。

参考上游 PR #2497(因混入无关改动未合并)。
2026-08-10 13:59:20 +08:00
feitianbubu 0d5e3ca9b0 fix(ops): show neutral SLA card when window has no requests 2026-07-11 19:48:08 +08:00
feitianbubu 35e8ba2a3e fix(announcements): use proper empty-state copy instead of error message 2026-07-11 18:28:21 +08:00
feitianbubu a6d868f27b fix(dashboard): include cache tokens in token card breakdown 2026-07-11 17:18:25 +08:00
feitianbubu 7d796f1119 fix(ui): adapt native form controls to dark mode via color-scheme 2026-07-11 00:08:26 +08:00
feitianbubu 3bff4b64b1 fix(ui): localize user role label in app header 2026-07-10 23:43:16 +08:00
feitianbubu 5e72deb7d4 feat: ops 错误详情弹窗支持自定义时间区间 2026-05-01 14:16:49 +08:00
1275 changed files with 134497 additions and 10018 deletions
+3
View File
@@ -49,6 +49,9 @@ coverage/
.env.*
!.env.example
# 本地闭源插件目录可能包含发布签名私钥,绝不能进入 Docker 构建上下文。
/plugins/
# Local config
config.yaml
config.local.yaml
+7
View File
@@ -35,3 +35,10 @@ exceptions:
mitigation: "Proxy configuration not user-controlled; upgrade when axios releases fix"
expires_on: "2026-07-10"
owner: "security@your-domain"
- package: nanoid
advisory: "GHSA-2v37-7h3g-55p8"
severity: high
reason: "Custom generator with size=0 not used; all nanoid calls use default size or positive values"
mitigation: "No zero-size custom generators in codebase; upgrade when nanoid releases fix"
expires_on: "2026-10-13"
owner: "security@your-domain"
+4 -3
View File
@@ -17,6 +17,7 @@ jobs:
/bin/bash -n deploy/apple-container.sh
/bin/bash deploy/tests/apple-container-test.sh
/bin/sh deploy/tests/docker-compose-security-test.sh
/bin/sh deploy/tests/docker-compose-gateway-env-test.sh
/bin/sh deploy/tests/docker-runtime-resources-test.sh
/bin/sh deploy/test-caddyfile-cache.sh
@@ -32,7 +33,7 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.5'
go version | grep -q 'go1.27.0'
- name: Unit tests
working-directory: backend
run: make test-unit
@@ -72,10 +73,10 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.5'
go version | grep -q 'go1.27.0'
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.9
version: v2.13
args: --timeout=30m
working-directory: backend
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
- name: Verify Go version
run: |
go version | grep -q 'go1.26.5'
go version | grep -q 'go1.27.0'
# Docker setup for GoReleaser
- name: Set up QEMU
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.5'
go version | grep -q 'go1.27.0'
- name: Run govulncheck
working-directory: backend
run: |
+12 -1
View File
@@ -1,4 +1,5 @@
docs/claude-relay-service/
# 本地杂项文档、测试数据和外部项目副本
/docs-local/
.codex
# ===================
@@ -136,13 +137,23 @@ docs/*
!docs/PAYMENT_CN.md
!docs/ADMIN_PAYMENT_INTEGRATION_API.md
!docs/ASYNC_IMAGE_TASKS.md
!docs/BATCH_IMAGE_MVP.md
!docs/COMPOSITE_GROUPS.md
!docs/PLUGIN_DEVELOPMENT.md
!docs/channel-monitor-v2-safe-defaults.md
!docs/legal/
!docs/legal/*.md
!docs/screenshots/
docs/screenshots/*
!docs/screenshots/mobile-account-actions-menu.png
.serena/
.codex/
frontend/coverage/
aicodex
output/
.codegraph/
# Vitest / Vite cache at repo root
.vite/
# 本地闭源插件开发目录及构建产物不进入 Sub2API 仓库
/plugins/
+5 -5
View File
@@ -34,8 +34,8 @@
### 开发工具
```bash
# golangci-lint(CI 用 v2.9,本地建议装同一版以免版本差异带来的噪音)
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9
# golangci-lint(CI 用 v2.13,本地建议装同一版以免版本差异带来的噪音)
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13
# pnpm (前端包管理)
npm install -g pnpm
@@ -47,13 +47,13 @@ npm install -g pnpm
| Workflow | 触发条件 | 检查内容 |
|----------|----------|----------|
| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.9 |
| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.13 |
| **security-scan.yml** | push, pull_request, 每周一 | govulncheck + gosec + pnpm audit |
| **release.yml** | tag `v*` | 构建发布(PR 不触发) |
### CI 要求
- Go 版本必须是 **1.26.5**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.26.5'`。升级 Go 时要同时改 `backend/go.mod` 和 `backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,否则 CI 会在版本校验步骤直接失败。
- Go 版本必须是 **1.27.0**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.27.0'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。
- 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml`
### 本地测试命令
@@ -203,7 +203,7 @@ go test -tags=integration ./...
**解决**:
```bash
cd backend
go generate ./ent # 重新生成 ent 代码
go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期)
git add ent/ # 生成的文件也要提交
```
+1 -1
View File
@@ -8,7 +8,7 @@
# =============================================================================
ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.26.5-alpine
ARG GOLANG_IMAGE=golang:1.27.0-alpine
ARG ALPINE_IMAGE=alpine:3.21
ARG POSTGRES_IMAGE=postgres:18-alpine
ARG GOPROXY=https://goproxy.cn,direct
+1
View File
@@ -1,6 +1,7 @@
.PHONY: build build-backend build-frontend test test-backend test-frontend test-frontend-critical
FRONTEND_CRITICAL_VITEST := \
src/i18n/__tests__/localeKeyCompleteness.spec.ts \
src/api/__tests__/client.spec.ts \
src/api/__tests__/tokenRefresh.spec.ts \
src/api/__tests__/channelMonitorV2.spec.ts \
+59 -37
View File
@@ -4,7 +4,7 @@
# Sub2API
[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/)
[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/)
[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/)
@@ -58,6 +58,11 @@ Please read the following carefully before using this project:
<td>Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for sub2api users: if you register via <a href="https://aigocode.com/invite/SUB2API">this link</a>, you'll receive an extra 10% bonus credit on your first top-up!</td>
</tr>
<tr>
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
<td>Real GPT-5.6 series at 3% of OpenAI pricing — <a href="https://codex-everywhere.com">CodexEverywhere</a> is democratizing access to frontier models for developers worldwide. We believe in transparency and honesty, with model quality verified by active community oversight for months. USD and crypto friendly. Start with a free $20 trial at <a href="https://codex-everywhere.com">codex-everywhere.com</a>.</td>
</tr>
<tr>
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
<td>Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through <a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus - Premium AI Accounts & Top-ups</a>, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF)</td>
@@ -91,12 +96,6 @@ Please read the following carefully before using this project:
</td>
</tr>
<tr>
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
<td>Thanks to Suixiang AI Gateway for sponsoring this project! <a href="https://sui-xiang.com/">Suixiang AI Gateway</a> is a reliable and efficient API relay service provider offering relay services for Claude, Codex, Gemini, and more. A privacy-focused relay — no data reselling, no model dilution; privacy, transparency, and lightning-fast after-sales support. New accounts get ¥0.5 in trial credit daily by signing in; top-ups are 1:1, no subscription required, pay-as-you-go. Multi-line redundancy, cross-region disaster recovery, automatic failover, and uninterrupted long-link SSE. 99.9% availability — critical calls never fall behind.
</td>
</tr>
<tr>
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
<td>Thanks to Proxy4Free for sponsoring this project! Proxy4Free is a data proxy service provider for developers and AI applications, offering residential proxies, static residential proxies, ISP proxies, and datacenter proxies for scenarios such as Web Scraping, Browser Automation, and AI Agents. With global IP resources, stable connections, and flexible switching, it helps developers improve data collection success rates and reduce the risk of IP bans. Register via <a href="https://www.proxy4free.com/?keyword=4yjqecpc">this link</a> to get started and easily build more stable and efficient automation workflows.
@@ -117,28 +116,11 @@ Please read the following carefully before using this project:
</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
<td>Thanks to Claude API for sponsoring this project! <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">Claude API</a> is an official-channel API provider focused on Claude models. Built on official Anthropic keys and the official AWS Bedrock channel, it delivers a stable integration experience for Claude Code and Agent applications, supports the full Claude model lineup, and retains official capabilities such as Tool Use and long context. The service involves no reverse engineering and no model degradation, making it a great fit for heavy Claude Code users, Agent engineers, and enterprise engineering teams. Register via the <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">exclusive link</a> and contact customer support to receive free trial credits; invoicing and team onboarding are also supported.
</td>
</tr>
<tr>
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
<td>Thanks to code0.ai for sponsoring this project! <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> is an AI coding workbench for developers and engineering teams, aggregating mainstream agent coding capabilities such as Claude Code and Codex, and covering common development scenarios including code generation, project understanding, debugging and fixing, code review, and documentation generation. It suits independent developers, Agent engineers, open-source maintainers, and enterprise R&D teams, with invoicing and team onboarding supported. Register via the <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">exclusive link</a> and contact customer support to receive free trial credits and experience a more efficient AI coding workflow.
</td>
</tr>
<tr>
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
<td><a href="https://nagora.ai/">Nagora</a> is a multi-model AI API gateway built for developers and teams. With a single account and API key, you can access more than 26 leading text and image models through one unified interface. It is compatible with OpenAI, Anthropic, and Gemini protocols and integrates seamlessly with development tools such as Claude Code, Codex, and Gemini CLI. The platform provides intelligent routing, automatic failover, transparent pricing, and consolidated billing, along with budget management, rate limiting, and concurrency controls. This makes AI usage more reliable and manageable across individual development, team collaboration, and production environments. No changes to your existing application are required. Simply replace the Base URL and API key to complete the integration in as little as one minute.</td>
</tr>
<tr>
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
<td>Thanks to <a href="https://www.novada.com/?sub2api/">Novada</a> for sponsoring this project! Novada provides residential, ISP, datacenter, and mobile proxies, along with Web Unlocker and Scraper APIs for developers building AI applications and automation workflows. With global IP coverage, flexible rotating and sticky sessions, and precise geo-targeting, Novada helps teams access web data reliably for AI agent workflows, cross-region testing, web research, and browser automation. Explore Novada to build more stable and scalable AI workflows.</td>
</tr>
<tr>
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="Qiniu AI" width="150"></a></td>
<td>Thanks to Qiniu AI for sponsoring this project! Qiniu AI is the enterprise-grade large-model MaaS platform under Qiniu Cloud (02567.HK), offering one-stop access to 150+ mainstream models worldwide, compatible with the protocols of major global model providers, and covering full-modality capabilities including text, image, audio, video, and file processing, serving over 1.69 million enterprises and developers. Qiniu AI offers an exclusive benefit for Sub2API users: register via <a href="https://s.qiniu.com/u6rQrq">this link</a> — enterprise users get 12 million tokens free, and developers get 3 million tokens free.</td>
@@ -174,6 +156,26 @@ Please read the following carefully before using this project:
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 90M+ global residential network resources across 195+ countries and regions, with rotation and sticky sessions for public data collection, RAG updates, model evaluation, and multi-region data workloads. 🟢Residential Proxy - 20% Off; 🟢Static Residential Proxy - Starting at ¥50.00/IP; 🟢Unlimited Residential Proxy - Starting at ¥19.8/Hour. ✅Get 500M Free Trial.</td>
</tr>
<tr>
<td width="180"><a href="https://go.apimart.ai/gh-sub2api"><img src="assets/partners/logos/apimart.jpg" alt="APIMart" width="150"></a></td>
<td>Thanks to APIMart for sponsoring this project! <a href="https://go.apimart.ai/gh-sub2api">APIMart</a> is a low-cost API platform for AI image and video generation — GPT-Image-2 from $0.006 per image, with 160+ images per dollar. One async API covers both image and video: submit a task, get an ID, and retrieve results via polling or callback. Batch tens of thousands of images without timeouts, and switch models without changing code. Pay as you go with no monthly fee — <a href="https://go.apimart.ai/gh-sub2api">sign up here</a> to get started.</td>
</tr>
<tr>
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
<td>Thanks to AxisNow for sponsoring this project! <a href="https://www.axisnow.io/">AxisNow</a> protects and accelerates websites and APIs, delivering an optimal access experience across mainland China and globally, while extending acceleration and security capabilities to native/mobile apps through client SDKs — <strong>self-hosted private-deployment CDN</strong> | <strong>subscription-based DDoS-protected CDN</strong> | <strong>autonomous, flexibly composable CDN network</strong>.</td>
</tr>
<tr>
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> is a source-level API gateway with its own massive account pool, purpose-built as an upstream relay for downstream transit stations and high-frequency developers — saving you the hassle of building and maintaining your own account pool. ✅ Direct from source: self-operated account pool with no middleman markup; 🧧 Cost crusher: combined rate multiplier as low as 0.03x, just 0.35% of official pricing; 🚀 Blazing fast: first-token latency < 1s, on par with the official native API. <a href="https://www.pp.dog/register?aff=SUB2API">Get started with PP.dog now</a></td>
</tr>
<tr>
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
<td>ColaProxy provides high-quality residential proxies built for web scraping, automation, and multi-account management. Get a free trial with non-expiring traffic, pricing starting as low as $0.3/GB, unlimited concurrent connections, and intelligent IP rotation for a smoother, more stable proxy experience. Use discount code COLA10 to get 10% off and start scaling your projects with reliable residential proxies today. <a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">Get started with ColaProxy</a></td>
</tr>
</table>
## Overview
@@ -206,7 +208,7 @@ Community projects that extend or integrate with Sub2API:
| Component | Technology |
|-----------|------------|
| Backend | Go 1.26.5, Gin, Ent |
| Backend | Go 1.27.0, Gin, Ent |
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
| Database | PostgreSQL 15+ |
| Cache/Queue | Redis 7+ |
@@ -550,12 +552,6 @@ default:
rate_multiplier: 1.0
```
### Sora Status (Temporarily Unavailable)
> ⚠️ Sora-related features are temporarily unavailable due to technical issues in upstream integration and media delivery.
> Please do not rely on Sora in production at this time.
> Existing `gateway.sora_*` configuration keys are reserved and may not take effect until these issues are resolved.
Additional security-related options are available in `config.yaml`:
- `cors.allowed_origins` for CORS allowlist
@@ -658,6 +654,32 @@ Or set `GATEWAY_OPENAI_WS_MODE_ROUTER_V2_ENABLED=true` in the environment.
Use `http_bridge` for client-WebSocket/upstream-HTTP operation when rolling out
or mitigating upstream WebSocket issues.
#### Force OpenAI upstream HTTP/SSE
When an egress proxy or network repeatedly reconnects OpenAI Responses
WebSockets, set the global fallback in the persisted deployment configuration:
```yaml
gateway:
openai_ws:
force_http: true
```
For Compose and Apple container deployments, the equivalent `.env` setting is:
```bash
GATEWAY_OPENAI_WS_FORCE_HTTP=true
```
This selects HTTP/SSE for OpenAI upstream Responses traffic that would
otherwise use WebSocket. It does not change the client-facing protocol or force
HTTP/1.1; configure `gateway.openai_http2.enabled` (or
`GATEWAY_OPENAI_HTTP2_ENABLED=false`) separately when a proxy is incompatible
with HTTP/2. Unlike the account-level `http_bridge` mode, this global fallback
takes effect without enabling `mode_router_v2_enabled`. Keep the setting in the
deployment's persisted `.env` or `config.yaml`, rather than inside a running
container, so it is read again after an image update or container recreation.
#### ⚠️ Important: Creating the Admin Account
The initial admin account is **only created via the setup wizard** (served at `http://<host>:8080` on first run). The `default.admin_email` / `default.admin_password` fields in `config.yaml` are **not used** to create it — they exist in the template for historical reasons.
@@ -736,7 +758,7 @@ Sub2API supports both Grok subscription accounts through xAI OAuth and standard
- Codex CLI style Responses WebSocket ingress is accepted on the Responses targets and bridged to xAI HTTP/SSE Responses upstream
- Text models: `grok-4.5`, `grok-4.3`, `grok-build-0.1`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`, and `grok-4.20-multi-agent-0309`
- Media targets for Grok groups: `/v1/images/generations`, `/images/generations`, `/v1/images/edits`, `/images/edits`, `/v1/videos/generations`, `/videos/generations`, `/v1/videos/edits`, `/videos/edits`, `/v1/videos/extensions`, `/videos/extensions`, `/v1/videos/{request_id}`, and `/videos/{request_id}`. Generation, editing, and extension requests require the group image-generation permission.
- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5`
- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-image-2.0`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5`
- JSON image-edit and video-generation requests accept image references in `image`, `images`, `reference_images`, and `mask` objects. Use `url` for xAI-compatible payloads; the legacy `image_url` field remains accepted and is normalized to `url` before forwarding.
- Out of scope for this provider: TTS, transcription, browser automation, cookies, and Grok web scraping
@@ -804,7 +826,7 @@ xAI quota is passive. Sub2API does not invent subscription quota values; it reco
`401` responses temporarily remove accounts with invalid credentials from scheduling. `403` responses are treated as access or entitlement failures instead of token-refresh loops. `429` responses use `Retry-After` or a short cooldown to temporarily remove the account from scheduling.
New Grok image and video generation requests use a media-specific eligibility check. API-key accounts remain eligible. OAuth accounts require positive paid-entitlement evidence from the xAI billing probe; Free, forbidden, missing, malformed, and inconclusive billing observations are excluded from new media generation. Unobserved OAuth accounts are probed before the first media request is forwarded, and imports run the billing-first quota probe proactively. Chat requests and video status lookups are not affected by this media-only quarantine. If no eligible account remains, the media endpoint returns HTTP `503` with error type `grok_media_no_eligible_account`.
New Grok image and video generation requests use a media-specific eligibility check. API-key accounts remain eligible. OAuth accounts with explicit Free or forbidden billing evidence are excluded from new media generation. Missing or malformed observations are probed before dispatch; a successful but incomplete billing response is treated as `billing_inconclusive` and remains eligible for backwards compatibility, because an unknown billing schema is not proof that the account lacks media entitlement. Operators can quarantine a known-bad account with `extra.grok_media_eligible=false` or force-enable a verified account with `true`. Imports run the billing-first quota probe proactively. Chat requests and video status lookups are not affected by this media-only quarantine. If no eligible account remains, the media endpoint returns HTTP `503` with error type `grok_media_no_eligible_account`.
Administrators can override automatic media eligibility through the account create/update API by setting `extra.grok_media_eligible` to `false` (exclude) or `true` (force eligible). On update, set it to `null` to remove the override and return to automatic probe-based behavior; omitting the field preserves the current override. A weekly allowance period alone is not treated as a paid tier signal. Successful image responses must contain at least one actual image output; empty HTTP `200` responses trigger account failover instead of being counted and returned as successful generations.
@@ -866,11 +888,11 @@ sub2api/
## Star History
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
</picture>
</a>
+39 -56
View File
@@ -4,7 +4,7 @@
# Sub2API
[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/)
[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/)
[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/)
@@ -59,6 +59,11 @@
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一站式集成 Claude Code、Codex 以及最新 Gemini 模型的综合平台,为您提供稳定、高效、高性价比的 AI 编程服务。平台提供灵活的订阅方案,零封号风险,免 VPN 直连,响应极速。AIGoCode 为 sub2api 用户准备了专属福利:通过<a href="https://aigocode.com/invite/SUB2API">此链接</a>注册,首次充值可额外获得 10% 赠送额度!</td>
</tr>
<tr>
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
<td>Real GPT-5.6 series at 3% of OpenAI pricing — <a href="https://codex-everywhere.com">CodexEverywhere</a> is democratizing access to frontier models for developers worldwide. We believe in transparency and honesty, with model quality verified by active community oversight for months. USD and crypto friendly. Start with a free $20 trial at <a href="https://codex-everywhere.com">codex-everywhere.com</a>.</td>
</tr>
<tr>
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
<td>感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过<a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus AI成品号专卖/代充</a>注册下单的用户,可享GPT 官网订阅一折 的震撼价格!</td>
@@ -94,12 +99,6 @@
</td>
</tr>
<tr>
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
<td>感谢 随想AI网关 赞助本项目!<a href="https://sui-xiang.com/">随想AI网关</a> 是一家可靠高效的 API 中继服务提供商,提供 Claude、Codex、Gemini 等的中继服务。注重隐私的中转站·无数据倒卖·无模型掺水,隐私,透明,极速售后。新账户注册每日签到就送 0.5 元测试额度,充值额度 1:1,无需订阅,按量付费。多线路冗余、跨区域容灾、自动故障切换,长链路 SSE 不中断。99.9% 可用性,关键调用从不掉队。
</td>
</tr>
<tr>
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
<td>感谢 Proxy4Free 赞助本项目!Proxy4Free 是面向开发者和 AI 应用的数据代理服务商,提供住宅代理、静态住宅代理、ISP 代理及数据中心代理等多种代理解决方案,适用于 Web Scraping、Browser Automation、AI Agent 等场景。支持全球 IP 资源、稳定连接与灵活切换,帮助开发者提升数据采集成功率,降低 IP 封禁风险。通过<a href="https://www.proxy4free.com/?keyword=4yjqecpc">此链接注册</a>即可开始体验,轻松构建更稳定、高效的自动化工作流。
@@ -120,28 +119,11 @@
</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
<td>感谢 Claude API 对本项目的赞助! <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">Claude API</a> 是专注 Claude 模型的官方渠道 API 服务商,基于 Anthropic 官方 Key 与 AWS Bedrock 官方渠道,提供稳定的 Claude Code 与 Agent 应用接入体验,支持 Claude 全系列模型,保留 Tool Use、长上下文等官方能力。服务非逆向、非降智,适合 Claude Code 深度用户、Agent 工程师与企业技术团队使用。通过<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">[专属链接]</a>注册后联系客服,可领取免费测试额度,并支持开票和团队对接。
</td>
</tr>
<tr>
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
<td>感谢 code0.ai 对本项目的赞助! <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> 是面向开发者与技术团队的 AI 编程工作台,聚合 Claude Code、Codex 等主流 Agent 编程能力,支持代码生成、项目理解、调试修复、代码审查与文档生成等常见研发场景。适合独立开发者、Agent 工程师、开源项目维护者和企业研发团队使用,支持开票和团队对接。通过<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">[专属链接]</a>注册后联系客服,可领取免费测试额度,体验更高效的 AI 编程工作流。
</td>
</tr>
<tr>
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
<td><a href="https://nagora.ai/">Nagora</a> 是专为开发者和团队打造的多模型 AI API 网关。通过一个账户和一枚 API Key,即可统一调用 26+ 款主流文本与图像模型,兼容 OpenAI、Anthropic 与 Gemini 协议,并可无缝接入 Claude Code、Codex、Gemini CLI 等开发工具。平台提供智能路由、自动故障转移、透明计费与统一账单,同时支持预算、限速、并发控制,让个人开发、团队协作和生产环境中的 AI 调用更稳定、更可控。无需改造现有应用,只需替换 Base URL 与 API Key,最快 1 分钟即可完成接入。</td>
</tr>
<tr>
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
<td>感谢 <a href="https://www.novada.com/?sub2api/">Novada</a> 赞助本项目!Novada 为构建 AI 应用与自动化工作流的开发者提供住宅代理、ISP 代理、数据中心代理与移动代理,以及 Web Unlocker 和 Scraper API。凭借全球 IP 覆盖、灵活的轮换与粘性会话以及精准的地理定位,Novada 帮助团队在 AI Agent 工作流、跨区域测试、网络调研与浏览器自动化等场景中稳定获取网络数据。立即体验 Novada,构建更稳定、更可扩展的 AI 工作流。</td>
</tr>
<tr>
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="七牛云AI" width="150"></a></td>
<td>感谢 七牛云AI 赞助本项目!七牛云AI 是七牛云(02567.HK)旗下企业级大模型 MaaS 平台,一站式调用全球 150+ 主流模型,兼容全球主流模型厂商协议,覆盖文本、图像、音频、视频、文件处理等全模态处理能力,服务超过169万企业及开发者用户。七牛云 AI 为 Sub2API 的用户提供了专属福利:通过<a href="https://s.qiniu.com/u6rQrq">此链接</a>注册,企业用户免费领1200万Token,开发者免费领300万Token。</td>
@@ -177,6 +159,26 @@
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 9000 万+ 全球住宅网络资源,覆盖 195+ 国家和地区,支持轮换和粘性会话,适用于公共数据采集、RAG 更新、模型评估和多区域数据工作负载。🟢住宅代理 - 8 折优惠;🟢静态住宅代理 - ¥50.00/IP 起;🟢无限住宅代理 - ¥19.8/小时 起。✅免费领取 500M 试用流量。</td>
</tr>
<tr>
<td width="180"><a href="https://go.apimart.ai/gh-sub2api"><img src="assets/partners/logos/apimart.jpg" alt="APIMart" width="150"></a></td>
<td>感谢 APIMart 赞助了本项目!<a href="https://go.apimart.ai/gh-sub2api">APIMart</a> 是专注于 AI 图片/视频生成的低价 API 平台,GPT-Image-2 低至 $0.006/张,1 美元可生成 160+ 张图片。图片、视频一套异步 API 通吃:提交任务获取 ID,通过轮询或回调获取结果;批量生成上万张图片也不会超时,切换模型无需修改代码。按量付费、无月费,通过<a href="https://go.apimart.ai/gh-sub2api">此注册链接</a>注册即可开始使用。</td>
</tr>
<tr>
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
<td>感谢 AxisNow 赞助了本项目!<a href="https://www.axisnow.io/">AxisNow</a> 保护并加速网站与 API,兼顾中国大陆及全球的访问体验,并通过客户端 SDK,将加速与安全能力延伸至原生/移动 App — <strong>自建私有部署 CDN</strong>|<strong>订阅式高防 CDN</strong>|<strong>自主可控、灵活组合的 CDN 网络</strong>。</td>
</tr>
<tr>
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> 是自建账号池的源头 API 网关,专注为下游中转站与高频开发者提供 API 网关中继服务,帮您省去自建号池的一切麻烦——✅ 源头直供:自持海量账号池,无中间商赚差价;🧧 成本屠夫:综合倍率低至 0.03x,成本仅为官方的千分之3.5;🚀 极速体验:首 Token 延迟 < 1s,流畅媲美官方原生 API。<a href="https://www.pp.dog/register?aff=SUB2API">立即接入PP.dog</a></td>
</tr>
<tr>
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
<td>ColaProxy 提供专为网页抓取、自动化和多账号管理打造的高质量住宅代理。免费试用,流量永不过期,价格低至 $0.3/GB,支持无限并发连接和智能 IP 轮换,带来更流畅、更稳定的代理体验。使用优惠码 COLA10 立享 9 折优惠,立即开始使用可靠的住宅代理扩展您的项目。<a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">立即开始使用 ColaProxy</a></td>
</tr>
</table>
## 项目概述
@@ -208,7 +210,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的
| 组件 | 技术 |
|------|------|
| 后端 | Go 1.26.5, Gin, Ent |
| 后端 | Go 1.27.0, Gin, Ent |
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
| 数据库 | PostgreSQL 15+ |
| 缓存/队列 | Redis 7+ |
@@ -563,33 +565,6 @@ default:
rate_multiplier: 1.0
```
### Sora 功能状态(暂不可用)
> ⚠️ 当前 Sora 相关功能因上游接入与媒体链路存在技术问题,暂时不可用。
> 现阶段请勿在生产环境依赖 Sora 能力。
> 文档中的 `gateway.sora_*` 配置仅作预留,待技术问题修复后再恢复可用。
### Sora 媒体签名 URL(功能恢复后可选)
当配置 `gateway.sora_media_signing_key` 且 `gateway.sora_media_signed_url_ttl_seconds > 0` 时,网关会将 Sora 输出的媒体地址改写为临时签名 URL(`/sora/media-signed/...`)。这样无需 API Key 即可在浏览器中直接访问,且具备过期控制与防篡改能力(签名包含 path + query)。
```yaml
gateway:
# /sora/media 是否强制要求 API Key(默认 false)
sora_media_require_api_key: false
# 媒体临时签名密钥(为空则禁用签名)
sora_media_signing_key: "your-signing-key"
# 临时签名 URL 有效期(秒)
sora_media_signed_url_ttl_seconds: 900
```
> 若未配置签名密钥,`/sora/media-signed` 将返回 503。
> 如需更严格的访问控制,可将 `sora_media_require_api_key` 设为 true,仅允许携带 API Key 的 `/sora/media` 访问。
访问策略说明:
- `/sora/media`:内部调用或客户端携带 API Key 才能下载
- `/sora/media-signed`:外部可访问,但有签名 + 过期控制
`config.yaml` 还支持以下安全相关配置:
- `cors.allowed_origins` 配置 CORS 白名单
@@ -731,6 +706,14 @@ go generate ./cmd/server
---
## OpenAI 图片模型
支持 `gpt-image-2.5-flare`、`gpt-image-2.5-sunburst` 及其 `2026-09-08` 日期快照,可通过 `/v1/images/generations`、`/v1/images/edits` 调用。`quality` 支持 `xhigh`、`max`、`auto`,合法自定义尺寸和图片 usage 明细保持透传。
OAuth / Setup Token 图片请求使用 Responses 主控模型调用 `image_generation` 工具,默认主控为 `gpt-5.6-luna`。可设置 `SUB2API_IMAGES_MAIN_MODEL` 切换为账号支持的文本模型;Docker Compose 用户修改 `.env` 后执行 `docker compose up -d` 重建容器。该配置不会替换所选图片模型,也不会覆盖 `/v1/responses` 请求中已经提供的文本主控模型。
升级后,无模型限制的账号自动支持新模型。已有显式账号映射或分组白名单需要加入两个 2.5 模型(日期快照按需加入);升级不会自动扩大管理员设置的模型权限。新模型内置价格包含官方文本输入、图片输入和图片输出 token 费率,远端价格表尚未更新时使用内置 2.5 价格;实际按次或按 token 计费仍由既有分组/渠道配置决定。
## 简易模式
简易模式适合个人开发者或内部团队快速使用,不依赖完整 SaaS 功能。
@@ -797,11 +780,11 @@ sub2api/
## Star History
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
</picture>
</a>
+31 -35
View File
@@ -4,7 +4,7 @@
# Sub2API
[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8.svg)](https://golang.org/)
[![Go](https://img.shields.io/badge/Go-1.27.0-00ADD8.svg)](https://golang.org/)
[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/)
@@ -58,6 +58,11 @@
<td>AIGoCode のご支援に感謝します!AIGoCode は Claude Code、Codex、最新の Gemini モデルを統合したオールインワンプラットフォームで、安定的かつ効率的でコストパフォーマンスに優れた AI コーディングサービスを提供します。柔軟なサブスクリプションプラン、アカウント停止リスクゼロ、VPN 不要の直接アクセス、超高速レスポンスが特長です。AIGoCode は sub2api ユーザー向けに特別特典を用意しています:<a href="https://aigocode.com/invite/SUB2API">こちらのリンク</a>から登録すると、初回チャージ時に 10% のボーナスクレジットを追加プレゼント!</td>
</tr>
<tr>
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
<td>OpenAI 公式価格のわずか 3% で本物の GPT-5.6 シリーズを提供 — <a href="https://codex-everywhere.com">CodexEverywhere</a> は世界中の開発者にフロンティアモデルへのアクセスを民主化しています。私たちは透明性と誠実さを信条とし、モデル品質は数か月にわたるアクティブなコミュニティの監視によって検証されています。USD および暗号通貨に対応。<a href="https://codex-everywhere.com">codex-everywhere.com</a> で $20 の無料トライアルから始めましょう。</td>
</tr>
<tr>
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
<td>本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらの<a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus AIアカウント専門店/代行チャージ</a>経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます!</td>
@@ -93,12 +98,6 @@
</td>
</tr>
<tr>
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
<td>随想AI ゲートウェイのご支援に感謝します!<a href="https://sui-xiang.com/">随想AI ゲートウェイ</a> は信頼性と効率に優れた API 中継サービスプロバイダーで、Claude、Codex、Gemini などの中継サービスを提供しています。プライバシー重視の中継ステーション・データ転売なし・モデル水増しなし、プライバシー・透明性・超高速アフターサービス。新規アカウント登録後、毎日サインインで 0.5 元のテストクレジットがもらえ、チャージは 1:1、サブスクリプション不要、従量課金。マルチライン冗長、クロスリージョン災害復旧、自動フェイルオーバー、長時間 SSE 接続が途切れません。99.9% の可用性、重要な呼び出しは決して遅れません。
</td>
</tr>
<tr>
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
<td>Proxy4Free のご支援に感謝します!Proxy4Free は開発者と AI アプリケーション向けのデータプロキシサービスプロバイダーで、住宅プロキシ、静的住宅プロキシ、ISP プロキシ、データセンタープロキシなど多様なプロキシソリューションを提供しており、Web Scraping、Browser Automation、AI Agent などのシナリオに適しています。グローバル IP リソース、安定した接続、柔軟な切り替えをサポートし、開発者のデータ収集成功率の向上と IP ブロックリスクの低減を支援します。<a href="https://www.proxy4free.com/?keyword=4yjqecpc">こちらのリンクから登録</a>して、より安定した効率的な自動化ワークフローを簡単に構築しましょう。
@@ -119,28 +118,11 @@
</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
<td>Claude API のご支援に感謝します!<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">Claude API</a> は Claude モデルに特化した公式チャネルの API サービスプロバイダーで、Anthropic 公式キーと AWS Bedrock 公式チャネルをベースに、Claude Code や Agent アプリケーションへの安定した接続体験を提供します。Claude 全シリーズのモデルに対応し、Tool Use や長文コンテキストなどの公式機能もそのまま利用可能。リバースエンジニアリングやモデル劣化のないサービスで、Claude Code のヘビーユーザー、Agent エンジニア、企業の技術チームに最適です。<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z">専用リンク</a>から登録後カスタマーサポートへご連絡いただくと、無料お試しクレジットを受け取れます。請求書発行やチーム導入にも対応しています。
</td>
</tr>
<tr>
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
<td>code0.ai のご支援に感謝します!<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> は開発者と技術チーム向けの AI プログラミングワークベンチで、Claude Code や Codex などの主要な Agent コーディング能力を集約し、コード生成、プロジェクト理解、デバッグと修正、コードレビュー、ドキュメント生成といった一般的な開発シーンをサポートします。個人開発者、Agent エンジニア、OSS メンテナー、企業の開発チームに最適で、請求書発行やチーム導入にも対応。<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">専用リンク</a>から登録後カスタマーサポートへご連絡いただくと、無料お試しクレジットを受け取り、より効率的な AI プログラミングワークフローを体験できます。
</td>
</tr>
<tr>
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
<td><a href="https://nagora.ai/">Nagora</a>は、開発者やチーム向けに設計されたマルチモデルAI APIゲートウェイです。1つのアカウントと1つのAPIキーだけで、26種類以上の主要なテキストモデルおよび画像モデルを一元的に利用できます。OpenAI、Anthropic、Geminiの各プロトコルに対応し、Claude Code、Codex、Gemini CLIなどの開発ツールにもシームレスに接続できます。 プラットフォームには、インテリジェントルーティング、自動フェイルオーバー、透明性の高い料金体系、請求の一元管理に加え、予算管理、レート制限、同時実行数の制御機能が備わっています。これにより、個人開発、チームでの共同作業、本番環境におけるAI APIの利用を、より安定的かつ柔軟に管理できます。 既存のアプリケーションを改修する必要はありません。Base URLとAPIキーを置き換えるだけで、最短1分で導入を完了できます。</td>
</tr>
<tr>
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
<td>Novada のご支援に感謝します!<a href="https://www.novada.com/?sub2api/">Novada</a> は、AI アプリケーションや自動化ワークフローを構築する開発者向けに、レジデンシャル、ISP、データセンター、モバイルプロキシに加え、Web Unlocker と Scraper API を提供しています。グローバルな IP カバレッジ、柔軟なローテーション/スティッキーセッション、精密なジオターゲティングにより、AI エージェントワークフロー、クロスリージョンテスト、Web リサーチ、ブラウザ自動化などのシーンで、チームが Web データへ確実にアクセスできるよう支援します。Novada で、より安定しスケーラブルな AI ワークフローを構築しましょう。</td>
</tr>
<tr>
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="Qiniu AI" width="150"></a></td>
<td>七牛云AI のご支援に感謝します!七牛云AI は、七牛云(02567.HK)傘下のエンタープライズ向け大規模モデル MaaS プラットフォームです。世界の主要モデル 150+ をワンストップで利用でき、グローバル主要モデルプロバイダーのプロトコルに対応し、テキスト、画像、音声、動画、ファイル処理などのフルモーダル処理能力をカバー。169万を超える企業・開発者ユーザーにサービスを提供しています。Sub2API ユーザー向けの限定特典として、<a href="https://s.qiniu.com/u6rQrq">こちらのリンク</a>から登録すると、企業ユーザーは 1200万 Token、開発者は 300万 Token を無料で獲得できます。</td>
@@ -176,6 +158,26 @@
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 195 以上の国と地域にわたる 9,000 万以上のグローバルレジデンシャルネットワークリソース。ローテーションとスティッキーセッションに対応し、パブリックデータ収集、RAG 更新、モデル評価、マルチリージョンデータワークロードに最適。🟢レジデンシャルプロキシ - 20% オフ;🟢スタティックレジデンシャルプロキシ - ¥50.00/IP から;🟢無制限レジデンシャルプロキシ - ¥19.8/時間 から。✅500M 無料トライアルを取得。</td>
</tr>
<tr>
<td width="180"><a href="https://go.apimart.ai/gh-sub2api"><img src="assets/partners/logos/apimart.jpg" alt="APIMart" width="150"></a></td>
<td>APIMart のご支援に感謝します!<a href="https://go.apimart.ai/gh-sub2api">APIMart</a> は AI 画像・動画生成に特化した低価格 API プラットフォームです。GPT-Image-2 は 1 枚 $0.006 から、1 ドルで 160 枚以上の画像を生成できます。画像と動画の両方に対応する非同期 API を 1 つで利用でき、タスクを送信して ID を取得し、ポーリングまたはコールバックで結果を取得できます。数万枚規模のバッチ処理でもタイムアウトせず、モデルを変更してもコードの変更は不要です。月額料金なしの従量課金制で、<a href="https://go.apimart.ai/gh-sub2api">こちらの登録リンク</a>からすぐに利用を開始できます。</td>
</tr>
<tr>
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
<td>AxisNow のご支援に感謝します!<a href="https://www.axisnow.io/">AxisNow</a> はウェブサイトと API を保護・高速化し、中国本土および世界中で快適なアクセス体験を実現します。クライアント SDK により、ネイティブ/モバイルアプリにも高速化・セキュリティ機能を拡張できます — <strong>自社構築・プライベートデプロイ CDN</strong>|<strong>サブスクリプション型高防御 CDN</strong>|<strong>自律制御で柔軟に組み合わせられる CDN ネットワーク</strong>。</td>
</tr>
<tr>
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> は自社運用の大規模アカウントプールを持つソースレベルの API ゲートウェイで、下流の中継ステーションや高頻度開発者向けに API ゲートウェイリレーサービスを提供し、アカウントプールの自主構築にかかるすべての手間を省きます。✅ ソース直供:自社運用の大規模アカウントプール、中間マージンなし;🧧 コスト最強:総合レート倍率わずか 0.03x、コストは公式の 0.35% のみ;🚀 超高速体験:ファーストトークンレイテンシ 1 秒未満、公式ネイティブ API に匹敵するスムーズさ。<a href="https://www.pp.dog/register?aff=SUB2API">今すぐ PP.dog に接続</a></td>
</tr>
<tr>
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
<td>ColaProxy は、Web スクレイピング、自動化、マルチアカウント管理向けに構築された高品質な住宅プロキシを提供します。有効期限なしのトラフィックで無料トライアルが利用でき、料金は $0.3/GB から、無制限の同時接続とインテリジェントな IP ローテーションにより、よりスムーズで安定したプロキシ体験を実現します。割引コード COLA10 で 10% オフ、信頼性の高い住宅プロキシでプロジェクトを今すぐスケールしましょう。<a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">ColaProxy を始める</a></td>
</tr>
</table>
## 概要
@@ -207,7 +209,7 @@ Sub2API を拡張・統合するコミュニティプロジェクト:
| コンポーネント | 技術 |
|-----------|------------|
| バックエンド | Go 1.26.5, Gin, Ent |
| バックエンド | Go 1.27.0, Gin, Ent |
| フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS |
| データベース | PostgreSQL 15+ |
| キャッシュ/キュー | Redis 7+ |
@@ -550,12 +552,6 @@ default:
rate_multiplier: 1.0
```
### Sora ステータス(一時的に利用不可)
> ⚠️ Sora 関連の機能は、上流統合およびメディア配信の技術的問題により一時的に利用できません。
> 現時点では本番環境で Sora に依存しないでください。
> 既存の `gateway.sora_*` 設定キーは予約されていますが、これらの問題が解決されるまで有効にならない場合があります。
`config.yaml` では追加のセキュリティ関連オプションも利用できます:
- `cors.allowed_origins` - CORS 許可リスト
@@ -732,11 +728,11 @@ sub2api/
## スター履歴
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
</picture>
</a>
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

+10
View File
@@ -121,6 +121,16 @@ linters:
# Default: true — must be true, ent generates 130K+ lines of code
generated-is-used: true
exclusions:
rules:
# G703/G704 污点分析在测试文件中只会命中 httptest mock 与测试者自设的
# 环境变量路径,不构成攻击面;且该分析跨环境结果不稳定(本地/CI 报告的
# 位置集合不同),逐点 nolint 无法收敛,故按路径豁免。生产代码不豁免,
# 必须逐点 //nolint:gosec 并写明信任边界。
- path: '_test\.go$'
linters: [ gosec ]
text: 'G70[34]'
formatters:
enable:
- gofmt
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26.5-alpine
FROM golang:1.27.0-alpine
WORKDIR /app
+1 -1
View File
@@ -1 +1 @@
0.1.175
0.2.3
+5
View File
@@ -153,6 +153,11 @@ func runMainServer() {
log.Fatalf("Failed to initialize application: %v", err)
}
defer app.Cleanup()
if app.PluginManager != nil {
if err := app.PluginManager.Start(context.Background()); err != nil {
log.Printf("Plugin manager started in degraded state: %v", err)
}
}
if app.PromptAudit != nil {
if err := app.PromptAudit.Start(context.Background()); err != nil {
// Startup continues so unrelated APIs stay up. Fail-closed (unavailable)
+40 -10
View File
@@ -25,9 +25,10 @@ import (
)
type Application struct {
Server *http.Server
PromptAudit *securityaudit.PromptService
Cleanup func()
Server *http.Server
PromptAudit *securityaudit.PromptService
PluginManager *service.PluginManager
Cleanup func()
}
func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
@@ -51,12 +52,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
// BuildInfo provider
provideServiceBuildInfo,
providePluginHostInfo,
// Cleanup function provider
provideCleanup,
// Application struct
wire.Struct(new(Application), "Server", "PromptAudit", "Cleanup"),
wire.Struct(new(Application), "Server", "PromptAudit", "PluginManager", "Cleanup"),
)
return nil, nil
}
@@ -72,6 +74,13 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
}
}
func providePluginHostInfo(buildInfo handler.BuildInfo) service.PluginHostInfo {
return service.PluginHostInfo{
Version: buildInfo.Version,
BuildType: buildInfo.BuildType,
}
}
func provideCleanup(
entClient *ent.Client,
rdb *redis.Client,
@@ -88,6 +97,7 @@ func provideCleanup(
schedulerSnapshot *service.SchedulerSnapshotService,
tokenRefresh *service.TokenRefreshService,
accountExpiry *service.AccountExpiryService,
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
codexVersionSync *service.OpenAICodexVersionSyncService,
proxyExpiry *service.ProxyExpiryService,
subscriptionExpiry *service.SubscriptionExpiryService,
@@ -115,7 +125,9 @@ func provideCleanup(
upstreamBillingProbe *service.UpstreamBillingProbeService,
ollamaCloudUsage *service.OllamaCloudUsageService,
auditLog *service.AuditLogService,
openAIAutoReset *service.OpenAIQuotaAutoResetService,
promptAudit *securityaudit.PromptService,
pluginManager *service.PluginManager,
) func() {
return func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -128,6 +140,18 @@ func provideCleanup(
// 应用层清理步骤可并行执行,基础设施资源(Redis/Ent)最后按顺序关闭。
parallelSteps := []cleanupStep{
{"PluginManager", func() error {
if pluginManager != nil {
pluginManager.Stop()
}
return nil
}},
{"OpenAIQuotaAutoResetService", func() error {
if openAIAutoReset != nil {
openAIAutoReset.Stop()
}
return nil
}},
{"OpsIngressRejectAggregator", func() error {
if opsIngressReject != nil {
opsIngressReject.Stop()
@@ -238,6 +262,12 @@ func provideCleanup(
accountExpiry.Stop()
return nil
}},
{"CNProviderBalanceCheckService", func() error {
if cnProviderBalanceCheck != nil {
cnProviderBalanceCheck.Stop()
}
return nil
}},
{"OpenAICodexVersionSyncService", func() error {
codexVersionSync.Stop()
return nil
@@ -321,12 +351,12 @@ func provideCleanup(
return nil
}},
{"ChannelMonitorV2Aggregator", func() error {
if channelMonitorV2Aggregator != nil {
channelMonitorV2Aggregator.Stop()
}
return nil
}},
{"ChannelMonitorRunner", func() error {
if channelMonitorV2Aggregator != nil {
channelMonitorV2Aggregator.Stop()
}
return nil
}},
{"ChannelMonitorRunner", func() error {
if channelMonitorRunner != nil {
channelMonitorRunner.Stop()
}
+62 -20
View File
@@ -137,7 +137,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
tlsFingerprintProfileCache := repository.NewTLSFingerprintProfileCache(redisClient)
tlsFingerprintProfileService := service.NewTLSFingerprintProfileService(tlsFingerprintProfileRepository, tlsFingerprintProfileCache)
channelRepository := repository.NewChannelRepository(db)
channelService := service.NewChannelService(channelRepository, groupRepository, apiKeyAuthCacheInvalidator, pricingService)
channelCachePubSub := repository.NewChannelCache(redisClient)
channelService := service.NewChannelService(channelRepository, groupRepository, apiKeyAuthCacheInvalidator, pricingService, channelCachePubSub)
modelPricingResolver := service.NewModelPricingResolver(channelService, billingService)
compositeModelRouteRepository := repository.NewCompositeModelRouteRepository(client)
compositeRouteResolver := service.NewCompositeRouteResolver(compositeModelRouteRepository)
@@ -178,7 +179,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService, settingService)
channelMonitorV2Repository := repository.NewChannelMonitorV2Repository(db)
channelMonitorV2Service := service.ProvideChannelMonitorV2Service(channelMonitorV2Repository, settingService)
channelMonitorV2Handler := handler.NewChannelMonitorV2Handler(channelMonitorV2Service)
channelMonitorV2Handler := handler.NewChannelMonitorV2Handler(channelMonitorV2Service, apiKeyService)
dashboardAggregationRepository := repository.NewDashboardAggregationRepository(db)
dashboardStatsCache := repository.NewDashboardCache(redisClient, configConfig)
dashboardService := service.NewDashboardService(usageLogRepository, dashboardAggregationRepository, dashboardStatsCache, configConfig)
@@ -189,25 +190,28 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
adminAccountRepository := repository.NewAdminAccountRepository(client, db, schedulerCache)
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
adminService := service.NewAdminService(userRepository, adminGroupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService, compositeModelRouteRepository, compositeRouteResolver, channelService)
adminService := service.NewAdminService(configConfig, userRepository, adminGroupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService, compositeModelRouteRepository, compositeRouteResolver, channelService)
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService, serviceUserPlatformQuotaRepository, billingCache, totpService, userService, settingService)
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
groupHandler := admin.NewGroupHandlerWithConfig(adminService, dashboardService, groupCapacityService, configConfig)
claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository, configConfig)
grokQuotaFetcher := service.NewGrokQuotaFetcher()
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository, settingService)
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIGatewayService)
usageCache := service.NewUsageCache()
accountUsageService := service.ProvideAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, grokQuotaFetcher, grokQuotaService, openAIQuotaService, usageCache, identityCache, tlsFingerprintProfileService, openAIGatewayService)
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService, settingService)
pluginRepository := repository.NewPluginRepository(db)
pluginHostInfo := providePluginHostInfo(buildInfo)
pluginManager := service.NewPluginManager(pluginRepository, secretEncryptor, configConfig, pluginHostInfo)
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService, settingService, pluginManager)
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
accountHandler := admin.ProvideAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator, grokQuotaService)
accountHandler := admin.ProvideAccountHandler(configConfig, adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator, grokQuotaService)
adminAnnouncementHandler := admin.NewAnnouncementHandler(announcementService)
dataManagementService := service.NewDataManagementService()
dataManagementHandler := admin.NewDataManagementHandler(dataManagementService)
backupObjectStoreFactory := repository.NewS3BackupStoreFactory()
dbDumper := repository.NewPgDumper(configConfig)
dbDumper := repository.NewPgDumper(configConfig, db)
backupService := service.ProvideBackupService(settingRepository, configConfig, secretEncryptor, backupObjectStoreFactory, dbDumper, leaderLockCache, db)
imageStorageFactory := repository.ProvideImageStorageFactory()
imageStorageSettingService := service.ProvideImageStorageSettingService(settingRepository, secretEncryptor, backupService, imageStorageFactory, configConfig)
@@ -218,6 +222,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig, tempUnschedCache, privacyClientFactory, proxyRepository, oAuthRefreshAPI, openAIGatewayService)
grokOAuthHandler := admin.NewGrokOAuthHandler(grokOAuthService, adminService, grokQuotaService, tokenRefreshService)
cnProviderQuotaService := service.ProvideCNProviderQuotaService(accountRepository, proxyRepository, httpUpstream, configConfig)
cnProviderBalanceService := service.ProvideCNProviderBalanceService(accountRepository, proxyRepository, httpUpstream, configConfig)
cnProviderHandler := admin.NewCNProviderHandler(cnProviderQuotaService, cnProviderBalanceService)
proxyHandler := admin.NewProxyHandler(adminService)
adminRedeemHandler := admin.NewRedeemHandler(adminService, redeemService)
promoHandler := admin.NewPromoHandler(promoService)
@@ -248,6 +255,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService)
pluginHandler := admin.NewPluginHandler(pluginManager)
adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService)
scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db)
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
@@ -277,14 +285,14 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
auditLogHandler := admin.NewAuditLogHandler(auditLogService, totpService)
upstreamBillingProbeService := service.ProvideUpstreamBillingProbeService(accountRepository, accountTestService, settingService, leaderLockCache, db)
ollamaCloudUsageService := service.ProvideOllamaCloudUsageService(accountRepository, httpUpstream, settingService, secretEncryptor, configConfig, leaderLockCache, db)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, grokOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, promptAdminHandler, paymentHandler, affiliateHandler, complianceHandler, auditLogHandler, upstreamBillingProbeService, ollamaCloudUsageService)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, grokOAuthHandler, cnProviderHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, pluginHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, promptAdminHandler, paymentHandler, affiliateHandler, complianceHandler, auditLogHandler, upstreamBillingProbeService, ollamaCloudUsageService)
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
legacyEngine := securityaudit.NewLegacyModerationAdapter(contentModerationService)
coordinator := securityaudit.NewCoordinator(legacyEngine, promptService)
gatewayHandler := handler.ProvideGatewayHandler(gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, userMessageQueueService, configConfig, settingService, coordinator)
openAIGatewayHandler := handler.ProvideOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, opsService, grokQuotaService, configConfig, coordinator)
openAIGatewayHandler := handler.ProvideOpenAIGatewayHandler(openAIGatewayService, pluginManager, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, opsService, grokQuotaService, configConfig, coordinator)
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo, notificationEmailService)
totpHandler := handler.NewTotpHandler(totpService)
passkeyRepository := repository.NewPasskeyRepository(db)
@@ -297,7 +305,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
handlerPaymentHandler := handler.NewPaymentHandler(paymentService, paymentConfigService)
paymentWebhookHandler := handler.NewPaymentWebhookHandler(paymentService, registry)
availableChannelHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService, settingService)
modelPlazaHandler := handler.NewModelPlazaHandler(channelService, apiKeyService, settingService)
modelPlazaService := service.NewModelPlazaService(channelRepository, groupRepository, pricingService, billingService, modelPricingResolver)
modelPlazaHandler := handler.NewModelPlazaHandler(modelPlazaService, apiKeyService, settingService)
imageTaskStore := repository.NewImageTaskStore(redisClient)
imageTaskService := service.ProvideImageTaskService(imageTaskStore, imageStorageSettingService)
asyncImageHandler := handler.NewAsyncImageHandler(imageTaskService, openAIGatewayHandler)
@@ -311,7 +320,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
batchImageHandler := handler.ProvideBatchImageHandler(batchImagePublicService, batchImageDownloadService, batchImageCleanupService, openAIGatewayHandler)
idempotencyCoordinator := service.ProvideIdempotencyCoordinator(idempotencyRepository, configConfig)
idempotencyCleanupService := service.ProvideIdempotencyCleanupService(idempotencyRepository, configConfig)
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, channelMonitorUserHandler, channelMonitorV2Handler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler, passkeyHandler, handlerPaymentHandler, paymentWebhookHandler, availableChannelHandler, modelPlazaHandler, asyncImageHandler, batchImageHandler, idempotencyCoordinator, idempotencyCleanupService)
openAIQuotaAutoResetService := service.ProvideOpenAIQuotaAutoResetService(accountRepository, openAIQuotaService, rateLimitService, idempotencyCoordinator, auditLogService, settingService, leaderLockCache)
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, channelMonitorUserHandler, channelMonitorV2Handler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler, passkeyHandler, handlerPaymentHandler, paymentWebhookHandler, availableChannelHandler, modelPlazaHandler, asyncImageHandler, batchImageHandler, idempotencyCoordinator, idempotencyCleanupService, openAIQuotaAutoResetService)
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService, settingService, auditLogService)
optionalJWTAuthMiddleware := middleware.NewOptionalJWTAuthMiddleware(authService, userService, settingService, auditLogService)
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService, auditLogService)
@@ -327,20 +337,23 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
opsIngressRejectAggregator := service.ProvideOpsIngressRejectAggregator(opsRepository, opsService)
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
cnProviderBalanceCheckService := service.ProvideCNProviderBalanceCheckService(accountRepository, cnProviderBalanceService, cnProviderQuotaService, configConfig)
openAICodexVersionSyncService := service.ProvideOpenAICodexVersionSyncService(settingRepository, settingService, gitHubReleaseClient)
proxyExpiryService := service.ProvideProxyExpiryService(proxyRepository)
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository, settingRepository, notificationEmailService, leaderLockCache, db)
batchImageWorkerRuntime := service.ProvideBatchImageWorkerRuntime(batchImageRepository, accountRepository, batchImageQueue, usageBillingRepository, usageLogRepository, batchImageModelPricingResolver, apiKeyAuthCacheInvalidator, configConfig)
scheduledTestRunnerService := service.ProvideScheduledTestRunnerService(scheduledTestPlanRepository, scheduledTestService, accountTestService, rateLimitService, configConfig)
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService, leaderLockCache, db)
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService)
channelMonitorQuotaFetcher := service.NewChannelMonitorQuotaFetcher(accountUsageService, cnProviderQuotaService, cnProviderBalanceService, accountRepository, configConfig)
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService, channelMonitorQuotaFetcher)
channelMonitorV2Aggregator := service.ProvideChannelMonitorV2Aggregator(channelMonitorV2Repository, db, settingService)
userPlatformQuotaUsageFlusher := service.ProvideUserPlatformQuotaUsageFlusher(configConfig, billingCache, serviceUserPlatformQuotaRepository, timingWheelService)
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, opsService, opsIngressRejectAggregator, apiKeyService, authCacheInvalidationWorker, schedulerSnapshotService, tokenRefreshService, accountExpiryService, openAICodexVersionSyncService, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, batchImageCleanupService, batchImageWorkerRuntime, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, channelMonitorV2Aggregator, userPlatformQuotaUsageFlusher, upstreamBillingProbeService, ollamaCloudUsageService, auditLogService, promptService)
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, opsService, opsIngressRejectAggregator, apiKeyService, authCacheInvalidationWorker, schedulerSnapshotService, tokenRefreshService, accountExpiryService, cnProviderBalanceCheckService, openAICodexVersionSyncService, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, batchImageCleanupService, batchImageWorkerRuntime, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, channelMonitorV2Aggregator, userPlatformQuotaUsageFlusher, upstreamBillingProbeService, ollamaCloudUsageService, auditLogService, openAIQuotaAutoResetService, promptService, pluginManager)
application := &Application{
Server: httpServer,
PromptAudit: promptService,
Cleanup: v,
Server: httpServer,
PromptAudit: promptService,
PluginManager: pluginManager,
Cleanup: v,
}
return application, nil
}
@@ -348,9 +361,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
// wire.go:
type Application struct {
Server *http.Server
PromptAudit *securityaudit.PromptService
Cleanup func()
Server *http.Server
PromptAudit *securityaudit.PromptService
PluginManager *service.PluginManager
Cleanup func()
}
func providePrivacyClientFactory() service.PrivacyClientFactory {
@@ -364,6 +378,13 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
}
}
func providePluginHostInfo(buildInfo handler.BuildInfo) service.PluginHostInfo {
return service.PluginHostInfo{
Version: buildInfo.Version,
BuildType: buildInfo.BuildType,
}
}
func provideCleanup(
entClient *ent.Client,
rdb *redis.Client,
@@ -380,6 +401,7 @@ func provideCleanup(
schedulerSnapshot *service.SchedulerSnapshotService,
tokenRefresh *service.TokenRefreshService,
accountExpiry *service.AccountExpiryService,
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
codexVersionSync *service.OpenAICodexVersionSyncService,
proxyExpiry *service.ProxyExpiryService,
subscriptionExpiry *service.SubscriptionExpiryService,
@@ -407,7 +429,9 @@ func provideCleanup(
upstreamBillingProbe *service.UpstreamBillingProbeService,
ollamaCloudUsage *service.OllamaCloudUsageService,
auditLog *service.AuditLogService,
openAIAutoReset *service.OpenAIQuotaAutoResetService,
promptAudit *securityaudit.PromptService,
pluginManager *service.PluginManager,
) func() {
return func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -419,6 +443,18 @@ func provideCleanup(
}
parallelSteps := []cleanupStep{
{"PluginManager", func() error {
if pluginManager != nil {
pluginManager.Stop()
}
return nil
}},
{"OpenAIQuotaAutoResetService", func() error {
if openAIAutoReset != nil {
openAIAutoReset.Stop()
}
return nil
}},
{"OpsIngressRejectAggregator", func() error {
if opsIngressReject != nil {
opsIngressReject.Stop()
@@ -529,6 +565,12 @@ func provideCleanup(
accountExpiry.Stop()
return nil
}},
{"CNProviderBalanceCheckService", func() error {
if cnProviderBalanceCheck != nil {
cnProviderBalanceCheck.Stop()
}
return nil
}},
{"OpenAICodexVersionSyncService", func() error {
codexVersionSync.Stop()
return nil
+3
View File
@@ -66,6 +66,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
schedulerSnapshotSvc,
tokenRefreshSvc,
accountExpirySvc,
nil, // cnProviderBalanceCheck
codexVersionSyncSvc,
proxyExpirySvc,
subscriptionExpirySvc,
@@ -93,7 +94,9 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
nil, // upstreamBillingProbe
nil, // ollamaCloudUsage
nil, // auditLog
nil, // openAIAutoReset
nil, // promptAudit
nil, // pluginManager
)
require.NotPanics(t, func() {
@@ -0,0 +1,136 @@
# Upstream error proxy attribution
## Scope
Gateway inference errors are stored in `ops_error_logs.upstream_errors`. Each
array item represents one upstream attempt, including same-account retries and
cross-account failover attempts. Proxy attribution is stored on that item; it
must not be reconstructed later from `accounts.proxy_id`.
This repository has one gateway implementation under `backend/internal/service`.
There is no separate `gateway/backend` source copy to synchronize.
## Event contract
Every newly persisted event contains these fields. `proxy_id` is emitted even
when its value is JSON `null`:
| Field | Meaning |
| --- | --- |
| `proxy_id` | Managed proxy ID used by the attempt; `null` for direct or unknown routes. |
| `proxy_name` | Snapshotted proxy name, `direct/no_proxy`, or `unknown`. |
Only proxy ID and name are stored. Proxy URL, protocol endpoint, host, port,
username, password, and authorization data are excluded.
The two sentinel names have strict meanings:
- `direct/no_proxy`: the transport was explicitly given no proxy.
- `unknown`: the route cannot be proven from event-time evidence. This includes
legacy events, pre-transport credential failures, and OpenAI WebSocket use of
the default HTTP client.
Invariant: `proxy_id` is `null` if and only if `proxy_name` is one of the two
sentinels. A managed proxy with a blank name (defensive only; `proxies.name` is
non-empty) is labeled `proxy`. Both the struct normalizer used at append time
and the JSON normalizer used on detail reads enforce this, so an event can never
carry `proxy_id=null` together with a real proxy name.
## Why the account snapshot is event-time evidence
Account selection loads `ProxyID` and `Proxy` into one in-memory snapshot. HTTP,
TLS-fingerprint, Bedrock, Gemini, Antigravity, OpenAI, and OpenAI WebSocket
forwarding use that snapshot for managed proxy routing.
Inference transports use a managed proxy only when both the binding ID and its
hydrated proxy object are present; the attribution accessors use the same rule.
OpenAI WebSocket additionally retains the existing default-client environment
proxy behavior when that snapshot does not yield a usable proxy. The transport
does not switch to a database backup proxy during a request.
Expired proxy fallback happens before scheduling by atomically rewriting the
account binding:
- backup proxy: `proxy_id` becomes the backup ID and
`proxy_fallback_origin_id` stores the expired proxy ID;
- direct fallback: `proxy_id` becomes null and
`proxy_fallback_origin_id` stores the expired proxy ID.
Custom Anthropic relays receive the same snapshotted proxy as their encoded
relay proxy parameter. OpenAI WebSocket requests keep the existing transport
behavior: configured account proxies use the proxy client, while an empty
account proxy leaves the HTTP client unset so `coder/websocket` uses
`http.DefaultClient`, including its `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`
handling.
For WebSocket errors without a usable account proxy, the event stores
`proxy_id=null` and `proxy_name=unknown`. It does not infer whether the default
client selected an environment proxy or a direct connection after the fact.
This applies to every event produced while a request is on the WebSocket
transport, including WS handshake/fallback errors and the first-output timeout
raised by the WS passthrough relay; `opsUpstreamWSProxyAttribution` is the
accessor for those sites and never returns `direct/no_proxy`.
Credential acquisition failures that occur before the inference transport is
opened are also recorded as unknown. The account's proxy binding is not treated
as proof that the credential operation or an inference request used that route.
Each failure site constructs a complete `OpsUpstreamErrorEvent`. Its literal
sets `proxy_id` and `proxy_name` from the same route input used by the attempt.
Credential-free accessors copy only the managed proxy ID and name. A fallback
proxy needs no special event flag: its event-time ID and name are the historical
route evidence. Shared helpers that assemble the event on behalf of a caller
(`handleUpstreamTransportError`, `handleOpenAIUpstreamTransportError`,
`appendOpenAICompactFallbackRetryOps`, `recordOpenAIRawStreamTruncation`,
`newOpenAIFirstOutputTimeoutError`) stamp attribution themselves, so a caller
cannot forget it. Every non-test `OpsUpstreamErrorEvent{` literal in
`backend/internal/service` must either set `ProxyName` or be passed to one of
those helpers; grep for literals missing `ProxyName` before merging changes
that add a failure site.
`appendOpsUpstreamError` accepts only the completed event. It does not receive
an `Account`, query current account state, or infer proxy attribution while
appending. Once appended, the event contains only scalar snapshot values, so
later account mutation or failover cannot change an earlier event.
Queue sanitization keeps attempt events newest-first under hard bounds:
- the newest 16 attempts retain `detail` and `upstream_response_body` (each
capped at the queue body limit) plus `upstream_url`/`message` up to 2048
bytes;
- older attempts keep timestamps, account, proxy, status, kind, stage, scope,
reason and a `upstream_url`/`message` trimmed to 512 bytes; an attempt whose
only payload was a cleared `detail` is still retained;
- at most 256 attempts and roughly 512 KiB of serialized JSON are kept per
request. When either bound drops attempts, the oldest retained event carries
`dropped_earlier_attempts` with the number of discarded earlier attempts. The
newest attempt is always retained because it decides the request outcome.
## Legacy events and analytics
Legacy JSON is not rewritten in the database. Detail reads edit only the
`proxy_id`/`proxy_name` keys of events that lack valid attribution, so keys
written by older struct versions and the stored key order are preserved. The
shared parser applies the same rule to the decoded structs. Both materialize
missing attribution as:
```json
{
"proxy_id": null,
"proxy_name": "unknown"
}
```
Proxy and region aggregation must use the event fields first. A safe grouping
rule is:
1. A non-null event `proxy_id` groups by that ID and uses the event
`proxy_name` as the historical label. A join to `proxies` may enrich current
metadata but must not replace the event label.
2. `proxy_id=null` and `proxy_name=direct/no_proxy` groups as direct.
3. Missing attribution, or `proxy_id=null` and `proxy_name=unknown`, groups as
unknown. Do not join through the account's current `proxy_id` and present it
as historical attribution.
If an operator deliberately produces a current-account cohort for legacy data,
the report must be labeled as a current snapshot; it is not historical proxy
attribution.
+28 -3
View File
@@ -27,9 +27,13 @@ type ChannelMonitor struct {
Name string `json:"name,omitempty"`
// Provider holds the value of the "provider" field.
Provider channelmonitor.Provider `json:"provider,omitempty"`
// probe = LLM probe (default); quota = account usage only; quota_probe = both
CheckMode string `json:"check_mode,omitempty"`
// AccountID holds the value of the "account_id" field.
AccountID *int64 `json:"account_id,omitempty"`
// OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions
APIMode string `json:"api_mode,omitempty"`
// Provider base origin, e.g. https://api.openai.com
// Provider base origin, e.g. https://api.openai.com; empty for quota-only monitors
Endpoint string `json:"endpoint,omitempty"`
// AES-256-GCM encrypted API key
APIKeyEncrypted string `json:"-"`
@@ -114,9 +118,9 @@ func (*ChannelMonitor) scanValues(columns []string) ([]any, error) {
values[i] = new([]byte)
case channelmonitor.FieldEnabled:
values[i] = new(sql.NullBool)
case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldJitterSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
case channelmonitor.FieldID, channelmonitor.FieldAccountID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldJitterSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
values[i] = new(sql.NullInt64)
case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldAPIMode, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldCheckMode, channelmonitor.FieldAPIMode, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
values[i] = new(sql.NullString)
case channelmonitor.FieldCreatedAt, channelmonitor.FieldUpdatedAt, channelmonitor.FieldLastCheckedAt:
values[i] = new(sql.NullTime)
@@ -165,6 +169,19 @@ func (_m *ChannelMonitor) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.Provider = channelmonitor.Provider(value.String)
}
case channelmonitor.FieldCheckMode:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field check_mode", values[i])
} else if value.Valid {
_m.CheckMode = value.String
}
case channelmonitor.FieldAccountID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field account_id", values[i])
} else if value.Valid {
_m.AccountID = new(int64)
*_m.AccountID = value.Int64
}
case channelmonitor.FieldAPIMode:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field api_mode", values[i])
@@ -326,6 +343,14 @@ func (_m *ChannelMonitor) String() string {
builder.WriteString("provider=")
builder.WriteString(fmt.Sprintf("%v", _m.Provider))
builder.WriteString(", ")
builder.WriteString("check_mode=")
builder.WriteString(_m.CheckMode)
builder.WriteString(", ")
if v := _m.AccountID; v != nil {
builder.WriteString("account_id=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("api_mode=")
builder.WriteString(_m.APIMode)
builder.WriteString(", ")
+30 -5
View File
@@ -23,6 +23,10 @@ const (
FieldName = "name"
// FieldProvider holds the string denoting the provider field in the database.
FieldProvider = "provider"
// FieldCheckMode holds the string denoting the check_mode field in the database.
FieldCheckMode = "check_mode"
// FieldAccountID holds the string denoting the account_id field in the database.
FieldAccountID = "account_id"
// FieldAPIMode holds the string denoting the api_mode field in the database.
FieldAPIMode = "api_mode"
// FieldEndpoint holds the string denoting the endpoint field in the database.
@@ -91,6 +95,8 @@ var Columns = []string{
FieldUpdatedAt,
FieldName,
FieldProvider,
FieldCheckMode,
FieldAccountID,
FieldAPIMode,
FieldEndpoint,
FieldAPIKeyEncrypted,
@@ -127,6 +133,10 @@ var (
UpdateDefaultUpdatedAt func() time.Time
// NameValidator is a validator for the "name" field. It is called by the builders before save.
NameValidator func(string) error
// DefaultCheckMode holds the default value on creation for the "check_mode" field.
DefaultCheckMode string
// CheckModeValidator is a validator for the "check_mode" field. It is called by the builders before save.
CheckModeValidator func(string) error
// DefaultAPIMode holds the default value on creation for the "api_mode" field.
DefaultAPIMode string
// APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save.
@@ -164,10 +174,15 @@ type Provider string
// Provider values.
const (
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderGrok Provider = "grok"
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderGrok Provider = "grok"
ProviderAntigravity Provider = "antigravity"
ProviderKimi Provider = "kimi"
ProviderZhipu Provider = "zhipu"
ProviderDeepseek Provider = "deepseek"
ProviderMinimax Provider = "minimax"
)
func (pr Provider) String() string {
@@ -177,7 +192,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax:
return nil
default:
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
@@ -212,6 +227,16 @@ func ByProvider(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldProvider, opts...).ToFunc()
}
// ByCheckMode orders the results by the check_mode field.
func ByCheckMode(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCheckMode, opts...).ToFunc()
}
// ByAccountID orders the results by the account_id field.
func ByAccountID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAccountID, opts...).ToFunc()
}
// ByAPIMode orders the results by the api_mode field.
func ByAPIMode(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAPIMode, opts...).ToFunc()
+125
View File
@@ -70,6 +70,16 @@ func Name(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldName, v))
}
// CheckMode applies equality check predicate on the "check_mode" field. It's identical to CheckModeEQ.
func CheckMode(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldCheckMode, v))
}
// AccountID applies equality check predicate on the "account_id" field. It's identical to AccountIDEQ.
func AccountID(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldAccountID, v))
}
// APIMode applies equality check predicate on the "api_mode" field. It's identical to APIModeEQ.
func APIMode(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v))
@@ -295,6 +305,121 @@ func ProviderNotIn(vs ...Provider) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNotIn(FieldProvider, vs...))
}
// CheckModeEQ applies the EQ predicate on the "check_mode" field.
func CheckModeEQ(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldCheckMode, v))
}
// CheckModeNEQ applies the NEQ predicate on the "check_mode" field.
func CheckModeNEQ(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNEQ(FieldCheckMode, v))
}
// CheckModeIn applies the In predicate on the "check_mode" field.
func CheckModeIn(vs ...string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldIn(FieldCheckMode, vs...))
}
// CheckModeNotIn applies the NotIn predicate on the "check_mode" field.
func CheckModeNotIn(vs ...string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNotIn(FieldCheckMode, vs...))
}
// CheckModeGT applies the GT predicate on the "check_mode" field.
func CheckModeGT(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldGT(FieldCheckMode, v))
}
// CheckModeGTE applies the GTE predicate on the "check_mode" field.
func CheckModeGTE(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldGTE(FieldCheckMode, v))
}
// CheckModeLT applies the LT predicate on the "check_mode" field.
func CheckModeLT(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldLT(FieldCheckMode, v))
}
// CheckModeLTE applies the LTE predicate on the "check_mode" field.
func CheckModeLTE(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldLTE(FieldCheckMode, v))
}
// CheckModeContains applies the Contains predicate on the "check_mode" field.
func CheckModeContains(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldContains(FieldCheckMode, v))
}
// CheckModeHasPrefix applies the HasPrefix predicate on the "check_mode" field.
func CheckModeHasPrefix(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldHasPrefix(FieldCheckMode, v))
}
// CheckModeHasSuffix applies the HasSuffix predicate on the "check_mode" field.
func CheckModeHasSuffix(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldHasSuffix(FieldCheckMode, v))
}
// CheckModeEqualFold applies the EqualFold predicate on the "check_mode" field.
func CheckModeEqualFold(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEqualFold(FieldCheckMode, v))
}
// CheckModeContainsFold applies the ContainsFold predicate on the "check_mode" field.
func CheckModeContainsFold(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldContainsFold(FieldCheckMode, v))
}
// AccountIDEQ applies the EQ predicate on the "account_id" field.
func AccountIDEQ(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldAccountID, v))
}
// AccountIDNEQ applies the NEQ predicate on the "account_id" field.
func AccountIDNEQ(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNEQ(FieldAccountID, v))
}
// AccountIDIn applies the In predicate on the "account_id" field.
func AccountIDIn(vs ...int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldIn(FieldAccountID, vs...))
}
// AccountIDNotIn applies the NotIn predicate on the "account_id" field.
func AccountIDNotIn(vs ...int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNotIn(FieldAccountID, vs...))
}
// AccountIDGT applies the GT predicate on the "account_id" field.
func AccountIDGT(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldGT(FieldAccountID, v))
}
// AccountIDGTE applies the GTE predicate on the "account_id" field.
func AccountIDGTE(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldGTE(FieldAccountID, v))
}
// AccountIDLT applies the LT predicate on the "account_id" field.
func AccountIDLT(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldLT(FieldAccountID, v))
}
// AccountIDLTE applies the LTE predicate on the "account_id" field.
func AccountIDLTE(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldLTE(FieldAccountID, v))
}
// AccountIDIsNil applies the IsNil predicate on the "account_id" field.
func AccountIDIsNil() predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldIsNull(FieldAccountID))
}
// AccountIDNotNil applies the NotNil predicate on the "account_id" field.
func AccountIDNotNil() predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldNotNull(FieldAccountID))
}
// APIModeEQ applies the EQ predicate on the "api_mode" field.
func APIModeEQ(v string) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v))
+168
View File
@@ -65,6 +65,34 @@ func (_c *ChannelMonitorCreate) SetProvider(v channelmonitor.Provider) *ChannelM
return _c
}
// SetCheckMode sets the "check_mode" field.
func (_c *ChannelMonitorCreate) SetCheckMode(v string) *ChannelMonitorCreate {
_c.mutation.SetCheckMode(v)
return _c
}
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableCheckMode(v *string) *ChannelMonitorCreate {
if v != nil {
_c.SetCheckMode(*v)
}
return _c
}
// SetAccountID sets the "account_id" field.
func (_c *ChannelMonitorCreate) SetAccountID(v int64) *ChannelMonitorCreate {
_c.mutation.SetAccountID(v)
return _c
}
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableAccountID(v *int64) *ChannelMonitorCreate {
if v != nil {
_c.SetAccountID(*v)
}
return _c
}
// SetAPIMode sets the "api_mode" field.
func (_c *ChannelMonitorCreate) SetAPIMode(v string) *ChannelMonitorCreate {
_c.mutation.SetAPIMode(v)
@@ -303,6 +331,10 @@ func (_c *ChannelMonitorCreate) defaults() {
v := channelmonitor.DefaultUpdatedAt()
_c.mutation.SetUpdatedAt(v)
}
if _, ok := _c.mutation.CheckMode(); !ok {
v := channelmonitor.DefaultCheckMode
_c.mutation.SetCheckMode(v)
}
if _, ok := _c.mutation.APIMode(); !ok {
v := channelmonitor.DefaultAPIMode
_c.mutation.SetAPIMode(v)
@@ -357,6 +389,14 @@ func (_c *ChannelMonitorCreate) check() error {
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
}
}
if _, ok := _c.mutation.CheckMode(); !ok {
return &ValidationError{Name: "check_mode", err: errors.New(`ent: missing required field "ChannelMonitor.check_mode"`)}
}
if v, ok := _c.mutation.CheckMode(); ok {
if err := channelmonitor.CheckModeValidator(v); err != nil {
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
}
}
if _, ok := _c.mutation.APIMode(); !ok {
return &ValidationError{Name: "api_mode", err: errors.New(`ent: missing required field "ChannelMonitor.api_mode"`)}
}
@@ -473,6 +513,14 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
_node.Provider = value
}
if value, ok := _c.mutation.CheckMode(); ok {
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
_node.CheckMode = value
}
if value, ok := _c.mutation.AccountID(); ok {
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
_node.AccountID = &value
}
if value, ok := _c.mutation.APIMode(); ok {
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
_node.APIMode = value
@@ -666,6 +714,42 @@ func (u *ChannelMonitorUpsert) UpdateProvider() *ChannelMonitorUpsert {
return u
}
// SetCheckMode sets the "check_mode" field.
func (u *ChannelMonitorUpsert) SetCheckMode(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldCheckMode, v)
return u
}
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateCheckMode() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldCheckMode)
return u
}
// SetAccountID sets the "account_id" field.
func (u *ChannelMonitorUpsert) SetAccountID(v int64) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldAccountID, v)
return u
}
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateAccountID() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldAccountID)
return u
}
// AddAccountID adds v to the "account_id" field.
func (u *ChannelMonitorUpsert) AddAccountID(v int64) *ChannelMonitorUpsert {
u.Add(channelmonitor.FieldAccountID, v)
return u
}
// ClearAccountID clears the value of the "account_id" field.
func (u *ChannelMonitorUpsert) ClearAccountID() *ChannelMonitorUpsert {
u.SetNull(channelmonitor.FieldAccountID)
return u
}
// SetAPIMode sets the "api_mode" field.
func (u *ChannelMonitorUpsert) SetAPIMode(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldAPIMode, v)
@@ -975,6 +1059,48 @@ func (u *ChannelMonitorUpsertOne) UpdateProvider() *ChannelMonitorUpsertOne {
})
}
// SetCheckMode sets the "check_mode" field.
func (u *ChannelMonitorUpsertOne) SetCheckMode(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetCheckMode(v)
})
}
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateCheckMode() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateCheckMode()
})
}
// SetAccountID sets the "account_id" field.
func (u *ChannelMonitorUpsertOne) SetAccountID(v int64) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetAccountID(v)
})
}
// AddAccountID adds v to the "account_id" field.
func (u *ChannelMonitorUpsertOne) AddAccountID(v int64) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddAccountID(v)
})
}
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateAccountID() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateAccountID()
})
}
// ClearAccountID clears the value of the "account_id" field.
func (u *ChannelMonitorUpsertOne) ClearAccountID() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearAccountID()
})
}
// SetAPIMode sets the "api_mode" field.
func (u *ChannelMonitorUpsertOne) SetAPIMode(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
@@ -1487,6 +1613,48 @@ func (u *ChannelMonitorUpsertBulk) UpdateProvider() *ChannelMonitorUpsertBulk {
})
}
// SetCheckMode sets the "check_mode" field.
func (u *ChannelMonitorUpsertBulk) SetCheckMode(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetCheckMode(v)
})
}
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateCheckMode() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateCheckMode()
})
}
// SetAccountID sets the "account_id" field.
func (u *ChannelMonitorUpsertBulk) SetAccountID(v int64) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetAccountID(v)
})
}
// AddAccountID adds v to the "account_id" field.
func (u *ChannelMonitorUpsertBulk) AddAccountID(v int64) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddAccountID(v)
})
}
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateAccountID() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateAccountID()
})
}
// ClearAccountID clears the value of the "account_id" field.
func (u *ChannelMonitorUpsertBulk) ClearAccountID() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearAccountID()
})
}
// SetAPIMode sets the "api_mode" field.
func (u *ChannelMonitorUpsertBulk) SetAPIMode(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
+116
View File
@@ -66,6 +66,47 @@ func (_u *ChannelMonitorUpdate) SetNillableProvider(v *channelmonitor.Provider)
return _u
}
// SetCheckMode sets the "check_mode" field.
func (_u *ChannelMonitorUpdate) SetCheckMode(v string) *ChannelMonitorUpdate {
_u.mutation.SetCheckMode(v)
return _u
}
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
func (_u *ChannelMonitorUpdate) SetNillableCheckMode(v *string) *ChannelMonitorUpdate {
if v != nil {
_u.SetCheckMode(*v)
}
return _u
}
// SetAccountID sets the "account_id" field.
func (_u *ChannelMonitorUpdate) SetAccountID(v int64) *ChannelMonitorUpdate {
_u.mutation.ResetAccountID()
_u.mutation.SetAccountID(v)
return _u
}
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
func (_u *ChannelMonitorUpdate) SetNillableAccountID(v *int64) *ChannelMonitorUpdate {
if v != nil {
_u.SetAccountID(*v)
}
return _u
}
// AddAccountID adds value to the "account_id" field.
func (_u *ChannelMonitorUpdate) AddAccountID(v int64) *ChannelMonitorUpdate {
_u.mutation.AddAccountID(v)
return _u
}
// ClearAccountID clears the value of the "account_id" field.
func (_u *ChannelMonitorUpdate) ClearAccountID() *ChannelMonitorUpdate {
_u.mutation.ClearAccountID()
return _u
}
// SetAPIMode sets the "api_mode" field.
func (_u *ChannelMonitorUpdate) SetAPIMode(v string) *ChannelMonitorUpdate {
_u.mutation.SetAPIMode(v)
@@ -453,6 +494,11 @@ func (_u *ChannelMonitorUpdate) check() error {
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
}
}
if v, ok := _u.mutation.CheckMode(); ok {
if err := channelmonitor.CheckModeValidator(v); err != nil {
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
}
}
if v, ok := _u.mutation.APIMode(); ok {
if err := channelmonitor.APIModeValidator(v); err != nil {
return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)}
@@ -517,6 +563,18 @@ func (_u *ChannelMonitorUpdate) sqlSave(ctx context.Context) (_node int, err err
if value, ok := _u.mutation.Provider(); ok {
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
}
if value, ok := _u.mutation.CheckMode(); ok {
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
}
if value, ok := _u.mutation.AccountID(); ok {
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
}
if value, ok := _u.mutation.AddedAccountID(); ok {
_spec.AddField(channelmonitor.FieldAccountID, field.TypeInt64, value)
}
if _u.mutation.AccountIDCleared() {
_spec.ClearField(channelmonitor.FieldAccountID, field.TypeInt64)
}
if value, ok := _u.mutation.APIMode(); ok {
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
}
@@ -755,6 +813,47 @@ func (_u *ChannelMonitorUpdateOne) SetNillableProvider(v *channelmonitor.Provide
return _u
}
// SetCheckMode sets the "check_mode" field.
func (_u *ChannelMonitorUpdateOne) SetCheckMode(v string) *ChannelMonitorUpdateOne {
_u.mutation.SetCheckMode(v)
return _u
}
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
func (_u *ChannelMonitorUpdateOne) SetNillableCheckMode(v *string) *ChannelMonitorUpdateOne {
if v != nil {
_u.SetCheckMode(*v)
}
return _u
}
// SetAccountID sets the "account_id" field.
func (_u *ChannelMonitorUpdateOne) SetAccountID(v int64) *ChannelMonitorUpdateOne {
_u.mutation.ResetAccountID()
_u.mutation.SetAccountID(v)
return _u
}
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
func (_u *ChannelMonitorUpdateOne) SetNillableAccountID(v *int64) *ChannelMonitorUpdateOne {
if v != nil {
_u.SetAccountID(*v)
}
return _u
}
// AddAccountID adds value to the "account_id" field.
func (_u *ChannelMonitorUpdateOne) AddAccountID(v int64) *ChannelMonitorUpdateOne {
_u.mutation.AddAccountID(v)
return _u
}
// ClearAccountID clears the value of the "account_id" field.
func (_u *ChannelMonitorUpdateOne) ClearAccountID() *ChannelMonitorUpdateOne {
_u.mutation.ClearAccountID()
return _u
}
// SetAPIMode sets the "api_mode" field.
func (_u *ChannelMonitorUpdateOne) SetAPIMode(v string) *ChannelMonitorUpdateOne {
_u.mutation.SetAPIMode(v)
@@ -1155,6 +1254,11 @@ func (_u *ChannelMonitorUpdateOne) check() error {
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
}
}
if v, ok := _u.mutation.CheckMode(); ok {
if err := channelmonitor.CheckModeValidator(v); err != nil {
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
}
}
if v, ok := _u.mutation.APIMode(); ok {
if err := channelmonitor.APIModeValidator(v); err != nil {
return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)}
@@ -1236,6 +1340,18 @@ func (_u *ChannelMonitorUpdateOne) sqlSave(ctx context.Context) (_node *ChannelM
if value, ok := _u.mutation.Provider(); ok {
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
}
if value, ok := _u.mutation.CheckMode(); ok {
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
}
if value, ok := _u.mutation.AccountID(); ok {
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
}
if value, ok := _u.mutation.AddedAccountID(); ok {
_spec.AddField(channelmonitor.FieldAccountID, field.TypeInt64, value)
}
if _u.mutation.AccountIDCleared() {
_spec.ClearField(channelmonitor.FieldAccountID, field.TypeInt64)
}
if value, ok := _u.mutation.APIMode(); ok {
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
}
+17
View File
@@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"strings"
"time"
@@ -11,6 +12,7 @@ import (
"entgo.io/ent/dialect/sql"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/internal/domain"
)
// ChannelMonitorHistory is the model entity for the ChannelMonitorHistory schema.
@@ -30,6 +32,8 @@ type ChannelMonitorHistory struct {
PingLatencyMs *int `json:"ping_latency_ms,omitempty"`
// Message holds the value of the "message" field.
Message string `json:"message,omitempty"`
// Quota holds the value of the "quota" field.
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
// CheckedAt holds the value of the "checked_at" field.
CheckedAt time.Time `json:"checked_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
@@ -63,6 +67,8 @@ func (*ChannelMonitorHistory) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case channelmonitorhistory.FieldQuota:
values[i] = new([]byte)
case channelmonitorhistory.FieldID, channelmonitorhistory.FieldMonitorID, channelmonitorhistory.FieldLatencyMs, channelmonitorhistory.FieldPingLatencyMs:
values[i] = new(sql.NullInt64)
case channelmonitorhistory.FieldModel, channelmonitorhistory.FieldStatus, channelmonitorhistory.FieldMessage:
@@ -128,6 +134,14 @@ func (_m *ChannelMonitorHistory) assignValues(columns []string, values []any) er
} else if value.Valid {
_m.Message = value.String
}
case channelmonitorhistory.FieldQuota:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field quota", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.Quota); err != nil {
return fmt.Errorf("unmarshal field quota: %w", err)
}
}
case channelmonitorhistory.FieldCheckedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field checked_at", values[i])
@@ -197,6 +211,9 @@ func (_m *ChannelMonitorHistory) String() string {
builder.WriteString("message=")
builder.WriteString(_m.Message)
builder.WriteString(", ")
builder.WriteString("quota=")
builder.WriteString(fmt.Sprintf("%v", _m.Quota))
builder.WriteString(", ")
builder.WriteString("checked_at=")
builder.WriteString(_m.CheckedAt.Format(time.ANSIC))
builder.WriteByte(')')
@@ -27,6 +27,8 @@ const (
FieldPingLatencyMs = "ping_latency_ms"
// FieldMessage holds the string denoting the message field in the database.
FieldMessage = "message"
// FieldQuota holds the string denoting the quota field in the database.
FieldQuota = "quota"
// FieldCheckedAt holds the string denoting the checked_at field in the database.
FieldCheckedAt = "checked_at"
// EdgeMonitor holds the string denoting the monitor edge name in mutations.
@@ -51,6 +53,7 @@ var Columns = []string{
FieldLatencyMs,
FieldPingLatencyMs,
FieldMessage,
FieldQuota,
FieldCheckedAt,
}
@@ -365,6 +365,16 @@ func MessageContainsFold(v string) predicate.ChannelMonitorHistory {
return predicate.ChannelMonitorHistory(sql.FieldContainsFold(FieldMessage, v))
}
// QuotaIsNil applies the IsNil predicate on the "quota" field.
func QuotaIsNil() predicate.ChannelMonitorHistory {
return predicate.ChannelMonitorHistory(sql.FieldIsNull(FieldQuota))
}
// QuotaNotNil applies the NotNil predicate on the "quota" field.
func QuotaNotNil() predicate.ChannelMonitorHistory {
return predicate.ChannelMonitorHistory(sql.FieldNotNull(FieldQuota))
}
// CheckedAtEQ applies the EQ predicate on the "checked_at" field.
func CheckedAtEQ(v time.Time) predicate.ChannelMonitorHistory {
return predicate.ChannelMonitorHistory(sql.FieldEQ(FieldCheckedAt, v))
@@ -13,6 +13,7 @@ import (
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/internal/domain"
)
// ChannelMonitorHistoryCreate is the builder for creating a ChannelMonitorHistory entity.
@@ -83,6 +84,12 @@ func (_c *ChannelMonitorHistoryCreate) SetNillableMessage(v *string) *ChannelMon
return _c
}
// SetQuota sets the "quota" field.
func (_c *ChannelMonitorHistoryCreate) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryCreate {
_c.mutation.SetQuota(v)
return _c
}
// SetCheckedAt sets the "checked_at" field.
func (_c *ChannelMonitorHistoryCreate) SetCheckedAt(v time.Time) *ChannelMonitorHistoryCreate {
_c.mutation.SetCheckedAt(v)
@@ -226,6 +233,10 @@ func (_c *ChannelMonitorHistoryCreate) createSpec() (*ChannelMonitorHistory, *sq
_spec.SetField(channelmonitorhistory.FieldMessage, field.TypeString, value)
_node.Message = value
}
if value, ok := _c.mutation.Quota(); ok {
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
_node.Quota = value
}
if value, ok := _c.mutation.CheckedAt(); ok {
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
_node.CheckedAt = value
@@ -401,6 +412,24 @@ func (u *ChannelMonitorHistoryUpsert) ClearMessage() *ChannelMonitorHistoryUpser
return u
}
// SetQuota sets the "quota" field.
func (u *ChannelMonitorHistoryUpsert) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsert {
u.Set(channelmonitorhistory.FieldQuota, v)
return u
}
// UpdateQuota sets the "quota" field to the value that was provided on create.
func (u *ChannelMonitorHistoryUpsert) UpdateQuota() *ChannelMonitorHistoryUpsert {
u.SetExcluded(channelmonitorhistory.FieldQuota)
return u
}
// ClearQuota clears the value of the "quota" field.
func (u *ChannelMonitorHistoryUpsert) ClearQuota() *ChannelMonitorHistoryUpsert {
u.SetNull(channelmonitorhistory.FieldQuota)
return u
}
// SetCheckedAt sets the "checked_at" field.
func (u *ChannelMonitorHistoryUpsert) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsert {
u.Set(channelmonitorhistory.FieldCheckedAt, v)
@@ -572,6 +601,27 @@ func (u *ChannelMonitorHistoryUpsertOne) ClearMessage() *ChannelMonitorHistoryUp
})
}
// SetQuota sets the "quota" field.
func (u *ChannelMonitorHistoryUpsertOne) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsertOne {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.SetQuota(v)
})
}
// UpdateQuota sets the "quota" field to the value that was provided on create.
func (u *ChannelMonitorHistoryUpsertOne) UpdateQuota() *ChannelMonitorHistoryUpsertOne {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.UpdateQuota()
})
}
// ClearQuota clears the value of the "quota" field.
func (u *ChannelMonitorHistoryUpsertOne) ClearQuota() *ChannelMonitorHistoryUpsertOne {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.ClearQuota()
})
}
// SetCheckedAt sets the "checked_at" field.
func (u *ChannelMonitorHistoryUpsertOne) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsertOne {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
@@ -909,6 +959,27 @@ func (u *ChannelMonitorHistoryUpsertBulk) ClearMessage() *ChannelMonitorHistoryU
})
}
// SetQuota sets the "quota" field.
func (u *ChannelMonitorHistoryUpsertBulk) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsertBulk {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.SetQuota(v)
})
}
// UpdateQuota sets the "quota" field to the value that was provided on create.
func (u *ChannelMonitorHistoryUpsertBulk) UpdateQuota() *ChannelMonitorHistoryUpsertBulk {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.UpdateQuota()
})
}
// ClearQuota clears the value of the "quota" field.
func (u *ChannelMonitorHistoryUpsertBulk) ClearQuota() *ChannelMonitorHistoryUpsertBulk {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
s.ClearQuota()
})
}
// SetCheckedAt sets the "checked_at" field.
func (u *ChannelMonitorHistoryUpsertBulk) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsertBulk {
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/internal/domain"
)
// ChannelMonitorHistoryUpdate is the builder for updating ChannelMonitorHistory entities.
@@ -145,6 +146,18 @@ func (_u *ChannelMonitorHistoryUpdate) ClearMessage() *ChannelMonitorHistoryUpda
return _u
}
// SetQuota sets the "quota" field.
func (_u *ChannelMonitorHistoryUpdate) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpdate {
_u.mutation.SetQuota(v)
return _u
}
// ClearQuota clears the value of the "quota" field.
func (_u *ChannelMonitorHistoryUpdate) ClearQuota() *ChannelMonitorHistoryUpdate {
_u.mutation.ClearQuota()
return _u
}
// SetCheckedAt sets the "checked_at" field.
func (_u *ChannelMonitorHistoryUpdate) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpdate {
_u.mutation.SetCheckedAt(v)
@@ -267,6 +280,12 @@ func (_u *ChannelMonitorHistoryUpdate) sqlSave(ctx context.Context) (_node int,
if _u.mutation.MessageCleared() {
_spec.ClearField(channelmonitorhistory.FieldMessage, field.TypeString)
}
if value, ok := _u.mutation.Quota(); ok {
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
}
if _u.mutation.QuotaCleared() {
_spec.ClearField(channelmonitorhistory.FieldQuota, field.TypeJSON)
}
if value, ok := _u.mutation.CheckedAt(); ok {
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
}
@@ -435,6 +454,18 @@ func (_u *ChannelMonitorHistoryUpdateOne) ClearMessage() *ChannelMonitorHistoryU
return _u
}
// SetQuota sets the "quota" field.
func (_u *ChannelMonitorHistoryUpdateOne) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpdateOne {
_u.mutation.SetQuota(v)
return _u
}
// ClearQuota clears the value of the "quota" field.
func (_u *ChannelMonitorHistoryUpdateOne) ClearQuota() *ChannelMonitorHistoryUpdateOne {
_u.mutation.ClearQuota()
return _u
}
// SetCheckedAt sets the "checked_at" field.
func (_u *ChannelMonitorHistoryUpdateOne) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpdateOne {
_u.mutation.SetCheckedAt(v)
@@ -587,6 +618,12 @@ func (_u *ChannelMonitorHistoryUpdateOne) sqlSave(ctx context.Context) (_node *C
if _u.mutation.MessageCleared() {
_spec.ClearField(channelmonitorhistory.FieldMessage, field.TypeString)
}
if value, ok := _u.mutation.Quota(); ok {
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
}
if _u.mutation.QuotaCleared() {
_spec.ClearField(channelmonitorhistory.FieldQuota, field.TypeJSON)
}
if value, ok := _u.mutation.CheckedAt(); ok {
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
}
@@ -100,10 +100,15 @@ type Provider string
// Provider values.
const (
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderGrok Provider = "grok"
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderGrok Provider = "grok"
ProviderAntigravity Provider = "antigravity"
ProviderKimi Provider = "kimi"
ProviderZhipu Provider = "zhipu"
ProviderDeepseek Provider = "deepseek"
ProviderMinimax Provider = "minimax"
)
func (pr Provider) String() string {
@@ -113,7 +118,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax:
return nil
default:
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
+17 -1
View File
@@ -4595,6 +4595,22 @@ func (c *ProxyClient) QueryAccounts(_m *Proxy) *AccountQuery {
return query
}
// QueryPrimaryProxies queries the primary_proxies edge of a Proxy.
func (c *ProxyClient) QueryPrimaryProxies(_m *Proxy) *ProxyQuery {
query := (&ProxyClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(proxy.Table, proxy.FieldID, id),
sqlgraph.To(proxy.Table, proxy.FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, proxy.PrimaryProxiesTable, proxy.PrimaryProxiesColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryBackupProxy queries the backup_proxy edge of a Proxy.
func (c *ProxyClient) QueryBackupProxy(_m *Proxy) *ProxyQuery {
query := (&ProxyClient{config: c.config}).Query()
@@ -4603,7 +4619,7 @@ func (c *ProxyClient) QueryBackupProxy(_m *Proxy) *ProxyQuery {
step := sqlgraph.NewStep(
sqlgraph.From(proxy.Table, proxy.FieldID, id),
sqlgraph.To(proxy.Table, proxy.FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
sqlgraph.Edge(sqlgraph.M2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
+61 -14
View File
@@ -4,6 +4,7 @@ package ent
import (
"encoding/json"
"encoding/json/jsontext"
"fmt"
"strings"
"time"
@@ -97,10 +98,10 @@ type Group struct {
AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"`
// STT 每小时价格(USD)
AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"`
// 是否按上下文长度应用模型阶梯价格
// 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价
LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"`
// 分组逐模型定价;优先级高于渠道和内置定价
ModelPricing json.RawMessage `json:"model_pricing,omitempty"`
ModelPricing jsontext.Value `json:"model_pricing,omitempty"`
// 是否仅允许 Claude Code 客户端
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
// 非 Claude Code 请求降级使用的分组 ID
@@ -121,6 +122,10 @@ type Group struct {
AllowMessagesDispatch bool `json:"allow_messages_dispatch,omitempty"`
// 是否允许此 OpenAI 分组访问 Live 接口
AllowLive bool `json:"allow_live,omitempty"`
// 是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority
ForceOpenaiFast bool `json:"force_openai_fast,omitempty"`
// 是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费
FreeOpenaiFast bool `json:"free_openai_fast,omitempty"`
// 仅允许非 apikey 类型账号关联到此分组
RequireOauthOnly bool `json:"require_oauth_only,omitempty"`
// 调度时仅允许 privacy 已成功设置的账号
@@ -129,13 +134,17 @@ type Group struct {
DefaultMappedModel string `json:"default_mapped_model,omitempty"`
// OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型
MessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config,omitempty"`
// 自定义 /v1/models 展示列表配置;仅影响模型列表响应,不影响调度
ModelsListConfig domain.GroupModelsListConfig `json:"models_list_config,omitempty"`
// 分组模型白名单:同时约束模型列表接口与请求准入
ModelAllowlist domain.GroupModelAllowlist `json:"model_allowlist,omitempty"`
// 固定账号获取 Codex Model Manifest 配置;开启后 /models 请求只用选定账号拉取(仅 openai 平台)
CodexModelsManifestConfig domain.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config,omitempty"`
// 分组 RPM 上限,0 表示不限制;设置后接管该分组用户的限流
RpmLimit int `json:"rpm_limit,omitempty"`
// OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
// OpenAI reasoning effort 自定义精确映射;先映射再应用上限
// 超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit,omitempty"`
// OpenAI reasoning effort 自定义映射;可按模型精确名、前缀或后缀限定,先映射再应用上限
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings,omitempty"`
// 是否启用利润控制:调度时仅允许账号计费倍率满足毛利率要求的账号进入候选池
ProfitControlEnabled bool `json:"profit_control_enabled,omitempty"`
@@ -249,15 +258,15 @@ func (*Group) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelAllowlist, group.FieldCodexModelsManifestConfig, group.FieldReasoningEffortMappings:
values[i] = new([]byte)
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldForceOpenaiFast, group.FieldFreeOpenaiFast, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
values[i] = new(sql.NullBool)
case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldBatchImageDiscountMultiplier, group.FieldBatchImageHoldMultiplier, group.FieldVideoRateMultiplier, group.FieldVideoPrice480p, group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, group.FieldSearchPricePer1k, group.FieldAudioRealtimePricePerMin, group.FieldAudioTtsPricePerMillionChars, group.FieldAudioSttPricePerHour, group.FieldProfitMinMargin, group.FieldProfitSafetyBuffer:
values[i] = new(sql.NullFloat64)
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit:
values[i] = new(sql.NullInt64)
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort:
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort, group.FieldMaxReasoningEffortOverLimit:
values[i] = new(sql.NullString)
case group.FieldCreatedAt, group.FieldUpdatedAt, group.FieldDeletedAt:
values[i] = new(sql.NullTime)
@@ -615,6 +624,18 @@ func (_m *Group) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.AllowLive = value.Bool
}
case group.FieldForceOpenaiFast:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field force_openai_fast", values[i])
} else if value.Valid {
_m.ForceOpenaiFast = value.Bool
}
case group.FieldFreeOpenaiFast:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field free_openai_fast", values[i])
} else if value.Valid {
_m.FreeOpenaiFast = value.Bool
}
case group.FieldRequireOauthOnly:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field require_oauth_only", values[i])
@@ -641,12 +662,20 @@ func (_m *Group) assignValues(columns []string, values []any) error {
return fmt.Errorf("unmarshal field messages_dispatch_model_config: %w", err)
}
}
case group.FieldModelsListConfig:
case group.FieldModelAllowlist:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field models_list_config", values[i])
return fmt.Errorf("unexpected type %T for field model_allowlist", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.ModelsListConfig); err != nil {
return fmt.Errorf("unmarshal field models_list_config: %w", err)
if err := json.Unmarshal(*value, &_m.ModelAllowlist); err != nil {
return fmt.Errorf("unmarshal field model_allowlist: %w", err)
}
}
case group.FieldCodexModelsManifestConfig:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field codex_models_manifest_config", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.CodexModelsManifestConfig); err != nil {
return fmt.Errorf("unmarshal field codex_models_manifest_config: %w", err)
}
}
case group.FieldRpmLimit:
@@ -661,6 +690,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.MaxReasoningEffort = value.String
}
case group.FieldMaxReasoningEffortOverLimit:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field max_reasoning_effort_over_limit", values[i])
} else if value.Valid {
_m.MaxReasoningEffortOverLimit = value.String
}
case group.FieldReasoningEffortMappings:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field reasoning_effort_mappings", values[i])
@@ -954,6 +989,12 @@ func (_m *Group) String() string {
builder.WriteString("allow_live=")
builder.WriteString(fmt.Sprintf("%v", _m.AllowLive))
builder.WriteString(", ")
builder.WriteString("force_openai_fast=")
builder.WriteString(fmt.Sprintf("%v", _m.ForceOpenaiFast))
builder.WriteString(", ")
builder.WriteString("free_openai_fast=")
builder.WriteString(fmt.Sprintf("%v", _m.FreeOpenaiFast))
builder.WriteString(", ")
builder.WriteString("require_oauth_only=")
builder.WriteString(fmt.Sprintf("%v", _m.RequireOauthOnly))
builder.WriteString(", ")
@@ -966,8 +1007,11 @@ func (_m *Group) String() string {
builder.WriteString("messages_dispatch_model_config=")
builder.WriteString(fmt.Sprintf("%v", _m.MessagesDispatchModelConfig))
builder.WriteString(", ")
builder.WriteString("models_list_config=")
builder.WriteString(fmt.Sprintf("%v", _m.ModelsListConfig))
builder.WriteString("model_allowlist=")
builder.WriteString(fmt.Sprintf("%v", _m.ModelAllowlist))
builder.WriteString(", ")
builder.WriteString("codex_models_manifest_config=")
builder.WriteString(fmt.Sprintf("%v", _m.CodexModelsManifestConfig))
builder.WriteString(", ")
builder.WriteString("rpm_limit=")
builder.WriteString(fmt.Sprintf("%v", _m.RpmLimit))
@@ -975,6 +1019,9 @@ func (_m *Group) String() string {
builder.WriteString("max_reasoning_effort=")
builder.WriteString(_m.MaxReasoningEffort)
builder.WriteString(", ")
builder.WriteString("max_reasoning_effort_over_limit=")
builder.WriteString(_m.MaxReasoningEffortOverLimit)
builder.WriteString(", ")
builder.WriteString("reasoning_effort_mappings=")
builder.WriteString(fmt.Sprintf("%v", _m.ReasoningEffortMappings))
builder.WriteString(", ")
+42 -5
View File
@@ -118,6 +118,10 @@ const (
FieldAllowMessagesDispatch = "allow_messages_dispatch"
// FieldAllowLive holds the string denoting the allow_live field in the database.
FieldAllowLive = "allow_live"
// FieldForceOpenaiFast holds the string denoting the force_openai_fast field in the database.
FieldForceOpenaiFast = "force_openai_fast"
// FieldFreeOpenaiFast holds the string denoting the free_openai_fast field in the database.
FieldFreeOpenaiFast = "free_openai_fast"
// FieldRequireOauthOnly holds the string denoting the require_oauth_only field in the database.
FieldRequireOauthOnly = "require_oauth_only"
// FieldRequirePrivacySet holds the string denoting the require_privacy_set field in the database.
@@ -126,12 +130,16 @@ const (
FieldDefaultMappedModel = "default_mapped_model"
// FieldMessagesDispatchModelConfig holds the string denoting the messages_dispatch_model_config field in the database.
FieldMessagesDispatchModelConfig = "messages_dispatch_model_config"
// FieldModelsListConfig holds the string denoting the models_list_config field in the database.
FieldModelsListConfig = "models_list_config"
// FieldModelAllowlist holds the string denoting the model_allowlist field in the database.
FieldModelAllowlist = "model_allowlist"
// FieldCodexModelsManifestConfig holds the string denoting the codex_models_manifest_config field in the database.
FieldCodexModelsManifestConfig = "codex_models_manifest_config"
// FieldRpmLimit holds the string denoting the rpm_limit field in the database.
FieldRpmLimit = "rpm_limit"
// FieldMaxReasoningEffort holds the string denoting the max_reasoning_effort field in the database.
FieldMaxReasoningEffort = "max_reasoning_effort"
// FieldMaxReasoningEffortOverLimit holds the string denoting the max_reasoning_effort_over_limit field in the database.
FieldMaxReasoningEffortOverLimit = "max_reasoning_effort_over_limit"
// FieldReasoningEffortMappings holds the string denoting the reasoning_effort_mappings field in the database.
FieldReasoningEffortMappings = "reasoning_effort_mappings"
// FieldProfitControlEnabled holds the string denoting the profit_control_enabled field in the database.
@@ -266,13 +274,17 @@ var Columns = []string{
FieldSortOrder,
FieldAllowMessagesDispatch,
FieldAllowLive,
FieldForceOpenaiFast,
FieldFreeOpenaiFast,
FieldRequireOauthOnly,
FieldRequirePrivacySet,
FieldDefaultMappedModel,
FieldMessagesDispatchModelConfig,
FieldModelsListConfig,
FieldModelAllowlist,
FieldCodexModelsManifestConfig,
FieldRpmLimit,
FieldMaxReasoningEffort,
FieldMaxReasoningEffortOverLimit,
FieldReasoningEffortMappings,
FieldProfitControlEnabled,
FieldProfitMinMargin,
@@ -386,6 +398,10 @@ var (
DefaultAllowMessagesDispatch bool
// DefaultAllowLive holds the default value on creation for the "allow_live" field.
DefaultAllowLive bool
// DefaultForceOpenaiFast holds the default value on creation for the "force_openai_fast" field.
DefaultForceOpenaiFast bool
// DefaultFreeOpenaiFast holds the default value on creation for the "free_openai_fast" field.
DefaultFreeOpenaiFast bool
// DefaultRequireOauthOnly holds the default value on creation for the "require_oauth_only" field.
DefaultRequireOauthOnly bool
// DefaultRequirePrivacySet holds the default value on creation for the "require_privacy_set" field.
@@ -396,14 +412,20 @@ var (
DefaultMappedModelValidator func(string) error
// DefaultMessagesDispatchModelConfig holds the default value on creation for the "messages_dispatch_model_config" field.
DefaultMessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig
// DefaultModelsListConfig holds the default value on creation for the "models_list_config" field.
DefaultModelsListConfig domain.GroupModelsListConfig
// DefaultModelAllowlist holds the default value on creation for the "model_allowlist" field.
DefaultModelAllowlist domain.GroupModelAllowlist
// DefaultCodexModelsManifestConfig holds the default value on creation for the "codex_models_manifest_config" field.
DefaultCodexModelsManifestConfig domain.GroupCodexModelsManifestConfig
// DefaultRpmLimit holds the default value on creation for the "rpm_limit" field.
DefaultRpmLimit int
// DefaultMaxReasoningEffort holds the default value on creation for the "max_reasoning_effort" field.
DefaultMaxReasoningEffort string
// MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
MaxReasoningEffortValidator func(string) error
// DefaultMaxReasoningEffortOverLimit holds the default value on creation for the "max_reasoning_effort_over_limit" field.
DefaultMaxReasoningEffortOverLimit string
// MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
MaxReasoningEffortOverLimitValidator func(string) error
// DefaultReasoningEffortMappings holds the default value on creation for the "reasoning_effort_mappings" field.
DefaultReasoningEffortMappings []domain.ReasoningEffortMapping
// DefaultProfitControlEnabled holds the default value on creation for the "profit_control_enabled" field.
@@ -657,6 +679,16 @@ func ByAllowLive(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAllowLive, opts...).ToFunc()
}
// ByForceOpenaiFast orders the results by the force_openai_fast field.
func ByForceOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldForceOpenaiFast, opts...).ToFunc()
}
// ByFreeOpenaiFast orders the results by the free_openai_fast field.
func ByFreeOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFreeOpenaiFast, opts...).ToFunc()
}
// ByRequireOauthOnly orders the results by the require_oauth_only field.
func ByRequireOauthOnly(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRequireOauthOnly, opts...).ToFunc()
@@ -682,6 +714,11 @@ func ByMaxReasoningEffort(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMaxReasoningEffort, opts...).ToFunc()
}
// ByMaxReasoningEffortOverLimit orders the results by the max_reasoning_effort_over_limit field.
func ByMaxReasoningEffortOverLimit(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMaxReasoningEffortOverLimit, opts...).ToFunc()
}
// ByProfitControlEnabled orders the results by the profit_control_enabled field.
func ByProfitControlEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldProfitControlEnabled, opts...).ToFunc()
+100
View File
@@ -290,6 +290,16 @@ func AllowLive(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldAllowLive, v))
}
// ForceOpenaiFast applies equality check predicate on the "force_openai_fast" field. It's identical to ForceOpenaiFastEQ.
func ForceOpenaiFast(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldForceOpenaiFast, v))
}
// FreeOpenaiFast applies equality check predicate on the "free_openai_fast" field. It's identical to FreeOpenaiFastEQ.
func FreeOpenaiFast(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
}
// RequireOauthOnly applies equality check predicate on the "require_oauth_only" field. It's identical to RequireOauthOnlyEQ.
func RequireOauthOnly(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
@@ -315,6 +325,11 @@ func MaxReasoningEffort(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffort, v))
}
// MaxReasoningEffortOverLimit applies equality check predicate on the "max_reasoning_effort_over_limit" field. It's identical to MaxReasoningEffortOverLimitEQ.
func MaxReasoningEffortOverLimit(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
}
// ProfitControlEnabled applies equality check predicate on the "profit_control_enabled" field. It's identical to ProfitControlEnabledEQ.
func ProfitControlEnabled(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
@@ -2270,6 +2285,26 @@ func AllowLiveNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldAllowLive, v))
}
// ForceOpenaiFastEQ applies the EQ predicate on the "force_openai_fast" field.
func ForceOpenaiFastEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldForceOpenaiFast, v))
}
// ForceOpenaiFastNEQ applies the NEQ predicate on the "force_openai_fast" field.
func ForceOpenaiFastNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldForceOpenaiFast, v))
}
// FreeOpenaiFastEQ applies the EQ predicate on the "free_openai_fast" field.
func FreeOpenaiFastEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
}
// FreeOpenaiFastNEQ applies the NEQ predicate on the "free_openai_fast" field.
func FreeOpenaiFastNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldFreeOpenaiFast, v))
}
// RequireOauthOnlyEQ applies the EQ predicate on the "require_oauth_only" field.
func RequireOauthOnlyEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
@@ -2460,6 +2495,71 @@ func MaxReasoningEffortContainsFold(v string) predicate.Group {
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffort, v))
}
// MaxReasoningEffortOverLimitEQ applies the EQ predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitEQ(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitNEQ applies the NEQ predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitNEQ(v string) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitIn applies the In predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldIn(FieldMaxReasoningEffortOverLimit, vs...))
}
// MaxReasoningEffortOverLimitNotIn applies the NotIn predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitNotIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldMaxReasoningEffortOverLimit, vs...))
}
// MaxReasoningEffortOverLimitGT applies the GT predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitGT(v string) predicate.Group {
return predicate.Group(sql.FieldGT(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitGTE applies the GTE predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitGTE(v string) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitLT applies the LT predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitLT(v string) predicate.Group {
return predicate.Group(sql.FieldLT(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitLTE applies the LTE predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitLTE(v string) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitContains applies the Contains predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitContains(v string) predicate.Group {
return predicate.Group(sql.FieldContains(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitHasPrefix applies the HasPrefix predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitHasPrefix(v string) predicate.Group {
return predicate.Group(sql.FieldHasPrefix(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitHasSuffix applies the HasSuffix predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitHasSuffix(v string) predicate.Group {
return predicate.Group(sql.FieldHasSuffix(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitEqualFold applies the EqualFold predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitEqualFold(v string) predicate.Group {
return predicate.Group(sql.FieldEqualFold(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitContainsFold applies the ContainsFold predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitContainsFold(v string) predicate.Group {
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffortOverLimit, v))
}
// ProfitControlEnabledEQ applies the EQ predicate on the "profit_control_enabled" field.
func ProfitControlEnabledEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
+302 -37
View File
@@ -4,7 +4,7 @@ package ent
import (
"context"
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"time"
@@ -575,7 +575,7 @@ func (_c *GroupCreate) SetNillableLongContextPricingEnabled(v *bool) *GroupCreat
}
// SetModelPricing sets the "model_pricing" field.
func (_c *GroupCreate) SetModelPricing(v json.RawMessage) *GroupCreate {
func (_c *GroupCreate) SetModelPricing(v jsontext.Value) *GroupCreate {
_c.mutation.SetModelPricing(v)
return _c
}
@@ -704,6 +704,34 @@ func (_c *GroupCreate) SetNillableAllowLive(v *bool) *GroupCreate {
return _c
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (_c *GroupCreate) SetForceOpenaiFast(v bool) *GroupCreate {
_c.mutation.SetForceOpenaiFast(v)
return _c
}
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
func (_c *GroupCreate) SetNillableForceOpenaiFast(v *bool) *GroupCreate {
if v != nil {
_c.SetForceOpenaiFast(*v)
}
return _c
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_c *GroupCreate) SetFreeOpenaiFast(v bool) *GroupCreate {
_c.mutation.SetFreeOpenaiFast(v)
return _c
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_c *GroupCreate) SetNillableFreeOpenaiFast(v *bool) *GroupCreate {
if v != nil {
_c.SetFreeOpenaiFast(*v)
}
return _c
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_c *GroupCreate) SetRequireOauthOnly(v bool) *GroupCreate {
_c.mutation.SetRequireOauthOnly(v)
@@ -760,16 +788,30 @@ func (_c *GroupCreate) SetNillableMessagesDispatchModelConfig(v *domain.OpenAIMe
return _c
}
// SetModelsListConfig sets the "models_list_config" field.
func (_c *GroupCreate) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupCreate {
_c.mutation.SetModelsListConfig(v)
// SetModelAllowlist sets the "model_allowlist" field.
func (_c *GroupCreate) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupCreate {
_c.mutation.SetModelAllowlist(v)
return _c
}
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
func (_c *GroupCreate) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupCreate {
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
func (_c *GroupCreate) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupCreate {
if v != nil {
_c.SetModelsListConfig(*v)
_c.SetModelAllowlist(*v)
}
return _c
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (_c *GroupCreate) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupCreate {
_c.mutation.SetCodexModelsManifestConfig(v)
return _c
}
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
func (_c *GroupCreate) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupCreate {
if v != nil {
_c.SetCodexModelsManifestConfig(*v)
}
return _c
}
@@ -802,6 +844,20 @@ func (_c *GroupCreate) SetNillableMaxReasoningEffort(v *string) *GroupCreate {
return _c
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_c *GroupCreate) SetMaxReasoningEffortOverLimit(v string) *GroupCreate {
_c.mutation.SetMaxReasoningEffortOverLimit(v)
return _c
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_c *GroupCreate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupCreate {
if v != nil {
_c.SetMaxReasoningEffortOverLimit(*v)
}
return _c
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_c *GroupCreate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupCreate {
_c.mutation.SetReasoningEffortMappings(v)
@@ -1095,6 +1151,14 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultAllowLive
_c.mutation.SetAllowLive(v)
}
if _, ok := _c.mutation.ForceOpenaiFast(); !ok {
v := group.DefaultForceOpenaiFast
_c.mutation.SetForceOpenaiFast(v)
}
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
v := group.DefaultFreeOpenaiFast
_c.mutation.SetFreeOpenaiFast(v)
}
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
v := group.DefaultRequireOauthOnly
_c.mutation.SetRequireOauthOnly(v)
@@ -1111,9 +1175,13 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultMessagesDispatchModelConfig
_c.mutation.SetMessagesDispatchModelConfig(v)
}
if _, ok := _c.mutation.ModelsListConfig(); !ok {
v := group.DefaultModelsListConfig
_c.mutation.SetModelsListConfig(v)
if _, ok := _c.mutation.ModelAllowlist(); !ok {
v := group.DefaultModelAllowlist
_c.mutation.SetModelAllowlist(v)
}
if _, ok := _c.mutation.CodexModelsManifestConfig(); !ok {
v := group.DefaultCodexModelsManifestConfig
_c.mutation.SetCodexModelsManifestConfig(v)
}
if _, ok := _c.mutation.RpmLimit(); !ok {
v := group.DefaultRpmLimit
@@ -1123,6 +1191,10 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultMaxReasoningEffort
_c.mutation.SetMaxReasoningEffort(v)
}
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
v := group.DefaultMaxReasoningEffortOverLimit
_c.mutation.SetMaxReasoningEffortOverLimit(v)
}
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
v := group.DefaultReasoningEffortMappings
_c.mutation.SetReasoningEffortMappings(v)
@@ -1286,6 +1358,12 @@ func (_c *GroupCreate) check() error {
if _, ok := _c.mutation.AllowLive(); !ok {
return &ValidationError{Name: "allow_live", err: errors.New(`ent: missing required field "Group.allow_live"`)}
}
if _, ok := _c.mutation.ForceOpenaiFast(); !ok {
return &ValidationError{Name: "force_openai_fast", err: errors.New(`ent: missing required field "Group.force_openai_fast"`)}
}
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
return &ValidationError{Name: "free_openai_fast", err: errors.New(`ent: missing required field "Group.free_openai_fast"`)}
}
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
return &ValidationError{Name: "require_oauth_only", err: errors.New(`ent: missing required field "Group.require_oauth_only"`)}
}
@@ -1303,8 +1381,11 @@ func (_c *GroupCreate) check() error {
if _, ok := _c.mutation.MessagesDispatchModelConfig(); !ok {
return &ValidationError{Name: "messages_dispatch_model_config", err: errors.New(`ent: missing required field "Group.messages_dispatch_model_config"`)}
}
if _, ok := _c.mutation.ModelsListConfig(); !ok {
return &ValidationError{Name: "models_list_config", err: errors.New(`ent: missing required field "Group.models_list_config"`)}
if _, ok := _c.mutation.ModelAllowlist(); !ok {
return &ValidationError{Name: "model_allowlist", err: errors.New(`ent: missing required field "Group.model_allowlist"`)}
}
if _, ok := _c.mutation.CodexModelsManifestConfig(); !ok {
return &ValidationError{Name: "codex_models_manifest_config", err: errors.New(`ent: missing required field "Group.codex_models_manifest_config"`)}
}
if _, ok := _c.mutation.RpmLimit(); !ok {
return &ValidationError{Name: "rpm_limit", err: errors.New(`ent: missing required field "Group.rpm_limit"`)}
@@ -1317,6 +1398,14 @@ func (_c *GroupCreate) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: errors.New(`ent: missing required field "Group.max_reasoning_effort_over_limit"`)}
}
if v, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
return &ValidationError{Name: "reasoning_effort_mappings", err: errors.New(`ent: missing required field "Group.reasoning_effort_mappings"`)}
}
@@ -1560,6 +1649,14 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
_node.AllowLive = value
}
if value, ok := _c.mutation.ForceOpenaiFast(); ok {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
_node.ForceOpenaiFast = value
}
if value, ok := _c.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
_node.FreeOpenaiFast = value
}
if value, ok := _c.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
_node.RequireOauthOnly = value
@@ -1576,9 +1673,13 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
_node.MessagesDispatchModelConfig = value
}
if value, ok := _c.mutation.ModelsListConfig(); ok {
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
_node.ModelsListConfig = value
if value, ok := _c.mutation.ModelAllowlist(); ok {
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
_node.ModelAllowlist = value
}
if value, ok := _c.mutation.CodexModelsManifestConfig(); ok {
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
_node.CodexModelsManifestConfig = value
}
if value, ok := _c.mutation.RpmLimit(); ok {
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
@@ -1588,6 +1689,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
_node.MaxReasoningEffort = value
}
if value, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
_node.MaxReasoningEffortOverLimit = value
}
if value, ok := _c.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
_node.ReasoningEffortMappings = value
@@ -2445,7 +2550,7 @@ func (u *GroupUpsert) UpdateLongContextPricingEnabled() *GroupUpsert {
}
// SetModelPricing sets the "model_pricing" field.
func (u *GroupUpsert) SetModelPricing(v json.RawMessage) *GroupUpsert {
func (u *GroupUpsert) SetModelPricing(v jsontext.Value) *GroupUpsert {
u.Set(group.FieldModelPricing, v)
return u
}
@@ -2618,6 +2723,30 @@ func (u *GroupUpsert) UpdateAllowLive() *GroupUpsert {
return u
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (u *GroupUpsert) SetForceOpenaiFast(v bool) *GroupUpsert {
u.Set(group.FieldForceOpenaiFast, v)
return u
}
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
func (u *GroupUpsert) UpdateForceOpenaiFast() *GroupUpsert {
u.SetExcluded(group.FieldForceOpenaiFast)
return u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsert) SetFreeOpenaiFast(v bool) *GroupUpsert {
u.Set(group.FieldFreeOpenaiFast, v)
return u
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsert) UpdateFreeOpenaiFast() *GroupUpsert {
u.SetExcluded(group.FieldFreeOpenaiFast)
return u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsert) SetRequireOauthOnly(v bool) *GroupUpsert {
u.Set(group.FieldRequireOauthOnly, v)
@@ -2666,15 +2795,27 @@ func (u *GroupUpsert) UpdateMessagesDispatchModelConfig() *GroupUpsert {
return u
}
// SetModelsListConfig sets the "models_list_config" field.
func (u *GroupUpsert) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsert {
u.Set(group.FieldModelsListConfig, v)
// SetModelAllowlist sets the "model_allowlist" field.
func (u *GroupUpsert) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsert {
u.Set(group.FieldModelAllowlist, v)
return u
}
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
func (u *GroupUpsert) UpdateModelsListConfig() *GroupUpsert {
u.SetExcluded(group.FieldModelsListConfig)
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
func (u *GroupUpsert) UpdateModelAllowlist() *GroupUpsert {
u.SetExcluded(group.FieldModelAllowlist)
return u
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (u *GroupUpsert) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsert {
u.Set(group.FieldCodexModelsManifestConfig, v)
return u
}
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
func (u *GroupUpsert) UpdateCodexModelsManifestConfig() *GroupUpsert {
u.SetExcluded(group.FieldCodexModelsManifestConfig)
return u
}
@@ -2708,6 +2849,18 @@ func (u *GroupUpsert) UpdateMaxReasoningEffort() *GroupUpsert {
return u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsert) SetMaxReasoningEffortOverLimit(v string) *GroupUpsert {
u.Set(group.FieldMaxReasoningEffortOverLimit, v)
return u
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsert) UpdateMaxReasoningEffortOverLimit() *GroupUpsert {
u.SetExcluded(group.FieldMaxReasoningEffortOverLimit)
return u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsert) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsert {
u.Set(group.FieldReasoningEffortMappings, v)
@@ -3615,7 +3768,7 @@ func (u *GroupUpsertOne) UpdateLongContextPricingEnabled() *GroupUpsertOne {
}
// SetModelPricing sets the "model_pricing" field.
func (u *GroupUpsertOne) SetModelPricing(v json.RawMessage) *GroupUpsertOne {
func (u *GroupUpsertOne) SetModelPricing(v jsontext.Value) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetModelPricing(v)
})
@@ -3817,6 +3970,34 @@ func (u *GroupUpsertOne) UpdateAllowLive() *GroupUpsertOne {
})
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (u *GroupUpsertOne) SetForceOpenaiFast(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetForceOpenaiFast(v)
})
}
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateForceOpenaiFast() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateForceOpenaiFast()
})
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsertOne) SetFreeOpenaiFast(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetFreeOpenaiFast(v)
})
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateFreeOpenaiFast() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateFreeOpenaiFast()
})
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsertOne) SetRequireOauthOnly(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
@@ -3873,17 +4054,31 @@ func (u *GroupUpsertOne) UpdateMessagesDispatchModelConfig() *GroupUpsertOne {
})
}
// SetModelsListConfig sets the "models_list_config" field.
func (u *GroupUpsertOne) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsertOne {
// SetModelAllowlist sets the "model_allowlist" field.
func (u *GroupUpsertOne) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetModelsListConfig(v)
s.SetModelAllowlist(v)
})
}
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateModelsListConfig() *GroupUpsertOne {
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateModelAllowlist() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateModelsListConfig()
s.UpdateModelAllowlist()
})
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (u *GroupUpsertOne) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetCodexModelsManifestConfig(v)
})
}
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateCodexModelsManifestConfig() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateCodexModelsManifestConfig()
})
}
@@ -3922,6 +4117,20 @@ func (u *GroupUpsertOne) UpdateMaxReasoningEffort() *GroupUpsertOne {
})
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsertOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetMaxReasoningEffortOverLimit(v)
})
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateMaxReasoningEffortOverLimit() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateMaxReasoningEffortOverLimit()
})
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsertOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
@@ -5005,7 +5214,7 @@ func (u *GroupUpsertBulk) UpdateLongContextPricingEnabled() *GroupUpsertBulk {
}
// SetModelPricing sets the "model_pricing" field.
func (u *GroupUpsertBulk) SetModelPricing(v json.RawMessage) *GroupUpsertBulk {
func (u *GroupUpsertBulk) SetModelPricing(v jsontext.Value) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetModelPricing(v)
})
@@ -5207,6 +5416,34 @@ func (u *GroupUpsertBulk) UpdateAllowLive() *GroupUpsertBulk {
})
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (u *GroupUpsertBulk) SetForceOpenaiFast(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetForceOpenaiFast(v)
})
}
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateForceOpenaiFast() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateForceOpenaiFast()
})
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsertBulk) SetFreeOpenaiFast(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetFreeOpenaiFast(v)
})
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateFreeOpenaiFast() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateFreeOpenaiFast()
})
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsertBulk) SetRequireOauthOnly(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
@@ -5263,17 +5500,31 @@ func (u *GroupUpsertBulk) UpdateMessagesDispatchModelConfig() *GroupUpsertBulk {
})
}
// SetModelsListConfig sets the "models_list_config" field.
func (u *GroupUpsertBulk) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsertBulk {
// SetModelAllowlist sets the "model_allowlist" field.
func (u *GroupUpsertBulk) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetModelsListConfig(v)
s.SetModelAllowlist(v)
})
}
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateModelsListConfig() *GroupUpsertBulk {
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateModelAllowlist() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateModelsListConfig()
s.UpdateModelAllowlist()
})
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (u *GroupUpsertBulk) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetCodexModelsManifestConfig(v)
})
}
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateCodexModelsManifestConfig() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateCodexModelsManifestConfig()
})
}
@@ -5312,6 +5563,20 @@ func (u *GroupUpsertBulk) UpdateMaxReasoningEffort() *GroupUpsertBulk {
})
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsertBulk) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetMaxReasoningEffortOverLimit(v)
})
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateMaxReasoningEffortOverLimit() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateMaxReasoningEffortOverLimit()
})
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsertBulk) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
+167 -21
View File
@@ -4,7 +4,7 @@ package ent
import (
"context"
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"time"
@@ -803,13 +803,13 @@ func (_u *GroupUpdate) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdat
}
// SetModelPricing sets the "model_pricing" field.
func (_u *GroupUpdate) SetModelPricing(v json.RawMessage) *GroupUpdate {
func (_u *GroupUpdate) SetModelPricing(v jsontext.Value) *GroupUpdate {
_u.mutation.SetModelPricing(v)
return _u
}
// AppendModelPricing appends value to the "model_pricing" field.
func (_u *GroupUpdate) AppendModelPricing(v json.RawMessage) *GroupUpdate {
func (_u *GroupUpdate) AppendModelPricing(v jsontext.Value) *GroupUpdate {
_u.mutation.AppendModelPricing(v)
return _u
}
@@ -989,6 +989,34 @@ func (_u *GroupUpdate) SetNillableAllowLive(v *bool) *GroupUpdate {
return _u
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (_u *GroupUpdate) SetForceOpenaiFast(v bool) *GroupUpdate {
_u.mutation.SetForceOpenaiFast(v)
return _u
}
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableForceOpenaiFast(v *bool) *GroupUpdate {
if v != nil {
_u.SetForceOpenaiFast(*v)
}
return _u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_u *GroupUpdate) SetFreeOpenaiFast(v bool) *GroupUpdate {
_u.mutation.SetFreeOpenaiFast(v)
return _u
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableFreeOpenaiFast(v *bool) *GroupUpdate {
if v != nil {
_u.SetFreeOpenaiFast(*v)
}
return _u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_u *GroupUpdate) SetRequireOauthOnly(v bool) *GroupUpdate {
_u.mutation.SetRequireOauthOnly(v)
@@ -1045,16 +1073,30 @@ func (_u *GroupUpdate) SetNillableMessagesDispatchModelConfig(v *domain.OpenAIMe
return _u
}
// SetModelsListConfig sets the "models_list_config" field.
func (_u *GroupUpdate) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpdate {
_u.mutation.SetModelsListConfig(v)
// SetModelAllowlist sets the "model_allowlist" field.
func (_u *GroupUpdate) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpdate {
_u.mutation.SetModelAllowlist(v)
return _u
}
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupUpdate {
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupUpdate {
if v != nil {
_u.SetModelsListConfig(*v)
_u.SetModelAllowlist(*v)
}
return _u
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (_u *GroupUpdate) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpdate {
_u.mutation.SetCodexModelsManifestConfig(v)
return _u
}
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupUpdate {
if v != nil {
_u.SetCodexModelsManifestConfig(*v)
}
return _u
}
@@ -1094,6 +1136,20 @@ func (_u *GroupUpdate) SetNillableMaxReasoningEffort(v *string) *GroupUpdate {
return _u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_u *GroupUpdate) SetMaxReasoningEffortOverLimit(v string) *GroupUpdate {
_u.mutation.SetMaxReasoningEffortOverLimit(v)
return _u
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdate {
if v != nil {
_u.SetMaxReasoningEffortOverLimit(*v)
}
return _u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_u *GroupUpdate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdate {
_u.mutation.SetReasoningEffortMappings(v)
@@ -1487,6 +1543,11 @@ func (_u *GroupUpdate) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
return nil
}
@@ -1797,6 +1858,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.AllowLive(); ok {
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
}
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
}
@@ -1809,8 +1876,11 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.MessagesDispatchModelConfig(); ok {
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
}
if value, ok := _u.mutation.ModelsListConfig(); ok {
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
if value, ok := _u.mutation.ModelAllowlist(); ok {
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
}
if value, ok := _u.mutation.CodexModelsManifestConfig(); ok {
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
}
if value, ok := _u.mutation.RpmLimit(); ok {
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
@@ -1821,6 +1891,9 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
}
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
}
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
}
@@ -2924,13 +2997,13 @@ func (_u *GroupUpdateOne) SetNillableLongContextPricingEnabled(v *bool) *GroupUp
}
// SetModelPricing sets the "model_pricing" field.
func (_u *GroupUpdateOne) SetModelPricing(v json.RawMessage) *GroupUpdateOne {
func (_u *GroupUpdateOne) SetModelPricing(v jsontext.Value) *GroupUpdateOne {
_u.mutation.SetModelPricing(v)
return _u
}
// AppendModelPricing appends value to the "model_pricing" field.
func (_u *GroupUpdateOne) AppendModelPricing(v json.RawMessage) *GroupUpdateOne {
func (_u *GroupUpdateOne) AppendModelPricing(v jsontext.Value) *GroupUpdateOne {
_u.mutation.AppendModelPricing(v)
return _u
}
@@ -3110,6 +3183,34 @@ func (_u *GroupUpdateOne) SetNillableAllowLive(v *bool) *GroupUpdateOne {
return _u
}
// SetForceOpenaiFast sets the "force_openai_fast" field.
func (_u *GroupUpdateOne) SetForceOpenaiFast(v bool) *GroupUpdateOne {
_u.mutation.SetForceOpenaiFast(v)
return _u
}
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableForceOpenaiFast(v *bool) *GroupUpdateOne {
if v != nil {
_u.SetForceOpenaiFast(*v)
}
return _u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_u *GroupUpdateOne) SetFreeOpenaiFast(v bool) *GroupUpdateOne {
_u.mutation.SetFreeOpenaiFast(v)
return _u
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableFreeOpenaiFast(v *bool) *GroupUpdateOne {
if v != nil {
_u.SetFreeOpenaiFast(*v)
}
return _u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_u *GroupUpdateOne) SetRequireOauthOnly(v bool) *GroupUpdateOne {
_u.mutation.SetRequireOauthOnly(v)
@@ -3166,16 +3267,30 @@ func (_u *GroupUpdateOne) SetNillableMessagesDispatchModelConfig(v *domain.OpenA
return _u
}
// SetModelsListConfig sets the "models_list_config" field.
func (_u *GroupUpdateOne) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpdateOne {
_u.mutation.SetModelsListConfig(v)
// SetModelAllowlist sets the "model_allowlist" field.
func (_u *GroupUpdateOne) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpdateOne {
_u.mutation.SetModelAllowlist(v)
return _u
}
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupUpdateOne {
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupUpdateOne {
if v != nil {
_u.SetModelsListConfig(*v)
_u.SetModelAllowlist(*v)
}
return _u
}
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
func (_u *GroupUpdateOne) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpdateOne {
_u.mutation.SetCodexModelsManifestConfig(v)
return _u
}
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupUpdateOne {
if v != nil {
_u.SetCodexModelsManifestConfig(*v)
}
return _u
}
@@ -3215,6 +3330,20 @@ func (_u *GroupUpdateOne) SetNillableMaxReasoningEffort(v *string) *GroupUpdateO
return _u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_u *GroupUpdateOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpdateOne {
_u.mutation.SetMaxReasoningEffortOverLimit(v)
return _u
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdateOne {
if v != nil {
_u.SetMaxReasoningEffortOverLimit(*v)
}
return _u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_u *GroupUpdateOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdateOne {
_u.mutation.SetReasoningEffortMappings(v)
@@ -3621,6 +3750,11 @@ func (_u *GroupUpdateOne) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
return nil
}
@@ -3948,6 +4082,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.AllowLive(); ok {
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
}
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
}
@@ -3960,8 +4100,11 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.MessagesDispatchModelConfig(); ok {
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
}
if value, ok := _u.mutation.ModelsListConfig(); ok {
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
if value, ok := _u.mutation.ModelAllowlist(); ok {
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
}
if value, ok := _u.mutation.CodexModelsManifestConfig(); ok {
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
}
if value, ok := _u.mutation.RpmLimit(); ok {
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
@@ -3972,6 +4115,9 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
}
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
}
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
}
+25 -12
View File
@@ -623,7 +623,9 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax"}},
{Name: "check_mode", Type: field.TypeString, Size: 32, Default: "probe"},
{Name: "account_id", Type: field.TypeInt64, Nullable: true},
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
{Name: "endpoint", Type: field.TypeString, Size: 500},
{Name: "api_key_encrypted", Type: field.TypeString},
@@ -648,7 +650,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "channel_monitors_channel_monitor_request_templates_request_template",
Columns: []*schema.Column{ChannelMonitorsColumns[19]},
Columns: []*schema.Column{ChannelMonitorsColumns[21]},
RefColumns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[0]},
OnDelete: schema.SetNull,
},
@@ -657,7 +659,7 @@ var (
{
Name: "channelmonitor_enabled_last_checked_at",
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[11], ChannelMonitorsColumns[14]},
Columns: []*schema.Column{ChannelMonitorsColumns[13], ChannelMonitorsColumns[16]},
},
{
Name: "channelmonitor_provider",
@@ -667,17 +669,22 @@ var (
{
Name: "channelmonitor_provider_api_mode",
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[4], ChannelMonitorsColumns[5]},
Columns: []*schema.Column{ChannelMonitorsColumns[4], ChannelMonitorsColumns[7]},
},
{
Name: "channelmonitor_group_name",
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[10]},
Columns: []*schema.Column{ChannelMonitorsColumns[12]},
},
{
Name: "channelmonitor_template_id",
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[19]},
Columns: []*schema.Column{ChannelMonitorsColumns[21]},
},
{
Name: "channelmonitor_account_id",
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[6]},
},
},
}
@@ -733,6 +740,7 @@ var (
{Name: "latency_ms", Type: field.TypeInt, Nullable: true},
{Name: "ping_latency_ms", Type: field.TypeInt, Nullable: true},
{Name: "message", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
{Name: "quota", Type: field.TypeJSON, Nullable: true},
{Name: "checked_at", Type: field.TypeTime},
{Name: "monitor_id", Type: field.TypeInt64},
}
@@ -744,7 +752,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "channel_monitor_histories_channel_monitors_history",
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7]},
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[8]},
RefColumns: []*schema.Column{ChannelMonitorsColumns[0]},
OnDelete: schema.Cascade,
},
@@ -753,12 +761,12 @@ var (
{
Name: "channelmonitorhistory_monitor_id_model_checked_at",
Unique: false,
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7], ChannelMonitorHistoriesColumns[1], ChannelMonitorHistoriesColumns[6]},
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[8], ChannelMonitorHistoriesColumns[1], ChannelMonitorHistoriesColumns[7]},
},
{
Name: "channelmonitorhistory_checked_at",
Unique: false,
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[6]},
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7]},
},
},
}
@@ -768,7 +776,7 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax"}},
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
{Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
{Name: "extra_headers", Type: field.TypeJSON},
@@ -946,13 +954,17 @@ var (
{Name: "sort_order", Type: field.TypeInt, Default: 0},
{Name: "allow_messages_dispatch", Type: field.TypeBool, Default: false},
{Name: "allow_live", Type: field.TypeBool, Default: false},
{Name: "force_openai_fast", Type: field.TypeBool, Default: false},
{Name: "free_openai_fast", Type: field.TypeBool, Default: false},
{Name: "require_oauth_only", Type: field.TypeBool, Default: false},
{Name: "require_privacy_set", Type: field.TypeBool, Default: false},
{Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""},
{Name: "messages_dispatch_model_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "models_list_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "model_allowlist", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "codex_models_manifest_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "rpm_limit", Type: field.TypeInt, Default: 0},
{Name: "max_reasoning_effort", Type: field.TypeString, Size: 20, Default: ""},
{Name: "max_reasoning_effort_over_limit", Type: field.TypeString, Size: 20, Default: "downgrade"},
{Name: "reasoning_effort_mappings", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "profit_control_enabled", Type: field.TypeBool, Default: false},
{Name: "profit_min_margin", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
@@ -1401,7 +1413,7 @@ var (
{Name: "expires_at", Type: field.TypeTime, Nullable: true},
{Name: "fallback_mode", Type: field.TypeString, Size: 20, Default: "none"},
{Name: "expiry_warn_days", Type: field.TypeInt, Default: 7},
{Name: "backup_proxy_id", Type: field.TypeInt64, Unique: true, Nullable: true},
{Name: "backup_proxy_id", Type: field.TypeInt64, Nullable: true},
}
// ProxiesTable holds the schema information for the "proxies" table.
ProxiesTable = &schema.Table{
@@ -1793,6 +1805,7 @@ var (
{Name: "signup_source", Type: field.TypeString, Default: "email"},
{Name: "last_login_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "last_active_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "restrict_public_groups", Type: field.TypeBool, Default: false},
{Name: "balance_notify_enabled", Type: field.TypeBool, Default: true},
{Name: "balance_notify_threshold_type", Type: field.TypeString, Default: "fixed"},
{Name: "balance_notify_threshold", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
+671 -84
View File
File diff suppressed because it is too large Load Diff
+18 -2
View File
@@ -55,11 +55,13 @@ type Proxy struct {
type ProxyEdges struct {
// Accounts holds the value of the accounts edge.
Accounts []*Account `json:"accounts,omitempty"`
// PrimaryProxies holds the value of the primary_proxies edge.
PrimaryProxies []*Proxy `json:"primary_proxies,omitempty"`
// BackupProxy holds the value of the backup_proxy edge.
BackupProxy *Proxy `json:"backup_proxy,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [2]bool
loadedTypes [3]bool
}
// AccountsOrErr returns the Accounts value or an error if the edge
@@ -71,12 +73,21 @@ func (e ProxyEdges) AccountsOrErr() ([]*Account, error) {
return nil, &NotLoadedError{edge: "accounts"}
}
// PrimaryProxiesOrErr returns the PrimaryProxies value or an error if the edge
// was not loaded in eager-loading.
func (e ProxyEdges) PrimaryProxiesOrErr() ([]*Proxy, error) {
if e.loadedTypes[1] {
return e.PrimaryProxies, nil
}
return nil, &NotLoadedError{edge: "primary_proxies"}
}
// BackupProxyOrErr returns the BackupProxy value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e ProxyEdges) BackupProxyOrErr() (*Proxy, error) {
if e.BackupProxy != nil {
return e.BackupProxy, nil
} else if e.loadedTypes[1] {
} else if e.loadedTypes[2] {
return nil, &NotFoundError{label: proxy.Label}
}
return nil, &NotLoadedError{edge: "backup_proxy"}
@@ -221,6 +232,11 @@ func (_m *Proxy) QueryAccounts() *AccountQuery {
return NewProxyClient(_m.config).QueryAccounts(_m)
}
// QueryPrimaryProxies queries the "primary_proxies" edge of the Proxy entity.
func (_m *Proxy) QueryPrimaryProxies() *ProxyQuery {
return NewProxyClient(_m.config).QueryPrimaryProxies(_m)
}
// QueryBackupProxy queries the "backup_proxy" edge of the Proxy entity.
func (_m *Proxy) QueryBackupProxy() *ProxyQuery {
return NewProxyClient(_m.config).QueryBackupProxy(_m)
+28 -1
View File
@@ -45,6 +45,8 @@ const (
FieldExpiryWarnDays = "expiry_warn_days"
// EdgeAccounts holds the string denoting the accounts edge name in mutations.
EdgeAccounts = "accounts"
// EdgePrimaryProxies holds the string denoting the primary_proxies edge name in mutations.
EdgePrimaryProxies = "primary_proxies"
// EdgeBackupProxy holds the string denoting the backup_proxy edge name in mutations.
EdgeBackupProxy = "backup_proxy"
// Table holds the table name of the proxy in the database.
@@ -56,6 +58,10 @@ const (
AccountsInverseTable = "accounts"
// AccountsColumn is the table column denoting the accounts relation/edge.
AccountsColumn = "proxy_id"
// PrimaryProxiesTable is the table that holds the primary_proxies relation/edge.
PrimaryProxiesTable = "proxies"
// PrimaryProxiesColumn is the table column denoting the primary_proxies relation/edge.
PrimaryProxiesColumn = "backup_proxy_id"
// BackupProxyTable is the table that holds the backup_proxy relation/edge.
BackupProxyTable = "proxies"
// BackupProxyColumn is the table column denoting the backup_proxy relation/edge.
@@ -219,6 +225,20 @@ func ByAccounts(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
}
}
// ByPrimaryProxiesCount orders the results by primary_proxies count.
func ByPrimaryProxiesCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newPrimaryProxiesStep(), opts...)
}
}
// ByPrimaryProxies orders the results by primary_proxies terms.
func ByPrimaryProxies(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newPrimaryProxiesStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
// ByBackupProxyField orders the results by backup_proxy field.
func ByBackupProxyField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
@@ -232,10 +252,17 @@ func newAccountsStep() *sqlgraph.Step {
sqlgraph.Edge(sqlgraph.O2M, true, AccountsTable, AccountsColumn),
)
}
func newPrimaryProxiesStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, PrimaryProxiesTable, PrimaryProxiesColumn),
)
}
func newBackupProxyStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn),
sqlgraph.Edge(sqlgraph.M2O, false, BackupProxyTable, BackupProxyColumn),
)
}
+24 -1
View File
@@ -913,12 +913,35 @@ func HasAccountsWith(preds ...predicate.Account) predicate.Proxy {
})
}
// HasPrimaryProxies applies the HasEdge predicate on the "primary_proxies" edge.
func HasPrimaryProxies() predicate.Proxy {
return predicate.Proxy(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, PrimaryProxiesTable, PrimaryProxiesColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasPrimaryProxiesWith applies the HasEdge predicate on the "primary_proxies" edge with a given conditions (other predicates).
func HasPrimaryProxiesWith(preds ...predicate.Proxy) predicate.Proxy {
return predicate.Proxy(func(s *sql.Selector) {
step := newPrimaryProxiesStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasBackupProxy applies the HasEdge predicate on the "backup_proxy" edge.
func HasBackupProxy() predicate.Proxy {
return predicate.Proxy(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn),
sqlgraph.Edge(sqlgraph.M2O, false, BackupProxyTable, BackupProxyColumn),
)
sqlgraph.HasNeighbors(s, step)
})
+33 -2
View File
@@ -202,6 +202,21 @@ func (_c *ProxyCreate) AddAccounts(v ...*Account) *ProxyCreate {
return _c.AddAccountIDs(ids...)
}
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
func (_c *ProxyCreate) AddPrimaryProxyIDs(ids ...int64) *ProxyCreate {
_c.mutation.AddPrimaryProxyIDs(ids...)
return _c
}
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
func (_c *ProxyCreate) AddPrimaryProxies(v ...*Proxy) *ProxyCreate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddPrimaryProxyIDs(ids...)
}
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
func (_c *ProxyCreate) SetBackupProxy(v *Proxy) *ProxyCreate {
return _c.SetBackupProxyID(v.ID)
@@ -432,13 +447,29 @@ func (_c *ProxyCreate) createSpec() (*Proxy, *sqlgraph.CreateSpec) {
}
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.BackupProxyIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Rel: sqlgraph.M2O,
Inverse: false,
Table: proxy.BackupProxyTable,
Columns: []string{proxy.BackupProxyColumn},
Bidi: true,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
+92 -16
View File
@@ -21,13 +21,14 @@ import (
// ProxyQuery is the builder for querying Proxy entities.
type ProxyQuery struct {
config
ctx *QueryContext
order []proxy.OrderOption
inters []Interceptor
predicates []predicate.Proxy
withAccounts *AccountQuery
withBackupProxy *ProxyQuery
modifiers []func(*sql.Selector)
ctx *QueryContext
order []proxy.OrderOption
inters []Interceptor
predicates []predicate.Proxy
withAccounts *AccountQuery
withPrimaryProxies *ProxyQuery
withBackupProxy *ProxyQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -86,6 +87,28 @@ func (_q *ProxyQuery) QueryAccounts() *AccountQuery {
return query
}
// QueryPrimaryProxies chains the current query on the "primary_proxies" edge.
func (_q *ProxyQuery) QueryPrimaryProxies() *ProxyQuery {
query := (&ProxyClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(proxy.Table, proxy.FieldID, selector),
sqlgraph.To(proxy.Table, proxy.FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, proxy.PrimaryProxiesTable, proxy.PrimaryProxiesColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryBackupProxy chains the current query on the "backup_proxy" edge.
func (_q *ProxyQuery) QueryBackupProxy() *ProxyQuery {
query := (&ProxyClient{config: _q.config}).Query()
@@ -100,7 +123,7 @@ func (_q *ProxyQuery) QueryBackupProxy() *ProxyQuery {
step := sqlgraph.NewStep(
sqlgraph.From(proxy.Table, proxy.FieldID, selector),
sqlgraph.To(proxy.Table, proxy.FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
sqlgraph.Edge(sqlgraph.M2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
@@ -295,13 +318,14 @@ func (_q *ProxyQuery) Clone() *ProxyQuery {
return nil
}
return &ProxyQuery{
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]proxy.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.Proxy{}, _q.predicates...),
withAccounts: _q.withAccounts.Clone(),
withBackupProxy: _q.withBackupProxy.Clone(),
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]proxy.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.Proxy{}, _q.predicates...),
withAccounts: _q.withAccounts.Clone(),
withPrimaryProxies: _q.withPrimaryProxies.Clone(),
withBackupProxy: _q.withBackupProxy.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
path: _q.path,
@@ -319,6 +343,17 @@ func (_q *ProxyQuery) WithAccounts(opts ...func(*AccountQuery)) *ProxyQuery {
return _q
}
// WithPrimaryProxies tells the query-builder to eager-load the nodes that are connected to
// the "primary_proxies" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *ProxyQuery) WithPrimaryProxies(opts ...func(*ProxyQuery)) *ProxyQuery {
query := (&ProxyClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withPrimaryProxies = query
return _q
}
// WithBackupProxy tells the query-builder to eager-load the nodes that are connected to
// the "backup_proxy" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *ProxyQuery) WithBackupProxy(opts ...func(*ProxyQuery)) *ProxyQuery {
@@ -408,8 +443,9 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy,
var (
nodes = []*Proxy{}
_spec = _q.querySpec()
loadedTypes = [2]bool{
loadedTypes = [3]bool{
_q.withAccounts != nil,
_q.withPrimaryProxies != nil,
_q.withBackupProxy != nil,
}
)
@@ -441,6 +477,13 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy,
return nil, err
}
}
if query := _q.withPrimaryProxies; query != nil {
if err := _q.loadPrimaryProxies(ctx, query, nodes,
func(n *Proxy) { n.Edges.PrimaryProxies = []*Proxy{} },
func(n *Proxy, e *Proxy) { n.Edges.PrimaryProxies = append(n.Edges.PrimaryProxies, e) }); err != nil {
return nil, err
}
}
if query := _q.withBackupProxy; query != nil {
if err := _q.loadBackupProxy(ctx, query, nodes, nil,
func(n *Proxy, e *Proxy) { n.Edges.BackupProxy = e }); err != nil {
@@ -483,6 +526,39 @@ func (_q *ProxyQuery) loadAccounts(ctx context.Context, query *AccountQuery, nod
}
return nil
}
func (_q *ProxyQuery) loadPrimaryProxies(ctx context.Context, query *ProxyQuery, nodes []*Proxy, init func(*Proxy), assign func(*Proxy, *Proxy)) error {
fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[int64]*Proxy)
for i := range nodes {
fks = append(fks, nodes[i].ID)
nodeids[nodes[i].ID] = nodes[i]
if init != nil {
init(nodes[i])
}
}
if len(query.ctx.Fields) > 0 {
query.ctx.AppendFieldOnce(proxy.FieldBackupProxyID)
}
query.Where(predicate.Proxy(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(proxy.PrimaryProxiesColumn), fks...))
}))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
fk := n.BackupProxyID
if fk == nil {
return fmt.Errorf(`foreign-key "backup_proxy_id" is nil for node %v`, n.ID)
}
node, ok := nodeids[*fk]
if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "backup_proxy_id" returned %v for node %v`, *fk, n.ID)
}
assign(node, n)
}
return nil
}
func (_q *ProxyQuery) loadBackupProxy(ctx context.Context, query *ProxyQuery, nodes []*Proxy, init func(*Proxy), assign func(*Proxy, *Proxy)) error {
ids := make([]int64, 0, len(nodes))
nodeids := make(map[int64][]*Proxy)
+170 -8
View File
@@ -262,6 +262,21 @@ func (_u *ProxyUpdate) AddAccounts(v ...*Account) *ProxyUpdate {
return _u.AddAccountIDs(ids...)
}
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
func (_u *ProxyUpdate) AddPrimaryProxyIDs(ids ...int64) *ProxyUpdate {
_u.mutation.AddPrimaryProxyIDs(ids...)
return _u
}
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
func (_u *ProxyUpdate) AddPrimaryProxies(v ...*Proxy) *ProxyUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddPrimaryProxyIDs(ids...)
}
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
func (_u *ProxyUpdate) SetBackupProxy(v *Proxy) *ProxyUpdate {
return _u.SetBackupProxyID(v.ID)
@@ -293,6 +308,27 @@ func (_u *ProxyUpdate) RemoveAccounts(v ...*Account) *ProxyUpdate {
return _u.RemoveAccountIDs(ids...)
}
// ClearPrimaryProxies clears all "primary_proxies" edges to the Proxy entity.
func (_u *ProxyUpdate) ClearPrimaryProxies() *ProxyUpdate {
_u.mutation.ClearPrimaryProxies()
return _u
}
// RemovePrimaryProxyIDs removes the "primary_proxies" edge to Proxy entities by IDs.
func (_u *ProxyUpdate) RemovePrimaryProxyIDs(ids ...int64) *ProxyUpdate {
_u.mutation.RemovePrimaryProxyIDs(ids...)
return _u
}
// RemovePrimaryProxies removes "primary_proxies" edges to Proxy entities.
func (_u *ProxyUpdate) RemovePrimaryProxies(v ...*Proxy) *ProxyUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemovePrimaryProxyIDs(ids...)
}
// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity.
func (_u *ProxyUpdate) ClearBackupProxy() *ProxyUpdate {
_u.mutation.ClearBackupProxy()
@@ -492,13 +528,58 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.PrimaryProxiesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedPrimaryProxiesIDs(); len(nodes) > 0 && !_u.mutation.PrimaryProxiesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.BackupProxyCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Rel: sqlgraph.M2O,
Inverse: false,
Table: proxy.BackupProxyTable,
Columns: []string{proxy.BackupProxyColumn},
Bidi: true,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
@@ -507,11 +588,11 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
}
if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Rel: sqlgraph.M2O,
Inverse: false,
Table: proxy.BackupProxyTable,
Columns: []string{proxy.BackupProxyColumn},
Bidi: true,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
@@ -774,6 +855,21 @@ func (_u *ProxyUpdateOne) AddAccounts(v ...*Account) *ProxyUpdateOne {
return _u.AddAccountIDs(ids...)
}
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
func (_u *ProxyUpdateOne) AddPrimaryProxyIDs(ids ...int64) *ProxyUpdateOne {
_u.mutation.AddPrimaryProxyIDs(ids...)
return _u
}
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
func (_u *ProxyUpdateOne) AddPrimaryProxies(v ...*Proxy) *ProxyUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddPrimaryProxyIDs(ids...)
}
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
func (_u *ProxyUpdateOne) SetBackupProxy(v *Proxy) *ProxyUpdateOne {
return _u.SetBackupProxyID(v.ID)
@@ -805,6 +901,27 @@ func (_u *ProxyUpdateOne) RemoveAccounts(v ...*Account) *ProxyUpdateOne {
return _u.RemoveAccountIDs(ids...)
}
// ClearPrimaryProxies clears all "primary_proxies" edges to the Proxy entity.
func (_u *ProxyUpdateOne) ClearPrimaryProxies() *ProxyUpdateOne {
_u.mutation.ClearPrimaryProxies()
return _u
}
// RemovePrimaryProxyIDs removes the "primary_proxies" edge to Proxy entities by IDs.
func (_u *ProxyUpdateOne) RemovePrimaryProxyIDs(ids ...int64) *ProxyUpdateOne {
_u.mutation.RemovePrimaryProxyIDs(ids...)
return _u
}
// RemovePrimaryProxies removes "primary_proxies" edges to Proxy entities.
func (_u *ProxyUpdateOne) RemovePrimaryProxies(v ...*Proxy) *ProxyUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemovePrimaryProxyIDs(ids...)
}
// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity.
func (_u *ProxyUpdateOne) ClearBackupProxy() *ProxyUpdateOne {
_u.mutation.ClearBackupProxy()
@@ -1034,13 +1151,58 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.PrimaryProxiesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedPrimaryProxiesIDs(); len(nodes) > 0 && !_u.mutation.PrimaryProxiesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
Table: proxy.PrimaryProxiesTable,
Columns: []string{proxy.PrimaryProxiesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.BackupProxyCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Rel: sqlgraph.M2O,
Inverse: false,
Table: proxy.BackupProxyTable,
Columns: []string{proxy.BackupProxyColumn},
Bidi: true,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
@@ -1049,11 +1211,11 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error)
}
if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Rel: sqlgraph.M2O,
Inverse: false,
Table: proxy.BackupProxyTable,
Columns: []string{proxy.BackupProxyColumn},
Bidi: true,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
},
+60 -46
View File
@@ -635,36 +635,28 @@ func init() {
return nil
}
}()
// channelmonitorDescCheckMode is the schema descriptor for check_mode field.
channelmonitorDescCheckMode := channelmonitorFields[2].Descriptor()
// channelmonitor.DefaultCheckMode holds the default value on creation for the check_mode field.
channelmonitor.DefaultCheckMode = channelmonitorDescCheckMode.Default.(string)
// channelmonitor.CheckModeValidator is a validator for the "check_mode" field. It is called by the builders before save.
channelmonitor.CheckModeValidator = channelmonitorDescCheckMode.Validators[0].(func(string) error)
// channelmonitorDescAPIMode is the schema descriptor for api_mode field.
channelmonitorDescAPIMode := channelmonitorFields[2].Descriptor()
channelmonitorDescAPIMode := channelmonitorFields[4].Descriptor()
// channelmonitor.DefaultAPIMode holds the default value on creation for the api_mode field.
channelmonitor.DefaultAPIMode = channelmonitorDescAPIMode.Default.(string)
// channelmonitor.APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save.
channelmonitor.APIModeValidator = channelmonitorDescAPIMode.Validators[0].(func(string) error)
// channelmonitorDescEndpoint is the schema descriptor for endpoint field.
channelmonitorDescEndpoint := channelmonitorFields[3].Descriptor()
channelmonitorDescEndpoint := channelmonitorFields[5].Descriptor()
// channelmonitor.EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save.
channelmonitor.EndpointValidator = func() func(string) error {
validators := channelmonitorDescEndpoint.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(endpoint string) error {
for _, fn := range fns {
if err := fn(endpoint); err != nil {
return err
}
}
return nil
}
}()
channelmonitor.EndpointValidator = channelmonitorDescEndpoint.Validators[0].(func(string) error)
// channelmonitorDescAPIKeyEncrypted is the schema descriptor for api_key_encrypted field.
channelmonitorDescAPIKeyEncrypted := channelmonitorFields[4].Descriptor()
channelmonitorDescAPIKeyEncrypted := channelmonitorFields[6].Descriptor()
// channelmonitor.APIKeyEncryptedValidator is a validator for the "api_key_encrypted" field. It is called by the builders before save.
channelmonitor.APIKeyEncryptedValidator = channelmonitorDescAPIKeyEncrypted.Validators[0].(func(string) error)
// channelmonitorDescPrimaryModel is the schema descriptor for primary_model field.
channelmonitorDescPrimaryModel := channelmonitorFields[5].Descriptor()
channelmonitorDescPrimaryModel := channelmonitorFields[7].Descriptor()
// channelmonitor.PrimaryModelValidator is a validator for the "primary_model" field. It is called by the builders before save.
channelmonitor.PrimaryModelValidator = func() func(string) error {
validators := channelmonitorDescPrimaryModel.Validators
@@ -682,35 +674,35 @@ func init() {
}
}()
// channelmonitorDescExtraModels is the schema descriptor for extra_models field.
channelmonitorDescExtraModels := channelmonitorFields[6].Descriptor()
channelmonitorDescExtraModels := channelmonitorFields[8].Descriptor()
// channelmonitor.DefaultExtraModels holds the default value on creation for the extra_models field.
channelmonitor.DefaultExtraModels = channelmonitorDescExtraModels.Default.([]string)
// channelmonitorDescGroupName is the schema descriptor for group_name field.
channelmonitorDescGroupName := channelmonitorFields[7].Descriptor()
channelmonitorDescGroupName := channelmonitorFields[9].Descriptor()
// channelmonitor.DefaultGroupName holds the default value on creation for the group_name field.
channelmonitor.DefaultGroupName = channelmonitorDescGroupName.Default.(string)
// channelmonitor.GroupNameValidator is a validator for the "group_name" field. It is called by the builders before save.
channelmonitor.GroupNameValidator = channelmonitorDescGroupName.Validators[0].(func(string) error)
// channelmonitorDescEnabled is the schema descriptor for enabled field.
channelmonitorDescEnabled := channelmonitorFields[8].Descriptor()
channelmonitorDescEnabled := channelmonitorFields[10].Descriptor()
// channelmonitor.DefaultEnabled holds the default value on creation for the enabled field.
channelmonitor.DefaultEnabled = channelmonitorDescEnabled.Default.(bool)
// channelmonitorDescIntervalSeconds is the schema descriptor for interval_seconds field.
channelmonitorDescIntervalSeconds := channelmonitorFields[9].Descriptor()
channelmonitorDescIntervalSeconds := channelmonitorFields[11].Descriptor()
// channelmonitor.IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save.
channelmonitor.IntervalSecondsValidator = channelmonitorDescIntervalSeconds.Validators[0].(func(int) error)
// channelmonitorDescJitterSeconds is the schema descriptor for jitter_seconds field.
channelmonitorDescJitterSeconds := channelmonitorFields[10].Descriptor()
channelmonitorDescJitterSeconds := channelmonitorFields[12].Descriptor()
// channelmonitor.DefaultJitterSeconds holds the default value on creation for the jitter_seconds field.
channelmonitor.DefaultJitterSeconds = channelmonitorDescJitterSeconds.Default.(int)
// channelmonitor.JitterSecondsValidator is a validator for the "jitter_seconds" field. It is called by the builders before save.
channelmonitor.JitterSecondsValidator = channelmonitorDescJitterSeconds.Validators[0].(func(int) error)
// channelmonitorDescExtraHeaders is the schema descriptor for extra_headers field.
channelmonitorDescExtraHeaders := channelmonitorFields[14].Descriptor()
channelmonitorDescExtraHeaders := channelmonitorFields[16].Descriptor()
// channelmonitor.DefaultExtraHeaders holds the default value on creation for the extra_headers field.
channelmonitor.DefaultExtraHeaders = channelmonitorDescExtraHeaders.Default.(map[string]string)
// channelmonitorDescBodyOverrideMode is the schema descriptor for body_override_mode field.
channelmonitorDescBodyOverrideMode := channelmonitorFields[15].Descriptor()
channelmonitorDescBodyOverrideMode := channelmonitorFields[17].Descriptor()
// channelmonitor.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field.
channelmonitor.DefaultBodyOverrideMode = channelmonitorDescBodyOverrideMode.Default.(string)
// channelmonitor.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
@@ -808,7 +800,7 @@ func init() {
// channelmonitorhistory.MessageValidator is a validator for the "message" field. It is called by the builders before save.
channelmonitorhistory.MessageValidator = channelmonitorhistoryDescMessage.Validators[0].(func(string) error)
// channelmonitorhistoryDescCheckedAt is the schema descriptor for checked_at field.
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[6].Descriptor()
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[7].Descriptor()
// channelmonitorhistory.DefaultCheckedAt holds the default value on creation for the checked_at field.
channelmonitorhistory.DefaultCheckedAt = channelmonitorhistoryDescCheckedAt.Default.(func() time.Time)
channelmonitorrequesttemplateMixin := schema.ChannelMonitorRequestTemplate{}.Mixin()
@@ -1165,52 +1157,70 @@ func init() {
groupDescAllowLive := groupFields[47].Descriptor()
// group.DefaultAllowLive holds the default value on creation for the allow_live field.
group.DefaultAllowLive = groupDescAllowLive.Default.(bool)
// groupDescForceOpenaiFast is the schema descriptor for force_openai_fast field.
groupDescForceOpenaiFast := groupFields[48].Descriptor()
// group.DefaultForceOpenaiFast holds the default value on creation for the force_openai_fast field.
group.DefaultForceOpenaiFast = groupDescForceOpenaiFast.Default.(bool)
// groupDescFreeOpenaiFast is the schema descriptor for free_openai_fast field.
groupDescFreeOpenaiFast := groupFields[49].Descriptor()
// group.DefaultFreeOpenaiFast holds the default value on creation for the free_openai_fast field.
group.DefaultFreeOpenaiFast = groupDescFreeOpenaiFast.Default.(bool)
// groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field.
groupDescRequireOauthOnly := groupFields[48].Descriptor()
groupDescRequireOauthOnly := groupFields[50].Descriptor()
// group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field.
group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool)
// groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field.
groupDescRequirePrivacySet := groupFields[49].Descriptor()
groupDescRequirePrivacySet := groupFields[51].Descriptor()
// group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field.
group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool)
// groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field.
groupDescDefaultMappedModel := groupFields[50].Descriptor()
groupDescDefaultMappedModel := groupFields[52].Descriptor()
// group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field.
group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string)
// group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save.
group.DefaultMappedModelValidator = groupDescDefaultMappedModel.Validators[0].(func(string) error)
// groupDescMessagesDispatchModelConfig is the schema descriptor for messages_dispatch_model_config field.
groupDescMessagesDispatchModelConfig := groupFields[51].Descriptor()
groupDescMessagesDispatchModelConfig := groupFields[53].Descriptor()
// group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field.
group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig)
// groupDescModelsListConfig is the schema descriptor for models_list_config field.
groupDescModelsListConfig := groupFields[52].Descriptor()
// group.DefaultModelsListConfig holds the default value on creation for the models_list_config field.
group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig)
// groupDescModelAllowlist is the schema descriptor for model_allowlist field.
groupDescModelAllowlist := groupFields[54].Descriptor()
// group.DefaultModelAllowlist holds the default value on creation for the model_allowlist field.
group.DefaultModelAllowlist = groupDescModelAllowlist.Default.(domain.GroupModelAllowlist)
// groupDescCodexModelsManifestConfig is the schema descriptor for codex_models_manifest_config field.
groupDescCodexModelsManifestConfig := groupFields[55].Descriptor()
// group.DefaultCodexModelsManifestConfig holds the default value on creation for the codex_models_manifest_config field.
group.DefaultCodexModelsManifestConfig = groupDescCodexModelsManifestConfig.Default.(domain.GroupCodexModelsManifestConfig)
// groupDescRpmLimit is the schema descriptor for rpm_limit field.
groupDescRpmLimit := groupFields[53].Descriptor()
groupDescRpmLimit := groupFields[56].Descriptor()
// group.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
group.DefaultRpmLimit = groupDescRpmLimit.Default.(int)
// groupDescMaxReasoningEffort is the schema descriptor for max_reasoning_effort field.
groupDescMaxReasoningEffort := groupFields[54].Descriptor()
groupDescMaxReasoningEffort := groupFields[57].Descriptor()
// group.DefaultMaxReasoningEffort holds the default value on creation for the max_reasoning_effort field.
group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string)
// group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
group.MaxReasoningEffortValidator = groupDescMaxReasoningEffort.Validators[0].(func(string) error)
// groupDescMaxReasoningEffortOverLimit is the schema descriptor for max_reasoning_effort_over_limit field.
groupDescMaxReasoningEffortOverLimit := groupFields[58].Descriptor()
// group.DefaultMaxReasoningEffortOverLimit holds the default value on creation for the max_reasoning_effort_over_limit field.
group.DefaultMaxReasoningEffortOverLimit = groupDescMaxReasoningEffortOverLimit.Default.(string)
// group.MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
group.MaxReasoningEffortOverLimitValidator = groupDescMaxReasoningEffortOverLimit.Validators[0].(func(string) error)
// groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field.
groupDescReasoningEffortMappings := groupFields[55].Descriptor()
groupDescReasoningEffortMappings := groupFields[59].Descriptor()
// group.DefaultReasoningEffortMappings holds the default value on creation for the reasoning_effort_mappings field.
group.DefaultReasoningEffortMappings = groupDescReasoningEffortMappings.Default.([]domain.ReasoningEffortMapping)
// groupDescProfitControlEnabled is the schema descriptor for profit_control_enabled field.
groupDescProfitControlEnabled := groupFields[56].Descriptor()
groupDescProfitControlEnabled := groupFields[60].Descriptor()
// group.DefaultProfitControlEnabled holds the default value on creation for the profit_control_enabled field.
group.DefaultProfitControlEnabled = groupDescProfitControlEnabled.Default.(bool)
// groupDescProfitMinMargin is the schema descriptor for profit_min_margin field.
groupDescProfitMinMargin := groupFields[57].Descriptor()
groupDescProfitMinMargin := groupFields[61].Descriptor()
// group.DefaultProfitMinMargin holds the default value on creation for the profit_min_margin field.
group.DefaultProfitMinMargin = groupDescProfitMinMargin.Default.(float64)
// groupDescProfitSafetyBuffer is the schema descriptor for profit_safety_buffer field.
groupDescProfitSafetyBuffer := groupFields[58].Descriptor()
groupDescProfitSafetyBuffer := groupFields[62].Descriptor()
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
@@ -2225,24 +2235,28 @@ func init() {
user.DefaultSignupSource = userDescSignupSource.Default.(string)
// user.SignupSourceValidator is a validator for the "signup_source" field. It is called by the builders before save.
user.SignupSourceValidator = userDescSignupSource.Validators[0].(func(string) error)
// userDescRestrictPublicGroups is the schema descriptor for restrict_public_groups field.
userDescRestrictPublicGroups := userFields[15].Descriptor()
// user.DefaultRestrictPublicGroups holds the default value on creation for the restrict_public_groups field.
user.DefaultRestrictPublicGroups = userDescRestrictPublicGroups.Default.(bool)
// userDescBalanceNotifyEnabled is the schema descriptor for balance_notify_enabled field.
userDescBalanceNotifyEnabled := userFields[15].Descriptor()
userDescBalanceNotifyEnabled := userFields[16].Descriptor()
// user.DefaultBalanceNotifyEnabled holds the default value on creation for the balance_notify_enabled field.
user.DefaultBalanceNotifyEnabled = userDescBalanceNotifyEnabled.Default.(bool)
// userDescBalanceNotifyThresholdType is the schema descriptor for balance_notify_threshold_type field.
userDescBalanceNotifyThresholdType := userFields[16].Descriptor()
userDescBalanceNotifyThresholdType := userFields[17].Descriptor()
// user.DefaultBalanceNotifyThresholdType holds the default value on creation for the balance_notify_threshold_type field.
user.DefaultBalanceNotifyThresholdType = userDescBalanceNotifyThresholdType.Default.(string)
// userDescBalanceNotifyExtraEmails is the schema descriptor for balance_notify_extra_emails field.
userDescBalanceNotifyExtraEmails := userFields[18].Descriptor()
userDescBalanceNotifyExtraEmails := userFields[19].Descriptor()
// user.DefaultBalanceNotifyExtraEmails holds the default value on creation for the balance_notify_extra_emails field.
user.DefaultBalanceNotifyExtraEmails = userDescBalanceNotifyExtraEmails.Default.(string)
// userDescTotalRecharged is the schema descriptor for total_recharged field.
userDescTotalRecharged := userFields[19].Descriptor()
userDescTotalRecharged := userFields[20].Descriptor()
// user.DefaultTotalRecharged holds the default value on creation for the total_recharged field.
user.DefaultTotalRecharged = userDescTotalRecharged.Default.(float64)
// userDescRpmLimit is the schema descriptor for rpm_limit field.
userDescRpmLimit := userFields[20].Descriptor()
userDescRpmLimit := userFields[21].Descriptor()
// user.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
user.DefaultRpmLimit = userDescRpmLimit.Default.(int)
userallowedgroupFields := schema.UserAllowedGroup{}.Fields()
+21 -3
View File
@@ -35,15 +35,32 @@ func (ChannelMonitor) Fields() []ent.Field {
NotEmpty().
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini", "grok"),
Values("openai", "anthropic", "gemini", "grok",
"antigravity", "kimi", "zhipu", "deepseek", "minimax"),
// check_mode: 'probe' | 'quota' | 'quota_probe'
// probe - LLM 探活(默认,原有行为)
// quota - 仅查关联账号的用量/余额(零 LLM 成本;endpoint/api_key 可空)
// quota_probe - 探活 + 配额并存(配额快照挂到主模型历史行)
// antigravity 无探活 adapter,仅允许 quota。
field.String("check_mode").
Default("probe").
MaxLen(32).
Comment("probe = LLM probe (default); quota = account usage only; quota_probe = both"),
// account_id: 配额模式的数据源账号(复用账号侧用量服务,不直接对接上游)。
// 普通字段而非 edge(FK 由 SQL 迁移管理);账号删除时数据库置空,
// 监控保留并报「账号未关联」。
field.Int64("account_id").
Optional().
Nillable(),
field.String("api_mode").
Default("chat_completions").
MaxLen(32).
Comment("OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions"),
// endpoint: 探活模式必填(service 层校验);quota 模式存空串
// (列保持 NOT NULL,去掉 NotEmpty 校验器即可)。
field.String("endpoint").
NotEmpty().
MaxLen(500).
Comment("Provider base origin, e.g. https://api.openai.com"),
Comment("Provider base origin, e.g. https://api.openai.com; empty for quota-only monitors"),
field.String("api_key_encrypted").
NotEmpty().
Sensitive().
@@ -115,5 +132,6 @@ func (ChannelMonitor) Indexes() []ent.Index {
index.Fields("provider", "api_mode"),
index.Fields("group_name"),
index.Fields("template_id"),
index.Fields("account_id"),
}
}
@@ -3,6 +3,8 @@ package schema
import (
"time"
"github.com/Wei-Shaw/sub2api/internal/domain"
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
@@ -43,6 +45,10 @@ func (ChannelMonitorHistory) Fields() []ent.Field {
Optional().
Default("").
MaxLen(500),
// quota: 配额模式(check_mode = quota / quota_probe)检测时附带的
// 归一化配额快照(domain.MonitorQuotaSnapshot,JSONB);探活模式为 NULL。
field.JSON("quota", &domain.MonitorQuotaSnapshot{}).
Optional(),
field.Time("checked_at").
Default(time.Now),
}
@@ -39,7 +39,8 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
NotEmpty().
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini", "grok"),
Values("openai", "anthropic", "gemini", "grok",
"antigravity", "kimi", "zhipu", "deepseek", "minimax"),
field.String("api_mode").
Default("chat_completions").
MaxLen(32).
+18 -4
View File
@@ -242,6 +242,12 @@ func (Group) Fields() []ent.Field {
field.Bool("allow_live").
Default(false).
Comment("是否允许此 OpenAI 分组访问 Live 接口"),
field.Bool("force_openai_fast").
Default(false).
Comment("是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority"),
field.Bool("free_openai_fast").
Default(false).
Comment("是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费"),
field.Bool("require_oauth_only").
Default(false).
Comment("仅允许非 apikey 类型账号关联到此分组"),
@@ -256,10 +262,14 @@ func (Group) Fields() []ent.Field {
Default(domain.OpenAIMessagesDispatchModelConfig{}).
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
Comment("OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型"),
field.JSON("models_list_config", domain.GroupModelsListConfig{}).
Default(domain.GroupModelsListConfig{}).
field.JSON("model_allowlist", domain.GroupModelAllowlist{}).
Default(domain.GroupModelAllowlist{}).
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
Comment("自定义 /v1/models 展示列表配置;仅影响模型列表响应,不影响调度"),
Comment("分组模型白名单:同时约束模型列表接口与请求准入"),
field.JSON("codex_models_manifest_config", domain.GroupCodexModelsManifestConfig{}).
Default(domain.GroupCodexModelsManifestConfig{}).
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
Comment("固定账号获取 Codex Model Manifest 配置;开启后 /models 请求只用选定账号拉取(仅 openai 平台)"),
// 分组级每分钟请求数上限(0 = 不限制)。设置后优先于用户级兜底生效。
field.Int("rpm_limit").
@@ -271,10 +281,14 @@ func (Group) Fields() []ent.Field {
MaxLen(20).
Default("").
Comment("OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max"),
field.String("max_reasoning_effort_over_limit").
MaxLen(20).
Default("downgrade").
Comment("超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问"),
field.JSON("reasoning_effort_mappings", []domain.ReasoningEffortMapping{}).
Default([]domain.ReasoningEffortMapping{}).
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
Comment("OpenAI reasoning effort 自定义精确映射;先映射再应用上限"),
Comment("OpenAI reasoning effort 自定义映射;可按模型精确名、前缀或后缀限定,先映射再应用上限"),
// 分组利润控制(migration 192/193):openai/anthropic/gemini/grok/antigravity
// 的 token 分组可启用,composite 分组不能直接启用。
+4
View File
@@ -73,6 +73,10 @@ func (Proxy) Edges() []ent.Edge {
// accounts: 使用此代理的账户(反向边)
edge.From("accounts", Account.Type).
Ref("proxy"),
// Directed many-to-one: a backup can serve multiple primary proxies.
// The inverse edge prevents Ent from treating this self-reference as symmetric.
edge.From("primary_proxies", Proxy.Type).
Ref("backup_proxy"),
edge.To("backup_proxy", Proxy.Type).
Field("backup_proxy_id").
Unique(),
+5
View File
@@ -96,6 +96,11 @@ func (User) Fields() []ent.Field {
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
// 公开分组访问限制:为 false 时用户可绑定任意非专属分组(默认行为),
// 为 true 时仅可绑定 user_allowed_groups 中列出的公开分组。
field.Bool("restrict_public_groups").
Default(false),
// 余额不足通知
field.Bool("balance_notify_enabled").
Default(true),
+2 -1
View File
@@ -41,7 +41,8 @@ func (UserPlatformQuota) Fields() []ent.Field {
// 注意:平台列表的单一权威源为 service.AllowedQuotaPlatforms;
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
switch s {
case "anthropic", "openai", "gemini", "antigravity", "grok":
case "anthropic", "openai", "gemini", "antigravity", "grok",
"kimi", "zhipu", "deepseek", "minimax":
return nil
default:
return fmt.Errorf("platform %q is not allowed", s)
+2 -1
View File
@@ -4,6 +4,7 @@ package ent
import (
"encoding/json"
"encoding/json/jsontext"
"fmt"
"strings"
"time"
@@ -25,7 +26,7 @@ type UsageCleanupTask struct {
// Status holds the value of the "status" field.
Status string `json:"status,omitempty"`
// Filters holds the value of the "filters" field.
Filters json.RawMessage `json:"filters,omitempty"`
Filters jsontext.Value `json:"filters,omitempty"`
// CreatedBy holds the value of the "created_by" field.
CreatedBy int64 `json:"created_by,omitempty"`
// DeletedRows holds the value of the "deleted_rows" field.
+5 -5
View File
@@ -4,7 +4,7 @@ package ent
import (
"context"
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"time"
@@ -58,7 +58,7 @@ func (_c *UsageCleanupTaskCreate) SetStatus(v string) *UsageCleanupTaskCreate {
}
// SetFilters sets the "filters" field.
func (_c *UsageCleanupTaskCreate) SetFilters(v json.RawMessage) *UsageCleanupTaskCreate {
func (_c *UsageCleanupTaskCreate) SetFilters(v jsontext.Value) *UsageCleanupTaskCreate {
_c.mutation.SetFilters(v)
return _c
}
@@ -375,7 +375,7 @@ func (u *UsageCleanupTaskUpsert) UpdateStatus() *UsageCleanupTaskUpsert {
}
// SetFilters sets the "filters" field.
func (u *UsageCleanupTaskUpsert) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsert {
func (u *UsageCleanupTaskUpsert) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsert {
u.Set(usagecleanuptask.FieldFilters, v)
return u
}
@@ -592,7 +592,7 @@ func (u *UsageCleanupTaskUpsertOne) UpdateStatus() *UsageCleanupTaskUpsertOne {
}
// SetFilters sets the "filters" field.
func (u *UsageCleanupTaskUpsertOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertOne {
func (u *UsageCleanupTaskUpsertOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertOne {
return u.Update(func(s *UsageCleanupTaskUpsert) {
s.SetFilters(v)
})
@@ -999,7 +999,7 @@ func (u *UsageCleanupTaskUpsertBulk) UpdateStatus() *UsageCleanupTaskUpsertBulk
}
// SetFilters sets the "filters" field.
func (u *UsageCleanupTaskUpsertBulk) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertBulk {
func (u *UsageCleanupTaskUpsertBulk) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertBulk {
return u.Update(func(s *UsageCleanupTaskUpsert) {
s.SetFilters(v)
})
+5 -5
View File
@@ -4,7 +4,7 @@ package ent
import (
"context"
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"time"
@@ -51,13 +51,13 @@ func (_u *UsageCleanupTaskUpdate) SetNillableStatus(v *string) *UsageCleanupTask
}
// SetFilters sets the "filters" field.
func (_u *UsageCleanupTaskUpdate) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdate {
func (_u *UsageCleanupTaskUpdate) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdate {
_u.mutation.SetFilters(v)
return _u
}
// AppendFilters appends value to the "filters" field.
func (_u *UsageCleanupTaskUpdate) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdate {
func (_u *UsageCleanupTaskUpdate) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdate {
_u.mutation.AppendFilters(v)
return _u
}
@@ -374,13 +374,13 @@ func (_u *UsageCleanupTaskUpdateOne) SetNillableStatus(v *string) *UsageCleanupT
}
// SetFilters sets the "filters" field.
func (_u *UsageCleanupTaskUpdateOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne {
func (_u *UsageCleanupTaskUpdateOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne {
_u.mutation.SetFilters(v)
return _u
}
// AppendFilters appends value to the "filters" field.
func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne {
func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne {
_u.mutation.AppendFilters(v)
return _u
}
+12 -1
View File
@@ -53,6 +53,8 @@ type User struct {
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
// LastActiveAt holds the value of the "last_active_at" field.
LastActiveAt *time.Time `json:"last_active_at,omitempty"`
// RestrictPublicGroups holds the value of the "restrict_public_groups" field.
RestrictPublicGroups bool `json:"restrict_public_groups,omitempty"`
// BalanceNotifyEnabled holds the value of the "balance_notify_enabled" field.
BalanceNotifyEnabled bool `json:"balance_notify_enabled,omitempty"`
// BalanceNotifyThresholdType holds the value of the "balance_notify_threshold_type" field.
@@ -237,7 +239,7 @@ func (*User) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case user.FieldTotpEnabled, user.FieldBalanceNotifyEnabled:
case user.FieldTotpEnabled, user.FieldRestrictPublicGroups, user.FieldBalanceNotifyEnabled:
values[i] = new(sql.NullBool)
case user.FieldBalance, user.FieldFrozenBalance, user.FieldBalanceNotifyThreshold, user.FieldTotalRecharged:
values[i] = new(sql.NullFloat64)
@@ -381,6 +383,12 @@ func (_m *User) assignValues(columns []string, values []any) error {
_m.LastActiveAt = new(time.Time)
*_m.LastActiveAt = value.Time
}
case user.FieldRestrictPublicGroups:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field restrict_public_groups", values[i])
} else if value.Valid {
_m.RestrictPublicGroups = value.Bool
}
case user.FieldBalanceNotifyEnabled:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field balance_notify_enabled", values[i])
@@ -588,6 +596,9 @@ func (_m *User) String() string {
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
builder.WriteString("restrict_public_groups=")
builder.WriteString(fmt.Sprintf("%v", _m.RestrictPublicGroups))
builder.WriteString(", ")
builder.WriteString("balance_notify_enabled=")
builder.WriteString(fmt.Sprintf("%v", _m.BalanceNotifyEnabled))
builder.WriteString(", ")
+10
View File
@@ -51,6 +51,8 @@ const (
FieldLastLoginAt = "last_login_at"
// FieldLastActiveAt holds the string denoting the last_active_at field in the database.
FieldLastActiveAt = "last_active_at"
// FieldRestrictPublicGroups holds the string denoting the restrict_public_groups field in the database.
FieldRestrictPublicGroups = "restrict_public_groups"
// FieldBalanceNotifyEnabled holds the string denoting the balance_notify_enabled field in the database.
FieldBalanceNotifyEnabled = "balance_notify_enabled"
// FieldBalanceNotifyThresholdType holds the string denoting the balance_notify_threshold_type field in the database.
@@ -212,6 +214,7 @@ var Columns = []string{
FieldSignupSource,
FieldLastLoginAt,
FieldLastActiveAt,
FieldRestrictPublicGroups,
FieldBalanceNotifyEnabled,
FieldBalanceNotifyThresholdType,
FieldBalanceNotifyThreshold,
@@ -280,6 +283,8 @@ var (
DefaultSignupSource string
// SignupSourceValidator is a validator for the "signup_source" field. It is called by the builders before save.
SignupSourceValidator func(string) error
// DefaultRestrictPublicGroups holds the default value on creation for the "restrict_public_groups" field.
DefaultRestrictPublicGroups bool
// DefaultBalanceNotifyEnabled holds the default value on creation for the "balance_notify_enabled" field.
DefaultBalanceNotifyEnabled bool
// DefaultBalanceNotifyThresholdType holds the default value on creation for the "balance_notify_threshold_type" field.
@@ -390,6 +395,11 @@ func ByLastActiveAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldLastActiveAt, opts...).ToFunc()
}
// ByRestrictPublicGroups orders the results by the restrict_public_groups field.
func ByRestrictPublicGroups(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRestrictPublicGroups, opts...).ToFunc()
}
// ByBalanceNotifyEnabled orders the results by the balance_notify_enabled field.
func ByBalanceNotifyEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBalanceNotifyEnabled, opts...).ToFunc()
+15
View File
@@ -145,6 +145,11 @@ func LastActiveAt(v time.Time) predicate.User {
return predicate.User(sql.FieldEQ(FieldLastActiveAt, v))
}
// RestrictPublicGroups applies equality check predicate on the "restrict_public_groups" field. It's identical to RestrictPublicGroupsEQ.
func RestrictPublicGroups(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldRestrictPublicGroups, v))
}
// BalanceNotifyEnabled applies equality check predicate on the "balance_notify_enabled" field. It's identical to BalanceNotifyEnabledEQ.
func BalanceNotifyEnabled(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
@@ -1115,6 +1120,16 @@ func LastActiveAtNotNil() predicate.User {
return predicate.User(sql.FieldNotNull(FieldLastActiveAt))
}
// RestrictPublicGroupsEQ applies the EQ predicate on the "restrict_public_groups" field.
func RestrictPublicGroupsEQ(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldRestrictPublicGroups, v))
}
// RestrictPublicGroupsNEQ applies the NEQ predicate on the "restrict_public_groups" field.
func RestrictPublicGroupsNEQ(v bool) predicate.User {
return predicate.User(sql.FieldNEQ(FieldRestrictPublicGroups, v))
}
// BalanceNotifyEnabledEQ applies the EQ predicate on the "balance_notify_enabled" field.
func BalanceNotifyEnabledEQ(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
+65
View File
@@ -270,6 +270,20 @@ func (_c *UserCreate) SetNillableLastActiveAt(v *time.Time) *UserCreate {
return _c
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (_c *UserCreate) SetRestrictPublicGroups(v bool) *UserCreate {
_c.mutation.SetRestrictPublicGroups(v)
return _c
}
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
func (_c *UserCreate) SetNillableRestrictPublicGroups(v *bool) *UserCreate {
if v != nil {
_c.SetRestrictPublicGroups(*v)
}
return _c
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_c *UserCreate) SetBalanceNotifyEnabled(v bool) *UserCreate {
_c.mutation.SetBalanceNotifyEnabled(v)
@@ -636,6 +650,10 @@ func (_c *UserCreate) defaults() error {
v := user.DefaultSignupSource
_c.mutation.SetSignupSource(v)
}
if _, ok := _c.mutation.RestrictPublicGroups(); !ok {
v := user.DefaultRestrictPublicGroups
_c.mutation.SetRestrictPublicGroups(v)
}
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
v := user.DefaultBalanceNotifyEnabled
_c.mutation.SetBalanceNotifyEnabled(v)
@@ -730,6 +748,9 @@ func (_c *UserCreate) check() error {
return &ValidationError{Name: "signup_source", err: fmt.Errorf(`ent: validator failed for field "User.signup_source": %w`, err)}
}
}
if _, ok := _c.mutation.RestrictPublicGroups(); !ok {
return &ValidationError{Name: "restrict_public_groups", err: errors.New(`ent: missing required field "User.restrict_public_groups"`)}
}
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
return &ValidationError{Name: "balance_notify_enabled", err: errors.New(`ent: missing required field "User.balance_notify_enabled"`)}
}
@@ -844,6 +865,10 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
_spec.SetField(user.FieldLastActiveAt, field.TypeTime, value)
_node.LastActiveAt = &value
}
if value, ok := _c.mutation.RestrictPublicGroups(); ok {
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
_node.RestrictPublicGroups = value
}
if value, ok := _c.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
_node.BalanceNotifyEnabled = value
@@ -1384,6 +1409,18 @@ func (u *UserUpsert) ClearLastActiveAt() *UserUpsert {
return u
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (u *UserUpsert) SetRestrictPublicGroups(v bool) *UserUpsert {
u.Set(user.FieldRestrictPublicGroups, v)
return u
}
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
func (u *UserUpsert) UpdateRestrictPublicGroups() *UserUpsert {
u.SetExcluded(user.FieldRestrictPublicGroups)
return u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsert) SetBalanceNotifyEnabled(v bool) *UserUpsert {
u.Set(user.FieldBalanceNotifyEnabled, v)
@@ -1819,6 +1856,20 @@ func (u *UserUpsertOne) ClearLastActiveAt() *UserUpsertOne {
})
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (u *UserUpsertOne) SetRestrictPublicGroups(v bool) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.SetRestrictPublicGroups(v)
})
}
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
func (u *UserUpsertOne) UpdateRestrictPublicGroups() *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.UpdateRestrictPublicGroups()
})
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsertOne) SetBalanceNotifyEnabled(v bool) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
@@ -2436,6 +2487,20 @@ func (u *UserUpsertBulk) ClearLastActiveAt() *UserUpsertBulk {
})
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (u *UserUpsertBulk) SetRestrictPublicGroups(v bool) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.SetRestrictPublicGroups(v)
})
}
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
func (u *UserUpsertBulk) UpdateRestrictPublicGroups() *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.UpdateRestrictPublicGroups()
})
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsertBulk) SetBalanceNotifyEnabled(v bool) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
+34
View File
@@ -321,6 +321,20 @@ func (_u *UserUpdate) ClearLastActiveAt() *UserUpdate {
return _u
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (_u *UserUpdate) SetRestrictPublicGroups(v bool) *UserUpdate {
_u.mutation.SetRestrictPublicGroups(v)
return _u
}
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
func (_u *UserUpdate) SetNillableRestrictPublicGroups(v *bool) *UserUpdate {
if v != nil {
_u.SetRestrictPublicGroups(*v)
}
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdate) SetBalanceNotifyEnabled(v bool) *UserUpdate {
_u.mutation.SetBalanceNotifyEnabled(v)
@@ -1069,6 +1083,9 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.LastActiveAtCleared() {
_spec.ClearField(user.FieldLastActiveAt, field.TypeTime)
}
if value, ok := _u.mutation.RestrictPublicGroups(); ok {
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
@@ -1997,6 +2014,20 @@ func (_u *UserUpdateOne) ClearLastActiveAt() *UserUpdateOne {
return _u
}
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
func (_u *UserUpdateOne) SetRestrictPublicGroups(v bool) *UserUpdateOne {
_u.mutation.SetRestrictPublicGroups(v)
return _u
}
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableRestrictPublicGroups(v *bool) *UserUpdateOne {
if v != nil {
_u.SetRestrictPublicGroups(*v)
}
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdateOne) SetBalanceNotifyEnabled(v bool) *UserUpdateOne {
_u.mutation.SetBalanceNotifyEnabled(v)
@@ -2775,6 +2806,9 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
if _u.mutation.LastActiveAtCleared() {
_spec.ClearField(user.FieldLastActiveAt, field.TypeTime)
}
if value, ok := _u.mutation.RestrictPublicGroups(); ok {
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
+15 -11
View File
@@ -1,6 +1,6 @@
module github.com/Wei-Shaw/sub2api
go 1.26.5
go 1.27.0
require (
entgo.io/ent v0.14.5
@@ -24,12 +24,14 @@ require (
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.8.0
github.com/imroc/req/v3 v3.59.0
github.com/klauspost/compress v1.18.2
github.com/lib/pq v1.10.9
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.17.2
github.com/redis/go-redis/v9 v9.22.0
github.com/refraction-networking/utls v1.8.2
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.6
@@ -54,6 +56,8 @@ require (
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/term v0.44.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.44.3
@@ -97,7 +101,6 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2/v2 v2.1.0 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
@@ -121,15 +124,17 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.6 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
@@ -149,6 +154,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
@@ -187,11 +193,10 @@ require (
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
@@ -200,8 +205,7 @@ require (
golang.org/x/text v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
+51 -20
View File
@@ -120,6 +120,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -157,8 +159,6 @@ github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dr
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2/v2 v2.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY=
@@ -176,6 +176,7 @@ github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -232,6 +233,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -262,6 +265,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs=
github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
@@ -269,6 +276,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.59.0 h1:PqKhJHyBmJYob47LVuTHwRZE00ZO6icbLHe5Zra13jo=
@@ -281,6 +290,8 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -291,6 +302,8 @@ github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxh
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -307,8 +320,12 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -350,6 +367,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -380,8 +399,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -439,6 +458,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -507,22 +527,26 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -608,6 +632,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -615,6 +641,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -678,29 +707,31 @@ golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+136 -6
View File
@@ -32,7 +32,7 @@ const (
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src 'self' https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
// UMQ(用户消息队列)模式常量
const (
@@ -61,6 +61,10 @@ const (
// 可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
// DefaultModelsListReadMaxBytes 上游模型列表响应体的默认读取上限。
// 可通过 gateway.models_list_read_max_bytes 配置项覆盖。
const DefaultModelsListReadMaxBytes int64 = 8 * 1024 * 1024
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig `mapstructure:"log"`
@@ -99,6 +103,18 @@ type Config struct {
Idempotency IdempotencyConfig `mapstructure:"idempotency"`
BatchImage BatchImageConfig `mapstructure:"batch_image"`
ImageStorage ImageStorageConfig `mapstructure:"image_storage"`
Plugins PluginConfig `mapstructure:"plugins"`
}
// PluginConfig 控制管理员手动上传的本地进程插件。
// 默认不包含插件,也不允许安装未签名插件;TrustedPublishers 用于追加第三方发布者。
type PluginConfig struct {
DataDir string `mapstructure:"data_dir"`
AllowUnsigned bool `mapstructure:"allow_unsigned"`
TrustedPublishers map[string]string `mapstructure:"trusted_publishers"`
MaxUploadBytes int64 `mapstructure:"max_upload_bytes"`
MaxUncompressedBytes int64 `mapstructure:"max_uncompressed_bytes"`
StartTimeoutSeconds int `mapstructure:"start_timeout_seconds"`
}
type LogConfig struct {
@@ -651,6 +667,8 @@ type PricingConfig struct {
DataDir string `mapstructure:"data_dir"`
// 回退文件路径
FallbackFile string `mapstructure:"fallback_file"`
// 覆盖补丁文件路径(可选):条目按字段浅合并覆盖目录/回退数据,优先级最高
OverrideFile string `mapstructure:"override_file"`
// 更新间隔(小时)
UpdateIntervalHours int `mapstructure:"update_interval_hours"`
// 哈希校验间隔(分钟)
@@ -830,6 +848,53 @@ type ProxyFallbackConfig struct {
type ProxyProbeConfig struct {
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
// URLs 按优先级排列的自定义探测 URL 列表。
// 留空时使用内置默认列表(ip-api → ipify)。
// 某些 AI API 专用代理只允许访问特定域名,配置多个备选可提高探测成功率。
URLs []ProbeURLConfig `mapstructure:"urls"`
}
// ProbeURLConfig 描述一个探测端点及其响应解析方式。
type ProbeURLConfig struct {
URL string `mapstructure:"url"`
Parser string `mapstructure:"parser"` // "ip-api" / "ipify" / "chatgpt-trace"
}
func normalizeProxyProbeURLs(targets []ProbeURLConfig) ([]ProbeURLConfig, error) {
if len(targets) == 0 {
return nil, nil
}
normalized := make([]ProbeURLConfig, 0, len(targets))
for i, target := range targets {
rawURL := strings.TrimSpace(target.URL)
parser := strings.ToLower(strings.TrimSpace(target.Parser))
if rawURL == "" {
return nil, fmt.Errorf("entry %d: url is required", i)
}
if parser == "" {
return nil, fmt.Errorf("entry %d: parser is required", i)
}
switch parser {
case "ip-api", "ipify", "chatgpt-trace":
default:
return nil, fmt.Errorf("entry %d: unsupported parser %q", i, target.Parser)
}
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Host == "" {
return nil, fmt.Errorf("entry %d: invalid url %q", i, target.URL)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("entry %d: url scheme must be http or https", i)
}
normalized = append(normalized, ProbeURLConfig{
URL: rawURL,
Parser: parser,
})
}
return normalized, nil
}
type BillingConfig struct {
@@ -887,6 +952,9 @@ type GatewayConfig struct {
// OpenAIResponseHeaderTimeout: OpenAI/Codex 上游等待响应头的超时时间(秒),0表示无超时
// OpenAI/Codex 请求可能在上游排队较久;默认不使用通用响应头超时截断。
OpenAIResponseHeaderTimeout int `mapstructure:"openai_response_header_timeout"`
// GrokResponseHeaderTimeout bounds the pre-first-byte wait for xAI/Grok.
// A zero value uses the provider-safe default instead of the generic gateway timeout.
GrokResponseHeaderTimeout int `mapstructure:"grok_response_header_timeout"`
// OpenAIFirstOutputTimeoutSeconds: native HTTP Responses 首个语义输出超时(秒),0表示禁用。
OpenAIFirstOutputTimeoutSeconds int `mapstructure:"openai_first_output_timeout_seconds"`
// OpenAIHighEffortFirstOutputTimeoutSeconds: high/xhigh/max 推理的首个语义输出超时(秒)。
@@ -898,6 +966,8 @@ type GatewayConfig struct {
TextMaxBodySize int64 `mapstructure:"text_max_body_size"`
// 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
// 上游模型列表响应体读取上限(字节)
ModelsListReadMaxBytes int64 `mapstructure:"models_list_read_max_bytes"`
// 代理探测响应体读取上限(字节)
ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
// Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
@@ -1025,6 +1095,10 @@ type GatewayConfig struct {
// Grok: Grok/xAI gateway scheduling and free-tier soft-gate settings.
Grok GatewayGrokConfig `mapstructure:"grok"`
// CNProviders: 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
// 仅作用于 payg(按量付费)账号:周期探测余额,低于阈值则临时停调。
CNProviders GatewayCNProvidersConfig `mapstructure:"cn_providers"`
}
// GatewayGrokConfig holds Grok-specific gateway scheduling knobs.
@@ -1057,6 +1131,18 @@ type GatewayGrokConfig struct {
FreeQuotaStatsCacheSeconds int `mapstructure:"free_quota_stats_cache_seconds"`
}
// GatewayCNProvidersConfig 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
//
// 仅作用于 payg(按量付费)账号(kimi/deepseek 有公开余额端点;zhipu 无,仅靠响应式 429/402)。
// - balance_check_enabled: 是否启用周期余额检测(默认 true)
// - balance_threshold: 余额低于此值(账户货币单位,默认 0.5)触发临时停调
// - balance_check_interval_minutes: 余额检测周期(分钟,默认 10)
type GatewayCNProvidersConfig struct {
BalanceCheckEnabled bool `mapstructure:"balance_check_enabled"`
BalanceThreshold float64 `mapstructure:"balance_threshold"`
BalanceCheckIntervalMinutes int `mapstructure:"balance_check_interval_minutes"`
}
type GatewayLiveConfig struct {
// MaxSessionDurationSeconds 是 Live 会话的硬上限。
MaxSessionDurationSeconds int `mapstructure:"max_session_duration_seconds"`
@@ -1527,10 +1613,10 @@ type OpsCleanupConfig struct {
Enabled bool `mapstructure:"enabled"`
Schedule string `mapstructure:"schedule"`
// Retention days (0 disables that cleanup target).
//
// vNext requirement: default 30 days across ops datasets.
// Retention days. Error and metrics targets accept 0 as an explicit truncate;
// system logs require a positive value because their runtime setting is bounded.
ErrorLogRetentionDays int `mapstructure:"error_log_retention_days"`
SystemLogRetentionDays int `mapstructure:"system_log_retention_days"`
MinuteMetricsRetentionDays int `mapstructure:"minute_metrics_retention_days"`
HourlyMetricsRetentionDays int `mapstructure:"hourly_metrics_retention_days"`
}
@@ -1703,6 +1789,10 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
// 环境变量支持
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if tz, ok := os.LookupEnv("TZ"); ok && strings.TrimSpace(tz) != "" {
// AutomaticEnv 会先把 timezone 映射到 TIMEZONE;显式 Set 保证标准 TZ 变量优先。
viper.Set("timezone", strings.TrimSpace(tz))
}
if err := viper.BindEnv("server.enable_server_timing", "ENABLE_SERVER_TIMING"); err != nil {
return nil, fmt.Errorf("bind ENABLE_SERVER_TIMING: %w", err)
}
@@ -1956,7 +2046,8 @@ func setDefaults() {
"api.moonshot.ai",
"api.moonshot.cn",
"open.bigmodel.cn",
"api.minimaxi.com",
"api.minimaxi.com", // MiniMax CN quota + inference
"api.minimax.io", // MiniMax intl; frozen allowlists must add this host to use the intl site
"generativelanguage.googleapis.com",
"cloudcode-pa.googleapis.com",
"*.openai.azure.com",
@@ -2160,6 +2251,7 @@ func setDefaults() {
viper.SetDefault("ops.cleanup.schedule", "0 2 * * *")
// Retention days: vNext defaults to 30 days across ops datasets.
viper.SetDefault("ops.cleanup.error_log_retention_days", 30)
viper.SetDefault("ops.cleanup.system_log_retention_days", 30)
viper.SetDefault("ops.cleanup.minute_metrics_retention_days", 30)
viper.SetDefault("ops.cleanup.hourly_metrics_retention_days", 30)
viper.SetDefault("ops.aggregation.enabled", true)
@@ -2191,14 +2283,23 @@ func setDefaults() {
viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
viper.SetDefault("rate_limit.oauth_401_cooldown_minutes", 10)
// Pricing - 从 model-price-repo 同步模型定价和上下文窗口数据(固定到 commit,避免分支漂移)
// Pricing - 从 model-price-repo main 分支同步模型定价和上下文窗口数据
viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.json")
viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.sha256")
viper.SetDefault("pricing.data_dir", "./data")
viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
viper.SetDefault("pricing.override_file", "")
viper.SetDefault("pricing.update_interval_hours", 24)
viper.SetDefault("pricing.hash_check_interval_minutes", 10)
// 本地进程插件。插件必须由管理员手动上传,项目默认不携带任何插件能力。
viper.SetDefault("plugins.data_dir", "")
viper.SetDefault("plugins.allow_unsigned", false)
viper.SetDefault("plugins.trusted_publishers", map[string]string{})
viper.SetDefault("plugins.max_upload_bytes", int64(128*1024*1024))
viper.SetDefault("plugins.max_uncompressed_bytes", int64(256*1024*1024))
viper.SetDefault("plugins.start_timeout_seconds", 15)
// Timezone (default to Asia/Shanghai for Chinese users)
viper.SetDefault("timezone", "Asia/Shanghai")
@@ -2260,6 +2361,7 @@ func setDefaults() {
// Gateway
viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
viper.SetDefault("gateway.openai_response_header_timeout", 0)
viper.SetDefault("gateway.grok_response_header_timeout", 120)
viper.SetDefault("gateway.openai_first_output_timeout_seconds", 0)
viper.SetDefault("gateway.openai_high_effort_first_output_timeout_seconds", 0)
viper.SetDefault("gateway.log_upstream_error_body", true)
@@ -2351,6 +2453,10 @@ func setDefaults() {
viper.SetDefault("gateway.grok.free_quota_soft_gate_percent", 95)
viper.SetDefault("gateway.grok.free_quota_window_hours", 24)
viper.SetDefault("gateway.grok.free_quota_stats_cache_seconds", 60)
// 国产供应商余额检测(kimi/deepseek payg;zhipu 无余额端点,仅靠响应式 429/402)。
viper.SetDefault("gateway.cn_providers.balance_check_enabled", true)
viper.SetDefault("gateway.cn_providers.balance_threshold", 0.5)
viper.SetDefault("gateway.cn_providers.balance_check_interval_minutes", 10)
viper.SetDefault("gateway.image_concurrency.enabled", false)
viper.SetDefault("gateway.image_concurrency.max_concurrent_requests", 0)
viper.SetDefault("gateway.image_concurrency.overflow_mode", ImageConcurrencyOverflowModeReject)
@@ -2361,6 +2467,7 @@ func setDefaults() {
viper.SetDefault("gateway.max_body_size", int64(256*1024*1024))
viper.SetDefault("gateway.text_max_body_size", int64(32*1024*1024))
viper.SetDefault("gateway.upstream_response_read_max_bytes", DefaultUpstreamResponseReadMaxBytes)
viper.SetDefault("gateway.models_list_read_max_bytes", DefaultModelsListReadMaxBytes)
viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
viper.SetDefault("gateway.gemini_debug_response_headers", false)
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
@@ -2545,6 +2652,20 @@ func (c *Config) Validate() error {
}
c.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
c.SetForwardedClientIPSettings(c.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
proxyProbeURLs, err := normalizeProxyProbeURLs(c.Security.ProxyProbe.URLs)
if err != nil {
return fmt.Errorf("security.proxy_probe.urls: %w", err)
}
c.Security.ProxyProbe.URLs = proxyProbeURLs
if c.Plugins.MaxUploadBytes <= 0 || c.Plugins.MaxUploadBytes > 1024*1024*1024 {
return fmt.Errorf("plugins.max_upload_bytes must be between 1 and 1073741824")
}
if c.Plugins.MaxUncompressedBytes < c.Plugins.MaxUploadBytes || c.Plugins.MaxUncompressedBytes > 2*1024*1024*1024 {
return fmt.Errorf("plugins.max_uncompressed_bytes must be between max_upload_bytes and 2147483648")
}
if c.Plugins.StartTimeoutSeconds < 1 || c.Plugins.StartTimeoutSeconds > 120 {
return fmt.Errorf("plugins.start_timeout_seconds must be between 1 and 120")
}
if c.Server.ReadHeaderTimeout < 1 || c.Server.ReadHeaderTimeout > 60 {
return fmt.Errorf("server.read_header_timeout must be between 1 and 60 seconds")
}
@@ -3156,6 +3277,9 @@ func (c *Config) Validate() error {
if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
}
if c.Gateway.ModelsListReadMaxBytes <= 0 {
return fmt.Errorf("gateway.models_list_read_max_bytes must be positive")
}
if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
}
@@ -3165,6 +3289,9 @@ func (c *Config) Validate() error {
if c.Gateway.OpenAIResponseHeaderTimeout < 0 {
return fmt.Errorf("gateway.openai_response_header_timeout must be non-negative")
}
if c.Gateway.GrokResponseHeaderTimeout < 0 || c.Gateway.GrokResponseHeaderTimeout > 1800 {
return fmt.Errorf("gateway.grok_response_header_timeout must be between 0-1800 seconds")
}
if c.Gateway.OpenAIFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIFirstOutputTimeoutSeconds > 600 ||
(c.Gateway.OpenAIFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIFirstOutputTimeoutSeconds < 30) {
return fmt.Errorf("gateway.openai_first_output_timeout_seconds must be 0 or between 30-600 seconds")
@@ -3548,6 +3675,9 @@ func (c *Config) Validate() error {
if c.Ops.Cleanup.ErrorLogRetentionDays < 0 {
return fmt.Errorf("ops.cleanup.error_log_retention_days must be non-negative")
}
if c.Ops.Cleanup.Enabled && c.Ops.Cleanup.SystemLogRetentionDays <= 0 {
return fmt.Errorf("ops.cleanup.system_log_retention_days must be positive when ops cleanup is enabled")
}
if c.Ops.Cleanup.MinuteMetricsRetentionDays < 0 {
return fmt.Errorf("ops.cleanup.minute_metrics_retention_days must be non-negative")
}
+73
View File
@@ -23,6 +23,45 @@ func resetViperWithJWTSecret(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
}
func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
resetViperWithJWTSecret(t)
cfg, err := Load()
require.NoError(t, err)
require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
}
func TestLoadTimezonePrecedence(t *testing.T) {
tests := []struct {
name string
fileTimezone string
timezoneEnv string
tzEnv string
want string
}{
{name: "default", want: "Asia/Shanghai"},
{name: "config_file", fileTimezone: "Europe/London", want: "Europe/London"},
{name: "timezone_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", want: "UTC"},
{name: "tz_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", tzEnv: "America/New_York", want: "America/New_York"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resetViperWithJWTSecret(t)
t.Setenv("TIMEZONE", tt.timezoneEnv)
t.Setenv("TZ", tt.tzEnv)
if tt.fileTimezone != "" {
configFile := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(configFile, []byte("timezone: "+tt.fileTimezone+"\n"), 0o600))
t.Setenv("CONFIG_FILE", configFile)
}
cfg, err := Load()
require.NoError(t, err)
require.Equal(t, tt.want, cfg.Timezone)
})
}
}
func TestLoadServerTimingConfig(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
resetViperWithJWTSecret(t)
@@ -521,6 +560,15 @@ func TestLoadOpenAIWSClientFirstMessageTimeoutFromEnv(t *testing.T) {
require.Equal(t, 120, cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds)
}
func TestLoadOpenAIWSForceHTTPFromEnv(t *testing.T) {
resetViperWithJWTSecret(t)
t.Setenv("GATEWAY_OPENAI_WS_FORCE_HTTP", "true")
cfg, err := Load()
require.NoError(t, err)
require.True(t, cfg.Gateway.OpenAIWS.ForceHTTP)
}
func TestLoadDefaultOpenAICompactModel(t *testing.T) {
resetViperWithJWTSecret(t)
@@ -1143,6 +1191,21 @@ func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
}
}
func TestLoadDefaultOpsCleanupConfig(t *testing.T) {
resetViperWithJWTSecret(t)
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if !cfg.Ops.Cleanup.Enabled {
t.Fatal("Ops.Cleanup.Enabled = false, want true")
}
if cfg.Ops.Cleanup.SystemLogRetentionDays != 30 {
t.Fatalf("Ops.Cleanup.SystemLogRetentionDays = %d, want 30", cfg.Ops.Cleanup.SystemLogRetentionDays)
}
}
func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
resetViperWithJWTSecret(t)
@@ -1762,6 +1825,11 @@ func TestValidateConfigErrors(t *testing.T) {
mutate: func(c *Config) { c.Gateway.TextMaxBodySize = c.Gateway.MaxBodySize + 1 },
wantErr: "gateway.text_max_body_size",
},
{
name: "gateway models list read limit",
mutate: func(c *Config) { c.Gateway.ModelsListReadMaxBytes = 0 },
wantErr: "gateway.models_list_read_max_bytes",
},
{
name: "gateway response header timeout",
mutate: func(c *Config) { c.Gateway.ResponseHeaderTimeout = -1 },
@@ -2084,6 +2152,11 @@ func TestValidateConfigErrors(t *testing.T) {
mutate: func(c *Config) { c.Ops.Cleanup.ErrorLogRetentionDays = -1 },
wantErr: "ops.cleanup.error_log_retention_days",
},
{
name: "ops cleanup system log retention",
mutate: func(c *Config) { c.Ops.Cleanup.SystemLogRetentionDays = 0 },
wantErr: "ops.cleanup.system_log_retention_days",
},
{
name: "ops cleanup minute retention",
mutate: func(c *Config) { c.Ops.Cleanup.MinuteMetricsRetentionDays = -1 },
@@ -45,6 +45,20 @@ func collectMapstructureKeys(t reflect.Type, prefix string, out map[string]strin
// is out of scope here — such settings need a config file either way.
continue
}
if ft.Kind() == reflect.Slice {
elem := ft.Elem()
for elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
if elem.Kind() == reflect.Struct {
// AutomaticEnv exposes one string value. Viper's string-to-slice
// hook can populate scalar slices, but it cannot decode a string
// into []struct. Registering a default would turn silent ignore
// into a startup unmarshal error, so structured slices remain
// config-file-only just like maps.
continue
}
}
out[strings.ToLower(key)] = ft.String()
}
}
@@ -62,7 +76,7 @@ func collectMapstructureKeys(t reflect.Type, prefix string, out map[string]strin
// were lost, silently disabling async image tasks for env-driven deployments.
//
// When this fails, register a zero-valued default in setEnvReachableDefaults
// for each reported key.
// for each reported scalar key. Maps and slices of structs are config-file-only.
func TestConfigKeysAreEnvReachable(t *testing.T) {
bound := map[string]string{}
collectMapstructureKeys(reflect.TypeOf(Config{}), "", bound)
@@ -0,0 +1,47 @@
//go:build unit
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNormalizeProxyProbeURLs(t *testing.T) {
t.Parallel()
got, err := normalizeProxyProbeURLs([]ProbeURLConfig{
{URL: " https://chatgpt.com/cdn-cgi/trace ", Parser: " CHATGPT-TRACE "},
{URL: "https://api64.ipify.org?format=json", Parser: "ipify"},
})
require.NoError(t, err)
require.Equal(t, []ProbeURLConfig{
{URL: "https://chatgpt.com/cdn-cgi/trace", Parser: "chatgpt-trace"},
{URL: "https://api64.ipify.org?format=json", Parser: "ipify"},
}, got)
}
func TestNormalizeProxyProbeURLsRejectsInvalidEntries(t *testing.T) {
t.Parallel()
tests := []struct {
name string
target ProbeURLConfig
wantErr string
}{
{name: "missing URL", target: ProbeURLConfig{Parser: "ipify"}, wantErr: "url is required"},
{name: "missing parser", target: ProbeURLConfig{URL: "https://example.com"}, wantErr: "parser is required"},
{name: "unknown parser", target: ProbeURLConfig{URL: "https://example.com", Parser: "ip_api"}, wantErr: "unsupported parser"},
{name: "relative URL", target: ProbeURLConfig{URL: "/cdn-cgi/trace", Parser: "chatgpt-trace"}, wantErr: "invalid url"},
{name: "unsupported scheme", target: ProbeURLConfig{URL: "ftp://example.com/file", Parser: "ipify"}, wantErr: "scheme must be http or https"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := normalizeProxyProbeURLs([]ProbeURLConfig{tt.target})
require.ErrorContains(t, err, tt.wantErr)
})
}
}
@@ -0,0 +1,68 @@
package domain
import "time"
// 渠道监控「配额模式」的归一化配额快照类型。
//
// 配额模式监控不直接对接上游,而是关联一个已有账号,复用账号侧的用量服务
// (AccountUsageService / CNProviderQuotaService / CNProviderBalanceService),
// 把各平台形态各异的用量数据归一成 MonitorQuotaSnapshot,随检测历史持久化
// 到 channel_monitor_histories.quota(JSONB),供管理端与用户端渲染。
//
// 类型放在 domain 包是因为 ent schema(internal/domain 的下游)需要引用它做
// field.JSON 序列化;service 不能被 ent import(会造成循环依赖)。
// MonitorQuotaTier 单个用量窗口的快照。
//
// Window 取值约定(与前端 monitorCommon.quota.windows.* 标签一一对应):
// - "5h" 5 小时滚动窗口(Claude/Codex/Kimi/Zhipu coding plan)
// - "7d" 7 天窗口(Claude/Codex)
// - "7d-sonnet" Claude 7 天 Sonnet 独立额度
// - "7d-fable" Claude 7 天 Fable 独立额度
// - "weekly" 周窗口(Kimi/Zhipu coding plan)
// - "daily" 日窗口(Gemini 日配额 / Grok 日请求)
// - "30d" 30 天窗口(Grok 月度)
// - "total" 无窗口语义的总量额度(Antigravity per-model 等)
//
// 同一 Window 可能出现多条(Gemini 多档日配额、Antigravity per-model、
// Grok requests/tokens),用 Label 区分:Label 是机器 token(requests/tokens/
// shared/pro/flash 或模型名),前端已知 token 走 i18n,未知原样展示。
type MonitorQuotaTier struct {
Window string `json:"window"`
Label string `json:"label,omitempty"`
UsedPercent float64 `json:"used_percent"` // 0-100+;仅有绝对值时按 used/limit 计算
Used float64 `json:"used,omitempty"`
Limit float64 `json:"limit,omitempty"`
ResetAt string `json:"reset_at,omitempty"` // RFC3339;未知时留空
}
// MonitorQuotaSnapshot 一次配额查询的完整快照。
//
// Source 取值:
// - "usage" 海外平台(AccountUsageService.GetUsage)
// - "cn_quota" 国产 Coding Plan(CNProviderQuotaService.QueryUsage)
// - "cn_balance" 国产按量付费余额(CNProviderBalanceService.QueryBalance)
type MonitorQuotaSnapshot struct {
Source string `json:"source"`
Success bool `json:"success"`
Tiers []MonitorQuotaTier `json:"tiers,omitempty"`
Balance *float64 `json:"balance,omitempty"` // cn_balance 主余额
Balances []MonitorBalance `json:"balances,omitempty"` // 多币种余额(如 DeepSeek CNY+USD)
Currency string `json:"currency,omitempty"` // 主余额币种
PlanLevel string `json:"plan_level,omitempty"` // 套餐等级(如智谱 level)
// BalanceLow 余额低于阈值或账号被上游标记不可用(仅 cn_balance 来源)。
// 抓取器按 Gateway.CNProviders.BalanceThreshold 判定,口径与账号停调
// (CNProviderBalanceCheckService.checkOne)一致:任一币种达标即健康。
BalanceLow bool `json:"balance_low,omitempty"`
// CredentialInvalid 上游 401/403 鉴权失败(区别于网络/解析错误),
// 检测状态据此推导 failed 而非 error。
CredentialInvalid bool `json:"credential_invalid,omitempty"`
Error string `json:"error,omitempty"` // Success=false 时的错误摘要
FetchedAt time.Time `json:"fetched_at"`
}
// MonitorBalance 单币种余额条目。
type MonitorBalance struct {
Currency string `json:"currency"`
Balance float64 `json:"balance"`
}
@@ -0,0 +1,15 @@
package domain
// GroupCodexModelsManifestConfig controls which accounts fetch the Codex
// models manifest for an OpenAI group. When enabled, the group's Codex
// /models requests bypass the scheduler and only use the pinned accounts.
type GroupCodexModelsManifestConfig struct {
Enabled bool `json:"enabled"`
// AccountIDs is the ordered list of pinned account IDs. Order decides
// merge precedence for duplicate model slugs.
AccountIDs []int64 `json:"account_ids,omitempty"`
// FallbackToScheduler controls the behavior when no pinned account is
// usable or all upstream fetches fail: fall back to the scheduler loop
// (true) or return the upstream error / 503 (false, default).
FallbackToScheduler bool `json:"fallback_to_scheduler,omitempty"`
}
+29 -5
View File
@@ -23,7 +23,29 @@ const (
PlatformGemini = "gemini"
PlatformAntigravity = "antigravity"
PlatformGrok = "grok"
PlatformComposite = "composite"
// 国产 OpenAI 兼容供应商(经 OpenAI 网关转发,按 Chat Completions 协议)。
PlatformKimi = "kimi" // Kimi (月之暗面 / Moonshot)
PlatformZhipu = "zhipu" // 智谱 GLM (bigmodel)
PlatformDeepseek = "deepseek" // DeepSeek
PlatformMiniMax = "minimax" // MiniMax (M 系列)
PlatformComposite = "composite"
)
// Account mode constants 区分国产供应商的「按量付费(余额)」与「Coding Plan」两种接入方式。
// 存储于 credentials["account_mode"],决定 base_url 预设与额度监控方式。
const (
AccountModePayG = "payg" // 按量付费:消耗余额,做余额检测冷却
AccountModeCoding = "coding" // Coding Plan:滚动用量窗口冷却(5h / weekly)
)
// API protocol constants 国产供应商的上游 API 协议维度。存储于
// credentials["api_protocol"],与 account_mode 正交:协议决定转发端点与格式,
// 模式决定额度监控方式。同协议请求零转换直通;跨协议组合才走转换链。
const (
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
APIProtocolResponses = "responses" // OpenAI Responses(deepseek / kimi / minimax 原生端点,适配 Codex)
APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
)
// Account type constants
@@ -77,6 +99,7 @@ const AntigravityGemini31ProAgentModel = "gemini-pro-agent"
// 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致
var DefaultAntigravityModelMapping = map[string]string{
// Claude 白名单
"claude-fable-5-1": "claude-fable-5-1", // 官方模型
"claude-fable-5": "claude-fable-5", // 官方模型
"claude-opus-4-8": "claude-opus-4-8", // 官方模型
"claude-opus-4-7": "claude-opus-4-7", // 官方模型
@@ -84,11 +107,11 @@ var DefaultAntigravityModelMapping = map[string]string{
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"claude-sonnet-4-5": "claude-sonnet-4-5", // 显式 canonical 选择透传
"claude-sonnet-4-5-thinking": "claude-sonnet-4-6", // 迁移旧兼容别名
// Claude 详细版本 ID 映射
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking", // 迁移旧模型
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-6", // 迁移旧模型
// Claude Haiku → Sonnet(无 Haiku 支持)
"claude-haiku-4-5": "claude-sonnet-4-6",
"claude-haiku-4-5-20251001": "claude-sonnet-4-6",
@@ -137,7 +160,8 @@ var DefaultAntigravityModelMapping = map[string]string{
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
var DefaultBedrockModelMapping = map[string]string{
// Claude Fable
"claude-fable-5": "anthropic.claude-fable-5",
"claude-fable-5-1": "anthropic.claude-fable-5-1",
"claude-fable-5": "anthropic.claude-fable-5",
// Claude Opus
"claude-opus-5": "us.anthropic.claude-opus-5-v1",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
+21 -4
View File
@@ -29,8 +29,9 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) {
t.Parallel()
cases := map[string]string{
"claude-fable-5": "claude-fable-5",
"claude-opus-4-8": "claude-opus-4-8",
"claude-fable-5-1": "claude-fable-5-1",
"claude-fable-5": "claude-fable-5",
"claude-opus-4-8": "claude-opus-4-8",
}
for from, want := range cases {
got, ok := DefaultAntigravityModelMapping[from]
@@ -43,6 +44,21 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) {
}
}
func TestDefaultAntigravityModelMapping_PreservesExplicitSonnet45AndMigratesLegacyAliases(t *testing.T) {
t.Parallel()
cases := map[string]string{
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-6",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-6",
}
for model, want := range cases {
if got := DefaultAntigravityModelMapping[model]; got != want {
t.Fatalf("expected model %q to map to %q, got %q", model, want, got)
}
}
}
func TestDefaultAntigravityModelMapping_Gemini31ProAliases(t *testing.T) {
t.Parallel()
@@ -77,8 +93,9 @@ func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) {
t.Parallel()
cases := map[string]string{
"claude-fable-5": "anthropic.claude-fable-5",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
"claude-fable-5-1": "anthropic.claude-fable-5-1",
"claude-fable-5": "anthropic.claude-fable-5",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
}
for from, want := range cases {
got, ok := DefaultBedrockModelMapping[from]
@@ -0,0 +1,8 @@
package domain
// GroupModelAllowlist 是分组级模型白名单:开启后既过滤模型列表类接口的返回内容,
// 也约束分组实际可调用的模型(网关准入在合成路由改写与调度之前完成)。
type GroupModelAllowlist struct {
Enabled bool `json:"enabled"`
Models []string `json:"models,omitempty"`
}
@@ -1,7 +0,0 @@
package domain
// GroupModelsListConfig controls the optional custom /v1/models response list.
type GroupModelsListConfig struct {
Enabled bool `json:"enabled"`
Models []string `json:"models,omitempty"`
}
+16 -3
View File
@@ -1,8 +1,21 @@
package domain
const (
ReasoningEffortMatchExact = "exact"
ReasoningEffortMatchPrefix = "prefix"
ReasoningEffortMatchSuffix = "suffix"
)
// ReasoningEffortMapping rewrites one explicit OpenAI/Codex reasoning effort
// value to another before the group ceiling is applied.
// value to another before the group ceiling is applied. To may also be "deny"
// to reject the request when the matching source value is present.
//
// Model and MatchType optionally scope the rewrite to a request model:
// exact matches the full model id, prefix/suffix match a model-id affix.
// Empty MatchType and empty Model mean the mapping applies to every model.
type ReasoningEffortMapping struct {
From string `json:"from"`
To string `json:"to"`
From string `json:"from"`
To string `json:"to"`
MatchType string `json:"match_type,omitempty"`
Model string `json:"model,omitempty"`
}
@@ -309,9 +309,11 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
_, _ = h.adminService.UpdateProxy(ctx, existingID, &service.UpdateProxyInput{
Status: normalizedStatus,
ExpiresAt: existingExpiresAt,
ClearExpiresAt: existingExpiresAt == nil,
FallbackMode: existingFallbackMode,
BackupProxyID: existingBackupProxyID,
ExpiryWarnDays: item.ExpiryWarnDays,
ClearBackupID: existingBackupProxyID == nil,
ExpiryWarnDays: &item.ExpiryWarnDays,
Name: proxy.Name,
Protocol: proxy.Protocol,
Host: proxy.Host,
@@ -383,9 +385,11 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
_, _ = h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{
Status: normalizedStatus,
ExpiresAt: expiresAt,
ClearExpiresAt: expiresAt == nil,
FallbackMode: fallbackMode,
BackupProxyID: backupProxyID,
ExpiryWarnDays: item.ExpiryWarnDays,
ClearBackupID: backupProxyID == nil,
ExpiryWarnDays: &item.ExpiryWarnDays,
Name: created.Name,
Protocol: created.Protocol,
Host: created.Host,
+254 -25
View File
@@ -17,6 +17,7 @@ import (
"sync"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/domain"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
@@ -64,6 +65,7 @@ type AccountHandler struct {
grokImportProber grokImportProber
upstreamBillingProbe *service.UpstreamBillingProbeService
ollamaCloudUsage *service.OllamaCloudUsageService
cfg *config.Config
}
// SetUpstreamBillingProbeService attaches the optional remote billing probe service.
@@ -190,6 +192,7 @@ type CheckMixedChannelRequest struct {
// AccountWithConcurrency extends Account with real-time concurrency info
type AccountWithConcurrency struct {
*dto.Account
simpleMode bool `json:"-"`
CurrentConcurrency int `json:"current_concurrency"`
SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
@@ -199,6 +202,123 @@ type AccountWithConcurrency struct {
CurrentRPM *int `json:"current_rpm,omitempty"` // 当前分钟 RPM 计数
}
// AccountListItemWithConcurrency is the compact account-list envelope used
// for lite=1. It embeds dto.AccountListItem instead of the full dto.Account,
// so groups/account_groups never appear in the list payload.
type AccountListItemWithConcurrency struct {
*dto.AccountListItem
CurrentConcurrency int `json:"current_concurrency"`
SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
CurrentWindowCost *float64 `json:"current_window_cost,omitempty"`
ActiveSessions *int `json:"active_sessions,omitempty"`
CurrentRPM *int `json:"current_rpm,omitempty"`
}
type simpleModeGroupReference struct {
ID int64 `json:"id"`
Name string `json:"name"`
Platform string `json:"platform"`
Status string `json:"status"`
}
type simpleModeAccountGroupReference struct {
AccountID int64 `json:"account_id"`
GroupID int64 `json:"group_id"`
Priority int `json:"priority"`
CreatedAt time.Time `json:"created_at"`
Group *simpleModeGroupReference `json:"group,omitempty"`
}
func simpleModeGroupReferenceFromDTO(group *dto.Group) *simpleModeGroupReference {
if group == nil {
return nil
}
return &simpleModeGroupReference{ID: group.ID, Name: group.Name, Platform: group.Platform, Status: group.Status}
}
func simpleModeCompositeGroupIDs(account *dto.Account) map[int64]struct{} {
hidden := make(map[int64]struct{})
if account == nil {
return hidden
}
for _, group := range account.Groups {
if group != nil && group.Platform == service.PlatformComposite {
hidden[group.ID] = struct{}{}
}
}
for _, accountGroup := range account.AccountGroups {
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
hidden[accountGroup.GroupID] = struct{}{}
}
}
return hidden
}
func filterSimpleModeGroupIDs(groupIDs []int64, hidden map[int64]struct{}) []int64 {
visible := make([]int64, 0, len(groupIDs))
for _, groupID := range groupIDs {
if _, ok := hidden[groupID]; !ok {
visible = append(visible, groupID)
}
}
return visible
}
func simpleModeCompositeServiceGroupIDs(account *service.Account) map[int64]struct{} {
hidden := make(map[int64]struct{})
if account == nil {
return hidden
}
for _, group := range account.Groups {
if group != nil && group.Platform == service.PlatformComposite {
hidden[group.ID] = struct{}{}
}
}
for _, accountGroup := range account.AccountGroups {
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
hidden[accountGroup.GroupID] = struct{}{}
}
}
return hidden
}
func (a AccountWithConcurrency) MarshalJSON() ([]byte, error) {
type alias AccountWithConcurrency
if !a.simpleMode || a.Account == nil {
return json.Marshal(alias(a))
}
groups := make([]simpleModeGroupReference, 0, len(a.Groups))
compositeIDs := simpleModeCompositeGroupIDs(a.Account)
for _, group := range a.Groups {
if group != nil && group.Platform == service.PlatformComposite {
continue
}
if ref := simpleModeGroupReferenceFromDTO(group); ref != nil {
groups = append(groups, *ref)
}
}
accountGroups := make([]simpleModeAccountGroupReference, 0, len(a.AccountGroups))
for _, accountGroup := range a.AccountGroups {
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
continue
}
if _, hidden := compositeIDs[accountGroup.GroupID]; hidden {
continue
}
accountGroups = append(accountGroups, simpleModeAccountGroupReference{
AccountID: accountGroup.AccountID, GroupID: accountGroup.GroupID, Priority: accountGroup.Priority,
CreatedAt: accountGroup.CreatedAt, Group: simpleModeGroupReferenceFromDTO(accountGroup.Group),
})
}
return json.Marshal(struct {
alias
GroupIDs []int64 `json:"group_ids,omitempty"`
Groups []simpleModeGroupReference `json:"groups"`
AccountGroups []simpleModeAccountGroupReference `json:"account_groups"`
}{alias: alias(a), GroupIDs: filterSimpleModeGroupIDs(a.GroupIDs, compositeIDs), Groups: groups, AccountGroups: accountGroups})
}
type AccountSchedulerScore struct {
BaseScore float64 `json:"base_score"`
StickyScore float64 `json:"sticky_score"`
@@ -223,9 +343,25 @@ func (h *AccountHandler) accountResponseFromService(account *service.Account) *d
return out
}
func (h *AccountHandler) accountListResponseFromService(account *service.Account) *dto.Account {
out := dto.AccountFromServiceShallow(account)
if out != nil && account != nil {
out.Proxy = dto.ProxyFromService(account.Proxy)
}
if h != nil && h.ollamaCloudUsage != nil && out != nil {
h.ollamaCloudUsage.EnrichState(out.OllamaCloudUsage)
}
return out
}
func (h *AccountHandler) isSimpleMode() bool {
return h != nil && h.cfg != nil && h.cfg.RunMode == config.RunModeSimple
}
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
item := AccountWithConcurrency{
Account: h.accountResponseFromService(account),
simpleMode: h.isSimpleMode(),
CurrentConcurrency: 0,
}
if account == nil {
@@ -650,8 +786,16 @@ func (h *AccountHandler) List(c *gin.Context) {
result := make([]AccountWithConcurrency, len(accounts))
for i := range accounts {
acc := &accounts[i]
accountResponse := h.accountResponseFromService(acc)
if lite {
accountResponse = h.accountListResponseFromService(acc)
if h.isSimpleMode() {
accountResponse.GroupIDs = filterSimpleModeGroupIDs(accountResponse.GroupIDs, simpleModeCompositeServiceGroupIDs(acc))
}
}
item := AccountWithConcurrency{
Account: h.accountResponseFromService(acc),
Account: accountResponse,
simpleMode: h.isSimpleMode(),
CurrentConcurrency: concurrencyCounts[acc.ID],
SchedulerScore: schedulerScores[acc.ID],
SchedulerScores: schedulerGroupScores[acc.ID],
@@ -683,7 +827,34 @@ func (h *AccountHandler) List(c *gin.Context) {
h.enrichShadowParents(c.Request.Context(), result)
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, lite)
if lite {
compact := make([]AccountListItemWithConcurrency, len(result))
for i := range result {
item := result[i]
compact[i] = AccountListItemWithConcurrency{
AccountListItem: dto.AccountListItemFromAccount(item.Account),
CurrentConcurrency: item.CurrentConcurrency,
SchedulerScore: item.SchedulerScore,
SchedulerScores: item.SchedulerScores,
CurrentWindowCost: item.CurrentWindowCost,
ActiveSessions: item.ActiveSessions,
CurrentRPM: item.CurrentRPM,
}
}
etag := buildAccountsListETag(compact, total, page, pageSize, platform, accountType, status, search, true)
if etag != "" {
c.Header("ETag", etag)
c.Header("Vary", "If-None-Match")
if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
c.Status(http.StatusNotModified)
return
}
}
response.Paginated(c, compact, total, page, pageSize)
return
}
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, false)
if etag != "" {
c.Header("ETag", etag)
c.Header("Vary", "If-None-Match")
@@ -696,23 +867,23 @@ func (h *AccountHandler) List(c *gin.Context) {
response.Paginated(c, result, total, page, pageSize)
}
func buildAccountsListETag(
items []AccountWithConcurrency,
func buildAccountsListETag[T any](
items []T,
total int64,
page, pageSize int,
platform, accountType, status, search string,
lite bool,
) string {
payload := struct {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Platform string `json:"platform"`
AccountType string `json:"type"`
Status string `json:"status"`
Search string `json:"search"`
Lite bool `json:"lite"`
Items []AccountWithConcurrency `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Platform string `json:"platform"`
AccountType string `json:"type"`
Status string `json:"status"`
Search string `json:"search"`
Lite bool `json:"lite"`
Items []T `json:"items"`
}{
Total: total,
Page: page,
@@ -837,6 +1008,10 @@ func (h *AccountHandler) Create(c *gin.Context) {
}
// base_rpm 输入校验:负值归零,超过 10000 截断
sanitizeExtraBaseRPM(req.Extra)
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
response.ErrorFrom(c, err)
return
}
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -970,6 +1145,10 @@ func (h *AccountHandler) Update(c *gin.Context) {
}
// base_rpm 输入校验:负值归零,超过 10000 截断
sanitizeExtraBaseRPM(req.Extra)
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
response.ErrorFrom(c, err)
return
}
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -1025,7 +1204,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
// 当前请求。探测错误仅记录日志,不向上下文传播:探测失败时标记保持缺失,
// 网关会按"现状即证据"默认走 Responses。
func (h *AccountHandler) scheduleOpenAIResponsesProbe(account *service.Account) {
if account == nil || account.Platform != service.PlatformOpenAI || account.Type != service.AccountTypeAPIKey {
if account == nil || account.Type != service.AccountTypeAPIKey ||
(account.Platform != service.PlatformOpenAI && !service.IsCNProvider(account.Platform)) {
return
}
if h.accountTestService == nil {
@@ -1423,6 +1603,10 @@ func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
response.ErrorFrom(c, err)
return
}
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
response.ErrorFrom(c, err)
return
}
// Drop SSO/password residue; re-auth must leave only OAuth tokens on disk.
req.Credentials = service.SanitizeStoredCredentials(existing.Platform, req.Credentials)
@@ -1874,6 +2058,14 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
return
}
}
groupIDs := make([]int64, 0)
for _, item := range req.Accounts {
groupIDs = append(groupIDs, item.GroupIDs...)
}
if err := h.adminService.ValidateAccountGroupBindings(c.Request.Context(), groupIDs); err != nil {
response.ErrorFrom(c, err)
return
}
executeAdminIdempotentJSON(c, "admin.accounts.batch_create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
success := 0
@@ -1896,6 +2088,15 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
// base_rpm 输入校验:负值归零,超过 10000 截断
sanitizeExtraBaseRPM(item.Extra)
if err := service.ValidateUpstreamRequestIDHeaderExtra(item.Extra); err != nil {
failed++
results = append(results, gin.H{
"name": item.Name,
"success": false,
"error": err.Error(),
})
continue
}
skipCheck := item.ConfirmMixedChannelRisk != nil && *item.ConfirmMixedChannelRisk
@@ -2090,6 +2291,10 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
}
// base_rpm 输入校验:负值归零,超过 10000 截断
sanitizeExtraBaseRPM(req.Extra)
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
response.ErrorFrom(c, err)
return
}
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -2575,6 +2780,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
// Handle OpenAI accounts
if account.IsOpenAI() {
// Prefer the shared, account-keyed upstream catalog. If discovery fails,
// retain the legacy local catalog below so the test dialog remains usable.
if h.accountTestService != nil {
if models, fetchErr := h.accountTestService.FetchOpenAIAccountModels(c.Request.Context(), account); fetchErr == nil {
response.Success(c, models)
return
}
}
// OpenAI 自动透传会绕过常规模型改写,测试/模型列表也应回落到默认模型集。
if account.IsOpenAIPassthroughEnabled() {
response.Success(c, openai.DefaultModels)
@@ -2613,8 +2826,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
// Handle Gemini accounts
if account.IsGemini() {
// For OAuth accounts: return default Gemini models
// Consumer Google One OAuth still uses the legacy Gemini CLI / Code
// Assist channel. Do not advertise newer 3.x or image models that the
// channel cannot serve.
if account.IsOAuth() {
if account.IsGeminiGoogleOne() {
response.Success(c, geminicli.GoogleOneModels)
return
}
response.Success(c, geminicli.DefaultModels)
return
}
@@ -2767,13 +2986,15 @@ func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
return
}
models, err := h.accountTestService.FetchUpstreamSupportedModels(c.Request.Context(), account)
catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), account)
if err != nil {
var syncErr *service.UpstreamModelSyncError
if errors.As(err, &syncErr) {
switch syncErr.Kind {
case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
response.BadRequest(c, syncErr.SafeMessage())
case service.UpstreamModelSyncErrorInternal:
response.InternalError(c, syncErr.SafeMessage())
default:
slog.Warn("sync_upstream_models_failed", "account_id", accountID, "kind", syncErr.Kind)
response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
@@ -2786,29 +3007,35 @@ func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
return
}
response.Success(c, gin.H{"models": models})
response.Success(c, catalog)
}
// SyncUpstreamModelsPreview handles syncing live supported models using provided credentials (no account ID needed).
// POST /api/v1/admin/accounts/models/sync-upstream-preview
func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
var req struct {
Platform string `json:"platform" binding:"required"`
Type string `json:"type" binding:"required"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key" binding:"required"`
Platform string `json:"platform" binding:"required"`
Type string `json:"type" binding:"required"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key" binding:"required"`
ModelMapping map[string]string `json:"model_mapping"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
modelMapping := make(map[string]any, len(req.ModelMapping))
for sourceModel, upstreamModel := range req.ModelMapping {
modelMapping[sourceModel] = upstreamModel
}
tempAccount := &service.Account{
Platform: req.Platform,
Type: req.Type,
Credentials: map[string]any{
"api_key": req.APIKey,
"base_url": req.BaseURL,
"api_key": req.APIKey,
"base_url": req.BaseURL,
"model_mapping": modelMapping,
},
}
@@ -2817,13 +3044,15 @@ func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
return
}
models, err := h.accountTestService.FetchUpstreamSupportedModels(c.Request.Context(), tempAccount)
catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), tempAccount)
if err != nil {
var syncErr *service.UpstreamModelSyncError
if errors.As(err, &syncErr) {
switch syncErr.Kind {
case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
response.BadRequest(c, syncErr.SafeMessage())
case service.UpstreamModelSyncErrorInternal:
response.InternalError(c, syncErr.SafeMessage())
default:
slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform, "kind", syncErr.Kind)
response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
@@ -2836,7 +3065,7 @@ func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
return
}
response.Success(c, gin.H{"models": models})
response.Success(c, catalog)
}
// SetPrivacy handles setting privacy for a single OpenAI/Antigravity OAuth account
@@ -38,14 +38,20 @@ func setupAvailableModelsRouter(adminSvc service.AdminService) *gin.Engine {
}
type syncUpstreamHTTPUpstream struct {
resp *http.Response
err error
resp *http.Response
responses []*http.Response
err error
}
func (u *syncUpstreamHTTPUpstream) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
if u.err != nil {
return nil, u.err
}
if len(u.responses) > 0 {
resp := u.responses[0]
u.responses = u.responses[1:]
return resp, nil
}
return u.resp, nil
}
@@ -68,6 +74,7 @@ func setupSyncUpstreamModelsRouter(adminSvc service.AdminService, upstream servi
)
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, accountTestSvc, nil, nil, nil, nil, nil)
router.POST("/api/v1/admin/accounts/:id/models/sync-upstream", handler.SyncUpstreamModels)
router.POST("/api/v1/admin/accounts/models/sync-upstream-preview", handler.SyncUpstreamModelsPreview)
return router
}
@@ -287,6 +294,42 @@ func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(
}, ids, "影子可用模型由 model_mapping 派生(非写死)")
}
func TestAccountHandlerGetAvailableModels_GeminiGoogleOneUsesConservativeCatalog(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
account: service.Account{
ID: 45,
Name: "google-one",
Platform: service.PlatformGemini,
Type: service.AccountTypeOAuth,
Status: service.StatusActive,
Credentials: map[string]any{
"oauth_type": "google_one",
},
},
}
router := setupAvailableModelsRouter(svc)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/45/models", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var resp struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
ids := make([]string, 0, len(resp.Data))
for _, model := range resp.Data {
ids = append(ids, model.ID)
}
require.ElementsMatch(t, []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"}, ids)
require.NotContains(t, ids, "gemini-3.5-flash")
require.NotContains(t, ids, "gemini-2.5-flash-image")
}
func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
@@ -311,6 +354,99 @@ func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testin
require.Contains(t, rec.Body.String(), "No OpenAI API key is available")
}
func TestAccountHandlerSyncUpstreamModelsReturnsCapabilityMetadata(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
account: service.Account{
ID: 48, Name: "custom-openai", Platform: service.PlatformOpenAI,
Type: service.AccountTypeAPIKey, Status: service.StatusActive,
Credentials: map[string]any{"api_key": "key", "base_url": "https://provider.example/v1"},
},
}
upstream := &syncUpstreamHTTPUpstream{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"models":[{
"id":"custom-thinking-model",
"reasoning":true,
"default_reasoning_level":"high",
"supported_reasoning_levels":["low","high"],
"input_modalities":["text","image"],
"context_window":256000
}]}`)),
}}
router := setupSyncUpstreamModelsRouter(svc, upstream)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/48/models/sync-upstream", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var resp struct {
Data service.UpstreamModelCatalog `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, []string{"custom-thinking-model"}, resp.Data.Models)
metadata := resp.Data.Metadata["custom-thinking-model"]
require.NotNil(t, metadata.Reasoning)
require.True(t, *metadata.Reasoning)
require.Equal(t, []string{"low", "high"}, metadata.SupportedReasoningLevels)
require.Equal(t, []string{"text", "image"}, metadata.InputModalities)
}
// Scenario: 创建账号 preview 将具体 mapping 传给 404/405 配置回退。
func TestAccountHandlerSyncUpstreamModelsPreviewUsesProvidedModelMapping(t *testing.T) {
upstream := &syncUpstreamHTTPUpstream{responses: []*http.Response{
{
StatusCode: http.StatusNotFound,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"error":"not found"}`)),
},
{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{
"configured-provider": {
"api": "https://provider.example/v1",
"models": {
"glm-5.3": {
"id": "glm-5.3",
"reasoning": true,
"reasoning_options": [{"type":"effort","values":["low","high"]}],
"modalities": {"input":["text"],"output":["text"]},
"limit": {"context":1000000,"output":131072}
}
}
}
}`)),
},
}}
router := setupSyncUpstreamModelsRouter(newStubAdminService(), upstream)
rec := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodPost,
"/api/v1/admin/accounts/models/sync-upstream-preview",
strings.NewReader(`{
"platform":"openai",
"type":"apikey",
"base_url":"https://provider.example/v1",
"api_key":"key",
"model_mapping":{"public-glm":"glm-5.3"}
}`),
)
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var resp struct {
Data service.UpstreamModelCatalog `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, []string{"glm-5.3"}, resp.Data.Models)
require.Equal(t, []string{"low", "high"}, resp.Data.Metadata["glm-5.3"].SupportedReasoningLevels)
}
func TestAccountHandlerSyncUpstreamModels_UpstreamErrorDoesNotExposeBody(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
@@ -341,3 +477,52 @@ func TestAccountHandlerSyncUpstreamModels_UpstreamErrorDoesNotExposeBody(t *test
require.Contains(t, rec.Body.String(), "Upstream model list request failed with HTTP 502")
require.NotContains(t, rec.Body.String(), "SECRET_TOKEN")
}
// Scenario: 能力补全失败显示部分成功。
func TestAccountHandlerSyncUpstreamModels_MetadataEnrichmentFailureReturnsWarning(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
account: service.Account{
ID: 46,
Name: "opencode-id-only-model-list",
Platform: service.PlatformOpenAI,
Type: service.AccountTypeAPIKey,
Status: service.StatusActive,
Credentials: map[string]any{
"api_key": "opencode-key",
"base_url": "https://opencode.ai/zen/v1",
},
},
}
upstream := &syncUpstreamHTTPUpstream{responses: []*http.Response{
{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"x-preview-f-free"}]}`)),
},
{
StatusCode: http.StatusBadGateway,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"error":"registry unavailable"}`)),
},
}}
router := setupSyncUpstreamModelsRouter(svc, upstream)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/46/models/sync-upstream", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var resp struct {
Data struct {
Models []string `json:"models"`
Warnings []struct {
Code string `json:"code"`
} `json:"warnings"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, []string{"x-preview-f-free"}, resp.Data.Models)
require.Len(t, resp.Data.Warnings, 1)
require.Equal(t, "upstream_model_metadata_incomplete", resp.Data.Warnings[0].Code)
}
@@ -1,9 +1,12 @@
package admin
import (
"bytes"
"compress/gzip"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -13,6 +16,101 @@ import (
"github.com/stretchr/testify/require"
)
func TestAccountHandlerListLiteUsesCompactDTOAndETag(t *testing.T) {
router, adminSvc := setupAccountListRouter()
now := time.Now().UTC()
groupID := int64(77)
adminSvc.accounts = []service.Account{{
ID: 501, Name: "compact-account", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth,
Credentials: map[string]any{"email": "compact@example.com", "access_token": strings.Repeat("x", 4096)},
Extra: map[string]any{"privacy_mode": "training_off"}, Status: service.StatusActive,
Schedulable: true, Concurrency: 4, GroupIDs: []int64{groupID},
Groups: []*service.Group{{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}},
AccountGroups: []service.AccountGroup{{AccountID: 501, GroupID: groupID, Priority: 2, Group: &service.Group{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}}},
CreatedAt: now, UpdatedAt: now,
}}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.NotEmpty(t, rec.Header().Get("ETag"))
var litePayload struct {
Data struct {
Items []map[string]any `json:"items"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &litePayload))
require.Len(t, litePayload.Data.Items, 1)
liteItem := litePayload.Data.Items[0]
require.Equal(t, float64(501), liteItem["id"])
require.Equal(t, []any{float64(groupID)}, liteItem["group_ids"])
require.Equal(t, true, liteItem["schedulable"])
require.NotContains(t, liteItem, "groups")
require.NotContains(t, liteItem, "account_groups")
credentials, ok := liteItem["credentials"].(map[string]any)
require.True(t, ok)
require.Equal(t, "compact@example.com", credentials["email"])
require.NotContains(t, credentials, "access_token")
credentialsStatus, ok := liteItem["credentials_status"].(map[string]any)
require.True(t, ok)
require.Equal(t, true, credentialsStatus["has_access_token"])
// The ETag must represent the same compact body and return 304 on refresh.
rec304 := httptest.NewRecorder()
req304 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
req304.Header.Set("If-None-Match", rec.Header().Get("ETag"))
router.ServeHTTP(rec304, req304)
require.Equal(t, http.StatusNotModified, rec304.Code)
// Omitting lite preserves the legacy full response shape.
recFull := httptest.NewRecorder()
reqFull := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20", nil)
router.ServeHTTP(recFull, reqFull)
require.Equal(t, http.StatusOK, recFull.Code)
var fullPayload struct {
Data struct {
Items []map[string]any `json:"items"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(recFull.Body.Bytes(), &fullPayload))
require.Contains(t, fullPayload.Data.Items[0], "groups")
require.Contains(t, fullPayload.Data.Items[0], "account_groups")
}
func TestAccountHandlerListLiteStaysBelowResponseBudget(t *testing.T) {
router, adminSvc := setupAccountListRouter()
now := time.Now().UTC()
accounts := make([]service.Account, 20)
for i := range accounts {
id := int64(600 + i)
groupID := int64(800 + i)
accounts[i] = service.Account{
ID: id, Name: "account-" + strconv.Itoa(i), Platform: service.PlatformOpenAI,
Type: service.AccountTypeOAuth, Status: service.StatusActive, Schedulable: true,
Concurrency: 4, GroupIDs: []int64{groupID},
Groups: []*service.Group{{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}},
AccountGroups: []service.AccountGroup{{AccountID: id, GroupID: groupID, Group: &service.Group{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}}},
CreatedAt: now, UpdatedAt: now,
}
}
adminSvc.accounts = accounts
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Less(t, rec.Body.Len(), 80*1024)
var compressed bytes.Buffer
zw := gzip.NewWriter(&compressed)
_, err := zw.Write(rec.Body.Bytes())
require.NoError(t, err)
require.NoError(t, zw.Close())
require.Less(t, compressed.Len(), 15*1024)
}
func setupAccountListRouter() (*gin.Engine, *stubAdminService) {
gin.SetMode(gin.TestMode)
router := gin.New()
@@ -0,0 +1,240 @@
package admin
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type simpleModeAccountService struct {
*stubAdminService
account service.Account
createCalls int
updateCalls int
bulkCalls int
}
func (s *simpleModeAccountService) GetAccount(context.Context, int64) (*service.Account, error) {
return &s.account, nil
}
func (s *simpleModeAccountService) CreateAccount(ctx context.Context, input *service.CreateAccountInput) (*service.Account, error) {
if err := s.ValidateAccountGroupBindings(ctx, input.GroupIDs); err != nil {
return nil, err
}
s.createCalls++
return &s.account, nil
}
func (s *simpleModeAccountService) UpdateAccount(ctx context.Context, _ int64, input *service.UpdateAccountInput) (*service.Account, error) {
if input.GroupIDs != nil {
if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
return nil, err
}
}
s.updateCalls++
return &s.account, nil
}
func (s *simpleModeAccountService) BulkUpdateAccounts(ctx context.Context, input *service.BulkUpdateAccountsInput) (*service.BulkUpdateAccountsResult, error) {
if input.GroupIDs != nil {
if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
return nil, err
}
}
s.bulkCalls++
return &service.BulkUpdateAccountsResult{}, nil
}
func (s *simpleModeAccountService) ValidateAccountGroupBindings(_ context.Context, groupIDs []int64) error {
for _, id := range groupIDs {
for i := range s.groups {
if s.groups[i].ID == id && s.groups[i].Platform == service.PlatformComposite {
return infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups cannot be bound in simple mode")
}
}
}
return nil
}
func TestAccountHandlerSimpleModeUsesMinimalGroupReferences(t *testing.T) {
gin.SetMode(gin.TestMode)
richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9, IsExclusive: true, SubscriptionType: service.SubscriptionTypeSubscription, RPMLimit: 99}
historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
account := service.Account{
ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
GroupIDs: []int64{7, 9},
Groups: []*service.Group{richGroup, historicalComposite},
AccountGroups: []service.AccountGroup{
{AccountID: 3, GroupID: 7, Priority: 2, Group: richGroup, Account: &service.Account{ID: 3, Extra: map[string]any{"secret": true}}},
{AccountID: 3, GroupID: 9, Priority: 3, Group: historicalComposite},
},
}
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
svc.accounts = []service.Account{account}
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h.cfg = &config.Config{RunMode: config.RunModeSimple}
r := gin.New()
r.GET("/accounts", h.List)
r.GET("/accounts/:id", h.GetByID)
r.POST("/accounts", h.Create)
r.PUT("/accounts/:id", h.Update)
tests := []struct {
method string
path string
body string
}{
{http.MethodGet, "/accounts", ""},
{http.MethodGet, "/accounts/3", ""},
{http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"}}`},
{http.MethodPut, "/accounts/3", `{"name":"account"}`},
}
for _, tt := range tests {
t.Run(tt.method+tt.path, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
req.Header.Set("Content-Type", "application/json")
res := httptest.NewRecorder()
r.ServeHTTP(res, req)
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
var payload map[string]any
require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
data, ok := payload["data"].(map[string]any)
require.True(t, ok)
if items, ok := data["items"].([]any); ok {
require.Len(t, items, 1)
data, ok = items[0].(map[string]any)
require.True(t, ok)
}
groups, ok := data["groups"].([]any)
require.True(t, ok)
require.Len(t, groups, 1)
require.Equal(t, []any{float64(7)}, data["group_ids"])
group, ok := groups[0].(map[string]any)
require.True(t, ok)
require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(group))
accountGroups, ok := data["account_groups"].([]any)
require.True(t, ok)
require.Len(t, accountGroups, 1)
accountGroup, ok := accountGroups[0].(map[string]any)
require.True(t, ok)
require.ElementsMatch(t, []string{"account_id", "group_id", "priority", "created_at", "group"}, mapKeys(accountGroup))
nestedGroup, ok := accountGroup["group"].(map[string]any)
require.True(t, ok)
require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(nestedGroup))
})
}
}
func TestAccountHandlerSimpleModeLitePreservesCompactShapeAndETag(t *testing.T) {
richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9}
historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
account := service.Account{
ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
GroupIDs: []int64{7, 9}, Groups: []*service.Group{richGroup, historicalComposite},
AccountGroups: []service.AccountGroup{{AccountID: 3, GroupID: 7, Group: richGroup}, {AccountID: 3, GroupID: 9, Group: historicalComposite}},
}
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
svc.accounts = []service.Account{account}
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h.cfg = &config.Config{RunMode: config.RunModeSimple}
r := gin.New()
r.GET("/accounts", h.List)
res := httptest.NewRecorder()
r.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil))
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
require.NotEmpty(t, res.Header().Get("ETag"))
var payload struct {
Data struct {
Items []map[string]any `json:"items"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
require.Len(t, payload.Data.Items, 1)
require.Equal(t, []any{float64(7)}, payload.Data.Items[0]["group_ids"])
require.NotContains(t, payload.Data.Items[0], "groups")
require.NotContains(t, payload.Data.Items[0], "account_groups")
notModified := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil)
req.Header.Set("If-None-Match", res.Header().Get("ETag"))
r.ServeHTTP(notModified, req)
require.Equal(t, http.StatusNotModified, notModified.Code)
}
func TestAccountHandlerSimpleModeRejectsCompositeGroupBindingsBeforeWrites(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, tt := range []struct{ name, method, path, body string }{
{"create", http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[9]}`},
{"update", http.MethodPut, "/accounts/3", `{"group_ids":[9]}`},
{"bulk", http.MethodPost, "/accounts/bulk-update", `{"account_ids":[3],"group_ids":[9]}`},
} {
t.Run(tt.name, func(t *testing.T) {
svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h.cfg = &config.Config{RunMode: config.RunModeSimple}
r := gin.New()
r.POST("/accounts", h.Create)
r.PUT("/accounts/:id", h.Update)
r.POST("/accounts/bulk-update", h.BulkUpdate)
res := httptest.NewRecorder()
req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(res, req)
require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
require.Zero(t, svc.createCalls)
require.Zero(t, svc.updateCalls)
require.Zero(t, svc.bulkCalls)
})
}
}
func TestAccountHandlerSimpleModePreservesBasicGroupBinding(t *testing.T) {
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: service.Account{ID: 3}}
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}}
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h.cfg = &config.Config{RunMode: config.RunModeSimple}
r := gin.New()
r.POST("/accounts", h.Create)
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/accounts", bytes.NewBufferString(`{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]}`))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(res, req)
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
require.Equal(t, 1, svc.createCalls)
}
func TestAccountHandlerSimpleModeBatchPrevalidatesAllGroupsAtomically(t *testing.T) {
svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h.cfg = &config.Config{RunMode: config.RunModeSimple}
r := gin.New()
r.POST("/accounts/batch", h.BatchCreate)
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/accounts/batch", bytes.NewBufferString(`{"accounts":[{"name":"first","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]},{"name":"second","platform":"anthropic","type":"apikey","credentials":{"key":"y"},"group_ids":[9]}]}`))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(res, req)
require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
require.Zero(t, svc.createCalls)
}
func TestAccountHandlerAdvancedModeKeepsFullGroupReferences(t *testing.T) {
group := &service.Group{ID: 7, Name: "advanced", RateMultiplier: 9, SubscriptionType: service.SubscriptionTypeSubscription}
h := NewAccountHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
raw, err := json.Marshal(h.buildAccountResponseWithRuntime(context.Background(), &service.Account{Groups: []*service.Group{group}}))
require.NoError(t, err)
require.Contains(t, string(raw), `"rate_multiplier":9`)
require.Contains(t, string(raw), `"subscription_type":"subscription"`)
}
@@ -0,0 +1,99 @@
package admin
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// upstreamBillingRatesResponse is deliberately smaller than the regular
// account-list response. The frontend uses the returned ID order to detect a
// page boundary/order change and only applies snapshots when the page is
// otherwise stable.
type upstreamBillingRatesResponse struct {
Items []service.UpstreamBillingRateSnapshotItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// GetUpstreamBillingRates returns persisted upstream billing snapshots for the
// current account-list page. It never probes an upstream and never loads
// today-stats, usage, concurrency, or credentials for the response.
//
// GET /api/v1/admin/accounts/upstream-billing-rates
func (h *AccountHandler) GetUpstreamBillingRates(c *gin.Context) {
if h.adminService == nil {
response.Error(c, http.StatusServiceUnavailable, "account service unavailable")
return
}
page, pageSize := response.ParsePagination(c)
platform := c.Query("platform")
accountType := c.Query("type")
status := c.Query("status")
search := strings.TrimSpace(c.Query("search"))
if len(search) > 100 {
search = search[:100]
}
privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
sortBy := c.DefaultQuery("sort_by", "name")
sortOrder := c.DefaultQuery("sort_order", "asc")
var groupID int64
if groupQuery := c.Query("group"); groupQuery != "" {
if groupQuery == accountListGroupUngroupedQueryValue {
groupID = service.AccountListGroupUngrouped
} else {
parsed, err := strconv.ParseInt(groupQuery, 10, 64)
if err != nil || parsed < 0 {
response.BadRequest(c, "invalid group filter")
return
}
groupID = parsed
}
}
accounts, total, err := h.adminService.ListAccounts(
c.Request.Context(), page, pageSize, platform, accountType, status,
search, groupID, privacyMode, sortBy, sortOrder,
)
if err != nil {
response.ErrorFrom(c, err)
return
}
payload := upstreamBillingRatesResponse{
Items: service.BuildUpstreamBillingRateSnapshotItems(accounts),
Total: total,
Page: page,
PageSize: pageSize,
}
c.Header("Cache-Control", "private, no-cache")
etag := buildUpstreamBillingRatesETag(payload)
if etag != "" {
c.Header("ETag", etag)
if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
c.Status(http.StatusNotModified)
return
}
}
response.Success(c, payload)
}
func buildUpstreamBillingRatesETag(payload upstreamBillingRatesResponse) string {
raw, err := json.Marshal(payload)
if err != nil {
return ""
}
sum := sha256.Sum256(raw)
return "\"" + hex.EncodeToString(sum[:]) + "\""
}
@@ -33,7 +33,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
router.GET("/api/v1/admin/groups", groupHandler.List)
router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
router.GET("/api/v1/admin/groups/:id/models-list-candidates", groupHandler.GetModelsListCandidates)
router.GET("/api/v1/admin/groups/:id/model-allowlist-candidates", groupHandler.GetGroupModelAllowlistCandidates)
router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
@@ -184,7 +184,7 @@ func TestGroupHandlerEndpoints(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/models-list-candidates?platform=openai", nil)
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/model-allowlist-candidates?platform=openai", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Body.String(), "gpt-5.5")
@@ -278,13 +278,15 @@ func TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier(t *testing.T) {
Rules: []dto.OpenAIFastPolicyRule{
{ServiceTier: "priority", Action: "filter", Scope: "all"},
{ServiceTier: "flex", Action: "block", Scope: "oauth"},
{ServiceTier: "ultrafast", Action: "pass", Scope: "all"},
{ServiceTier: "all", Action: "pass", Scope: "apikey"},
},
}
out := openaiFastPolicySettingsFromDTO(in)
require.Len(t, out.Rules, 3)
require.Len(t, out.Rules, 4)
require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
require.Equal(t, service.OpenAIFastTierFlex, out.Rules[1].ServiceTier)
require.Equal(t, service.OpenAIFastTierAny, out.Rules[2].ServiceTier)
require.Equal(t, service.OpenAIFastTierUltrafast, out.Rules[2].ServiceTier)
require.Equal(t, service.OpenAIFastTierAny, out.Rules[3].ServiceTier)
})
}
@@ -24,6 +24,13 @@ type stubAdminService struct {
boundAuthIdentity *service.AdminBindAuthIdentityInput
boundAuthIdentityFor int64
createdAccounts []*service.CreateAccountInput
createdGroups []*service.CreateGroupInput
updatedGroups []*service.UpdateGroupInput
deletedGroupIDs []int64
guardedDeletedGroupIDs []int64
deleteGroupIfEmptyErr error
advancedGroupOperationCalls int
lastListGroupsIsExclusive *bool
createdProxies []*service.CreateProxyInput
updatedProxyIDs []int64
updatedProxies []*service.UpdateProxyInput
@@ -267,6 +274,7 @@ func (s *stubAdminService) BindUserAuthIdentity(ctx context.Context, userID int6
}
func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]service.Group, int64, error) {
s.lastListGroupsIsExclusive = isExclusive
return s.groups, int64(len(s.groups)), nil
}
@@ -295,6 +303,7 @@ func (s *stubAdminService) GetGroupModelsListCandidates(ctx context.Context, id
}
func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int64) ([]service.CompositeModelRoute, error) {
s.advancedGroupOperationCalls++
return []service.CompositeModelRoute{
{
ID: 1,
@@ -311,6 +320,7 @@ func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int6
}
func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
s.advancedGroupOperationCalls++
return &service.CompositeModelRoute{
ID: 1,
GroupID: groupID,
@@ -326,6 +336,7 @@ func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int
}
func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
s.advancedGroupOperationCalls++
return &service.CompositeModelRoute{
ID: routeID,
GroupID: groupID,
@@ -341,10 +352,12 @@ func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, ro
}
func (s *stubAdminService) DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error {
s.advancedGroupOperationCalls++
return nil
}
func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRoutePreviewRequest) (*service.CompositeRouteDecision, error) {
s.advancedGroupOperationCalls++
decision, err := service.NewCompositeRouteResolver(nil).Resolve(ctx, groupID, input.Model, input.Endpoint)
if err != nil {
return nil, err
@@ -353,11 +366,13 @@ func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID in
}
func (s *stubAdminService) CreateGroup(ctx context.Context, input *service.CreateGroupInput) (*service.Group, error) {
s.createdGroups = append(s.createdGroups, input)
group := service.Group{ID: 200, Name: input.Name, Status: service.StatusActive}
return &group, nil
}
func (s *stubAdminService) DuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*service.Group, error) {
s.advancedGroupOperationCalls++
group := service.Group{ID: 201, Name: "group (Copy)", Status: "inactive"}
return &group, nil
}
@@ -367,14 +382,21 @@ func (s *stubAdminService) RecoverDuplicateGroup(ctx context.Context, id int64,
}
func (s *stubAdminService) UpdateGroup(ctx context.Context, id int64, input *service.UpdateGroupInput) (*service.Group, error) {
s.updatedGroups = append(s.updatedGroups, input)
group := service.Group{ID: id, Name: input.Name, Status: service.StatusActive}
return &group, nil
}
func (s *stubAdminService) DeleteGroup(ctx context.Context, id int64) error {
s.deletedGroupIDs = append(s.deletedGroupIDs, id)
return nil
}
func (s *stubAdminService) DeleteGroupIfEmpty(ctx context.Context, id int64) error {
s.guardedDeletedGroupIDs = append(s.guardedDeletedGroupIDs, id)
return s.deleteGroupIfEmptyErr
}
func (s *stubAdminService) GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]service.APIKey, int64, error) {
return s.apiKeys, int64(len(s.apiKeys)), nil
}
@@ -384,10 +406,12 @@ func (s *stubAdminService) GetGroupRateMultipliers(_ context.Context, _ int64) (
}
func (s *stubAdminService) ClearGroupRateMultipliers(_ context.Context, _ int64) error {
s.advancedGroupOperationCalls++
return nil
}
func (s *stubAdminService) BatchSetGroupRateMultipliers(_ context.Context, _ int64, _ []service.GroupRateMultiplierInput) error {
s.advancedGroupOperationCalls++
return nil
}
@@ -396,6 +420,7 @@ func (s *stubAdminService) ClearGroupRPMOverrides(_ context.Context, _ int64) er
}
func (s *stubAdminService) BatchSetGroupRPMOverrides(_ context.Context, _ int64, _ []service.GroupRPMOverrideInput) error {
s.advancedGroupOperationCalls++
return nil
}
@@ -491,6 +516,8 @@ func (s *stubAdminService) CreateAccount(ctx context.Context, input *service.Cre
return &account, nil
}
func (s *stubAdminService) ValidateAccountGroupBindings(context.Context, []int64) error { return nil }
func (s *stubAdminService) DuplicateAccount(ctx context.Context, id int64, actorScope, operationKey string) (*service.Account, error) {
account := service.Account{ID: 301, Name: "account (Copy)", Status: service.StatusActive, Schedulable: false}
return &account, nil
+225 -96
View File
@@ -57,29 +57,51 @@ type updateChannelRequest struct {
}
type channelModelPricingRequest struct {
Platform string `json:"platform" binding:"omitempty,max=50"`
Models []string `json:"models" binding:"required,min=1,max=100"`
BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
Intervals []pricingIntervalRequest `json:"intervals"`
Platform string `json:"platform" binding:"omitempty,max=50"`
Models []string `json:"models" binding:"required,min=1,max=100"`
BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price" binding:"omitempty,min=0"`
CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"`
FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"`
MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier" binding:"omitempty,gt=0"`
ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
Intervals []pricingIntervalRequest `json:"intervals"`
TimePricing *channelTimePricingRequest `json:"time_pricing"`
}
type channelTimePricingRequest struct {
Timezone string `json:"timezone"`
WeekdaysOnly bool `json:"weekdays_only"`
Periods []channelTimePricingPeriodRequest `json:"periods"`
}
type channelTimePricingPeriodRequest struct {
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Multiplier float64 `json:"multiplier"`
}
type pricingIntervalRequest struct {
MinTokens int `json:"min_tokens"`
MaxTokens *int `json:"max_tokens"`
TierLabel string `json:"tier_label"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
PerRequestPrice *float64 `json:"per_request_price"`
SortOrder int `json:"sort_order"`
MinTokens int `json:"min_tokens"`
MaxTokens *int `json:"max_tokens"`
TierLabel string `json:"tier_label"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
InputMultiplier *float64 `json:"input_multiplier" binding:"omitempty,gt=0"`
OutputMultiplier *float64 `json:"output_multiplier" binding:"omitempty,gt=0"`
CacheWriteMultiplier *float64 `json:"cache_write_multiplier" binding:"omitempty,gt=0"`
CacheReadMultiplier *float64 `json:"cache_read_multiplier" binding:"omitempty,gt=0"`
PerRequestPrice *float64 `json:"per_request_price"`
SortOrder int `json:"sort_order"`
}
type accountStatsPricingRuleRequest struct {
@@ -108,31 +130,53 @@ type channelResponse struct {
}
type channelModelPricingResponse struct {
ID int64 `json:"id"`
Platform string `json:"platform"`
Models []string `json:"models"`
BillingMode string `json:"billing_mode"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
ImageInputPrice *float64 `json:"image_input_price"`
ImageOutputPrice *float64 `json:"image_output_price"`
PerRequestPrice *float64 `json:"per_request_price"`
Intervals []pricingIntervalResponse `json:"intervals"`
ID int64 `json:"id"`
Platform string `json:"platform"`
Models []string `json:"models"`
BillingMode string `json:"billing_mode"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
FastMultiplier *float64 `json:"fast_multiplier"`
FlexMultiplier *float64 `json:"flex_multiplier"`
MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
ImageInputPrice *float64 `json:"image_input_price"`
ImageOutputPrice *float64 `json:"image_output_price"`
PerRequestPrice *float64 `json:"per_request_price"`
Intervals []pricingIntervalResponse `json:"intervals"`
TimePricing *channelTimePricingResponse `json:"time_pricing"`
}
type channelTimePricingResponse struct {
Timezone string `json:"timezone"`
WeekdaysOnly bool `json:"weekdays_only"`
Periods []channelTimePricingPeriodResponse `json:"periods"`
}
type channelTimePricingPeriodResponse struct {
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Multiplier float64 `json:"multiplier"`
}
type pricingIntervalResponse struct {
ID int64 `json:"id"`
MinTokens int `json:"min_tokens"`
MaxTokens *int `json:"max_tokens"`
TierLabel string `json:"tier_label,omitempty"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
PerRequestPrice *float64 `json:"per_request_price"`
SortOrder int `json:"sort_order"`
ID int64 `json:"id"`
MinTokens int `json:"min_tokens"`
MaxTokens *int `json:"max_tokens"`
TierLabel string `json:"tier_label,omitempty"`
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
InputMultiplier *float64 `json:"input_multiplier"`
OutputMultiplier *float64 `json:"output_multiplier"`
CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
PerRequestPrice *float64 `json:"per_request_price"`
SortOrder int `json:"sort_order"`
}
type accountStatsPricingRuleResponse struct {
@@ -216,37 +260,66 @@ func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingRespon
intervals = append(intervals, intervalToResponse(iv))
}
return channelModelPricingResponse{
ID: p.ID,
Platform: platform,
Models: models,
BillingMode: billingMode,
InputPrice: p.InputPrice,
OutputPrice: p.OutputPrice,
CacheWritePrice: p.CacheWritePrice,
CacheReadPrice: p.CacheReadPrice,
ImageInputPrice: p.ImageInputPrice,
ImageOutputPrice: p.ImageOutputPrice,
PerRequestPrice: p.PerRequestPrice,
Intervals: intervals,
ID: p.ID,
Platform: platform,
Models: models,
BillingMode: billingMode,
InputPrice: p.InputPrice,
OutputPrice: p.OutputPrice,
CacheWritePrice: p.CacheWritePrice,
CacheWrite1hPrice: p.CacheWrite1hPrice,
CacheReadPrice: p.CacheReadPrice,
FastMultiplier: p.FastMultiplier,
FlexMultiplier: p.FlexMultiplier,
MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
ImageInputPrice: p.ImageInputPrice,
ImageOutputPrice: p.ImageOutputPrice,
PerRequestPrice: p.PerRequestPrice,
Intervals: intervals,
TimePricing: timePricingToResponse(p.TimePricing),
}
}
func timePricingToResponse(value *service.ChannelTimePricing) *channelTimePricingResponse {
if value == nil {
return nil
}
periods := make([]channelTimePricingPeriodResponse, 0, len(value.Periods))
for _, period := range value.Periods {
periods = append(periods, channelTimePricingPeriodResponse{
StartTime: period.StartTime,
EndTime: period.EndTime,
Multiplier: period.Multiplier,
})
}
return &channelTimePricingResponse{
Timezone: value.Timezone,
WeekdaysOnly: value.WeekdaysOnly,
Periods: periods,
}
}
func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse {
return pricingIntervalResponse{
ID: iv.ID,
MinTokens: iv.MinTokens,
MaxTokens: iv.MaxTokens,
TierLabel: iv.TierLabel,
InputPrice: iv.InputPrice,
OutputPrice: iv.OutputPrice,
CacheWritePrice: iv.CacheWritePrice,
CacheReadPrice: iv.CacheReadPrice,
PerRequestPrice: iv.PerRequestPrice,
SortOrder: iv.SortOrder,
ID: iv.ID,
MinTokens: iv.MinTokens,
MaxTokens: iv.MaxTokens,
TierLabel: iv.TierLabel,
InputPrice: iv.InputPrice,
OutputPrice: iv.OutputPrice,
CacheWritePrice: iv.CacheWritePrice,
CacheWrite1hPrice: iv.CacheWrite1hPrice,
CacheReadPrice: iv.CacheReadPrice,
InputMultiplier: iv.InputMultiplier,
OutputMultiplier: iv.OutputMultiplier,
CacheWriteMultiplier: iv.CacheWriteMultiplier,
CacheReadMultiplier: iv.CacheReadMultiplier,
PerRequestPrice: iv.PerRequestPrice,
SortOrder: iv.SortOrder,
}
}
func pricingRequestToService(reqs []channelModelPricingRequest) []service.ChannelModelPricing {
func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMultipliers bool) []service.ChannelModelPricing {
result := make([]service.ChannelModelPricing, 0, len(reqs))
for _, r := range reqs {
billingMode := service.BillingMode(r.BillingMode)
@@ -256,41 +329,83 @@ func pricingRequestToService(reqs []channelModelPricingRequest) []service.Channe
platform := r.Platform
intervals := make([]service.PricingInterval, 0, len(r.Intervals))
for _, iv := range r.Intervals {
var inputMultiplier, outputMultiplier, cacheWriteMultiplier, cacheReadMultiplier *float64
if allowChannelMultipliers {
inputMultiplier = iv.InputMultiplier
outputMultiplier = iv.OutputMultiplier
cacheWriteMultiplier = iv.CacheWriteMultiplier
cacheReadMultiplier = iv.CacheReadMultiplier
}
intervals = append(intervals, service.PricingInterval{
MinTokens: iv.MinTokens,
MaxTokens: iv.MaxTokens,
TierLabel: iv.TierLabel,
InputPrice: iv.InputPrice,
OutputPrice: iv.OutputPrice,
CacheWritePrice: iv.CacheWritePrice,
CacheReadPrice: iv.CacheReadPrice,
PerRequestPrice: iv.PerRequestPrice,
SortOrder: iv.SortOrder,
MinTokens: iv.MinTokens,
MaxTokens: iv.MaxTokens,
TierLabel: iv.TierLabel,
InputPrice: iv.InputPrice,
OutputPrice: iv.OutputPrice,
CacheWritePrice: iv.CacheWritePrice,
CacheWrite1hPrice: iv.CacheWrite1hPrice,
CacheReadPrice: iv.CacheReadPrice,
InputMultiplier: inputMultiplier,
OutputMultiplier: outputMultiplier,
CacheWriteMultiplier: cacheWriteMultiplier,
CacheReadMultiplier: cacheReadMultiplier,
PerRequestPrice: iv.PerRequestPrice,
SortOrder: iv.SortOrder,
})
}
var fastMultiplier, flexMultiplier, maxReasoningEffortMultiplier *float64
if allowChannelMultipliers {
fastMultiplier = r.FastMultiplier
flexMultiplier = r.FlexMultiplier
maxReasoningEffortMultiplier = r.MaxReasoningEffortMultiplier
}
result = append(result, service.ChannelModelPricing{
Platform: platform,
Models: r.Models,
BillingMode: billingMode,
InputPrice: r.InputPrice,
OutputPrice: r.OutputPrice,
CacheWritePrice: r.CacheWritePrice,
CacheReadPrice: r.CacheReadPrice,
ImageInputPrice: r.ImageInputPrice,
ImageOutputPrice: r.ImageOutputPrice,
PerRequestPrice: r.PerRequestPrice,
Intervals: intervals,
Platform: platform,
Models: r.Models,
BillingMode: billingMode,
InputPrice: r.InputPrice,
OutputPrice: r.OutputPrice,
CacheWritePrice: r.CacheWritePrice,
CacheWrite1hPrice: r.CacheWrite1hPrice,
CacheReadPrice: r.CacheReadPrice,
FastMultiplier: fastMultiplier,
FlexMultiplier: flexMultiplier,
MaxReasoningEffortMultiplier: maxReasoningEffortMultiplier,
ImageInputPrice: r.ImageInputPrice,
ImageOutputPrice: r.ImageOutputPrice,
PerRequestPrice: r.PerRequestPrice,
Intervals: intervals,
TimePricing: timePricingRequestToService(r.TimePricing),
})
}
return result
}
func timePricingRequestToService(value *channelTimePricingRequest) *service.ChannelTimePricing {
if value == nil {
return nil
}
periods := make([]service.ChannelTimePricingPeriod, 0, len(value.Periods))
for _, period := range value.Periods {
periods = append(periods, service.ChannelTimePricingPeriod{
StartTime: period.StartTime,
EndTime: period.EndTime,
Multiplier: period.Multiplier,
})
}
return &service.ChannelTimePricing{
Timezone: value.Timezone,
WeekdaysOnly: value.WeekdaysOnly,
Periods: periods,
}
}
func accountStatsPricingRuleRequestToService(r accountStatsPricingRuleRequest) service.AccountStatsPricingRule {
return service.AccountStatsPricingRule{
Name: r.Name,
GroupIDs: r.GroupIDs,
AccountIDs: r.AccountIDs,
Pricing: pricingRequestToService(r.Pricing),
Pricing: pricingRequestToService(r.Pricing, false),
}
}
@@ -351,7 +466,7 @@ func (h *ChannelHandler) Create(c *gin.Context) {
return
}
pricing := pricingRequestToService(req.ModelPricing)
pricing := pricingRequestToService(req.ModelPricing, true)
// Main model_pricing requires a platform; default to anthropic for backward compatibility.
for i := range pricing {
if pricing[i].Platform == "" {
@@ -425,7 +540,7 @@ func (h *ChannelHandler) Update(c *gin.Context) {
ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
}
if req.ModelPricing != nil {
pricing := pricingRequestToService(*req.ModelPricing)
pricing := pricingRequestToService(*req.ModelPricing, true)
for i := range pricing {
if pricing[i].Platform == "" {
pricing[i].Platform = service.PlatformAnthropic
@@ -495,15 +610,25 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
response.Success(c, gin.H{"found": false})
return
}
cacheWritePrice := pricing.CacheCreationPricePerToken
var cacheWrite1hPrice *float64
if pricing.SupportsCacheBreakdown {
if pricing.CacheCreation5mPrice > 0 {
cacheWritePrice = pricing.CacheCreation5mPrice
}
cacheWrite1hPrice = &pricing.CacheCreation1hPrice
}
response.Success(c, gin.H{
"found": true,
"input_price": pricing.InputPricePerToken,
"output_price": pricing.OutputPricePerToken,
"cache_write_price": pricing.CacheCreationPricePerToken,
"cache_read_price": pricing.CacheReadPricePerToken,
"image_input_price": pricing.ImageInputPricePerToken,
"image_output_price": pricing.ImageOutputPricePerToken,
"found": true,
"input_price": pricing.InputPricePerToken,
"output_price": pricing.OutputPricePerToken,
"cache_write_price": cacheWritePrice,
"cache_write_1h_price": cacheWrite1hPrice,
"cache_read_price": pricing.CacheReadPricePerToken,
"max_reasoning_effort_multiplier": pricing.MaxReasoningEffortMultiplier,
"image_input_price": pricing.ImageInputPricePerToken,
"image_output_price": pricing.ImageOutputPricePerToken,
})
}
@@ -512,9 +637,13 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
var platformToLiteLLMProvider = map[string]string{
service.PlatformAnthropic: "anthropic",
service.PlatformOpenAI: "openai",
service.PlatformGemini: "google",
service.PlatformGemini: "gemini",
service.PlatformAntigravity: "anthropic",
service.PlatformGrok: "xai",
service.PlatformKimi: "moonshot",
service.PlatformZhipu: "zhipu",
service.PlatformDeepseek: "deepseek",
service.PlatformMiniMax: "minimax",
}
// SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
@@ -305,7 +305,7 @@ func TestPricingRequestToService_Defaults(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := pricingRequestToService([]channelModelPricingRequest{tt.req})
result := pricingRequestToService([]channelModelPricingRequest{tt.req}, true)
require.Len(t, result, 1)
switch tt.wantField {
case "BillingMode":
@@ -320,19 +320,20 @@ func TestPricingRequestToService_Defaults(t *testing.T) {
func TestPricingRequestToService_WithAllFields(t *testing.T) {
reqs := []channelModelPricingRequest{
{
Platform: "openai",
Models: []string{"gpt-4", "gpt-4o"},
BillingMode: "per_request",
InputPrice: float64Ptr(0.01),
OutputPrice: float64Ptr(0.03),
CacheWritePrice: float64Ptr(0.005),
CacheReadPrice: float64Ptr(0.002),
ImageOutputPrice: float64Ptr(0.04),
PerRequestPrice: float64Ptr(0.5),
Platform: "openai",
Models: []string{"gpt-4", "gpt-4o"},
BillingMode: "per_request",
InputPrice: float64Ptr(0.01),
OutputPrice: float64Ptr(0.03),
CacheWritePrice: float64Ptr(0.005),
CacheWrite1hPrice: float64Ptr(0.008),
CacheReadPrice: float64Ptr(0.002),
ImageOutputPrice: float64Ptr(0.04),
PerRequestPrice: float64Ptr(0.5),
},
}
result := pricingRequestToService(reqs)
result := pricingRequestToService(reqs, true)
require.Len(t, result, 1)
r := result[0]
require.Equal(t, "openai", r.Platform)
@@ -341,6 +342,7 @@ func TestPricingRequestToService_WithAllFields(t *testing.T) {
require.Equal(t, float64Ptr(0.01), r.InputPrice)
require.Equal(t, float64Ptr(0.03), r.OutputPrice)
require.Equal(t, float64Ptr(0.005), r.CacheWritePrice)
require.Equal(t, float64Ptr(0.008), r.CacheWrite1hPrice)
require.Equal(t, float64Ptr(0.002), r.CacheReadPrice)
require.Equal(t, float64Ptr(0.04), r.ImageOutputPrice)
require.Equal(t, float64Ptr(0.5), r.PerRequestPrice)
@@ -353,15 +355,16 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
BillingMode: "per_request",
Intervals: []pricingIntervalRequest{
{
MinTokens: 0,
MaxTokens: intPtr(2000),
TierLabel: "small",
InputPrice: float64Ptr(0.01),
OutputPrice: float64Ptr(0.02),
CacheWritePrice: float64Ptr(0.003),
CacheReadPrice: float64Ptr(0.001),
PerRequestPrice: float64Ptr(0.1),
SortOrder: 1,
MinTokens: 0,
MaxTokens: intPtr(2000),
TierLabel: "small",
InputPrice: float64Ptr(0.01),
OutputPrice: float64Ptr(0.02),
CacheWritePrice: float64Ptr(0.003),
CacheWrite1hPrice: float64Ptr(0.006),
CacheReadPrice: float64Ptr(0.001),
PerRequestPrice: float64Ptr(0.1),
SortOrder: 1,
},
{
MinTokens: 2000,
@@ -373,7 +376,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
},
}
result := pricingRequestToService(reqs)
result := pricingRequestToService(reqs, true)
require.Len(t, result, 1)
require.Len(t, result[0].Intervals, 2)
@@ -384,6 +387,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
require.Equal(t, float64Ptr(0.01), iv0.InputPrice)
require.Equal(t, float64Ptr(0.02), iv0.OutputPrice)
require.Equal(t, float64Ptr(0.003), iv0.CacheWritePrice)
require.Equal(t, float64Ptr(0.006), iv0.CacheWrite1hPrice)
require.Equal(t, float64Ptr(0.001), iv0.CacheReadPrice)
require.Equal(t, float64Ptr(0.1), iv0.PerRequestPrice)
require.Equal(t, 1, iv0.SortOrder)
@@ -396,7 +400,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
}
func TestPricingRequestToService_EmptySlice(t *testing.T) {
result := pricingRequestToService([]channelModelPricingRequest{})
result := pricingRequestToService([]channelModelPricingRequest{}, true)
require.NotNil(t, result)
require.Empty(t, result)
}
@@ -410,7 +414,7 @@ func TestPricingRequestToService_NilPriceFields(t *testing.T) {
},
}
result := pricingRequestToService(reqs)
result := pricingRequestToService(reqs, true)
require.Len(t, result, 1)
r := result[0]
require.Nil(t, r.InputPrice)
@@ -421,6 +425,92 @@ func TestPricingRequestToService_NilPriceFields(t *testing.T) {
require.Nil(t, r.PerRequestPrice)
}
func TestPricingRequestToService_TimePricing(t *testing.T) {
req := channelModelPricingRequest{
Models: []string{"gpt-5"},
BillingMode: "token",
TimePricing: &channelTimePricingRequest{
Timezone: "Asia/Shanghai",
WeekdaysOnly: true,
Periods: []channelTimePricingPeriodRequest{{
StartTime: "09:00", EndTime: "12:00", Multiplier: 2,
}},
},
}
got := pricingRequestToService([]channelModelPricingRequest{req}, true)
require.Equal(t, "Asia/Shanghai", got[0].TimePricing.Timezone)
require.True(t, got[0].TimePricing.WeekdaysOnly)
require.Equal(t, 2.0, got[0].TimePricing.Periods[0].Multiplier)
}
func TestPricingRequestToService_TimePricingNil(t *testing.T) {
got := pricingRequestToService([]channelModelPricingRequest{{Models: []string{"gpt-5"}}}, true)
require.Nil(t, got[0].TimePricing)
}
// 账号成本统计规则不支持倍率:allowChannelMultipliers=false 时必须丢弃,
// 避免渠道倍率意外污染账号成本口径。
func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
req := channelModelPricingRequest{
Models: []string{"gpt-5"},
BillingMode: "token",
FastMultiplier: float64Ptr(2.5),
FlexMultiplier: float64Ptr(0.5),
MaxReasoningEffortMultiplier: float64Ptr(3),
Intervals: []pricingIntervalRequest{{
MinTokens: 272000,
InputMultiplier: float64Ptr(2),
OutputMultiplier: float64Ptr(1.5),
CacheWriteMultiplier: float64Ptr(2),
CacheReadMultiplier: float64Ptr(2),
}},
}
allowed := pricingRequestToService([]channelModelPricingRequest{req}, true)
require.Equal(t, float64Ptr(2.5), allowed[0].FastMultiplier)
require.Equal(t, float64Ptr(0.5), allowed[0].FlexMultiplier)
require.Equal(t, float64Ptr(3), allowed[0].MaxReasoningEffortMultiplier)
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].InputMultiplier)
require.Equal(t, float64Ptr(1.5), allowed[0].Intervals[0].OutputMultiplier)
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheWriteMultiplier)
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheReadMultiplier)
dropped := pricingRequestToService([]channelModelPricingRequest{req}, false)
require.Nil(t, dropped[0].FastMultiplier)
require.Nil(t, dropped[0].FlexMultiplier)
require.Nil(t, dropped[0].MaxReasoningEffortMultiplier)
require.Nil(t, dropped[0].Intervals[0].InputMultiplier)
require.Nil(t, dropped[0].Intervals[0].OutputMultiplier)
require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier)
require.Nil(t, dropped[0].Intervals[0].CacheReadMultiplier)
// 非倍率字段不受开关影响
require.Equal(t, 272000, dropped[0].Intervals[0].MinTokens)
}
func TestPricingToResponse_TimePricing(t *testing.T) {
got := pricingToResponse(&service.ChannelModelPricing{
BillingMode: service.BillingModeToken,
TimePricing: &service.ChannelTimePricing{
Timezone: "Asia/Shanghai",
WeekdaysOnly: true,
Periods: []service.ChannelTimePricingPeriod{{
StartTime: "14:00", EndTime: "18:00", Multiplier: 1.25,
}},
},
})
require.NotNil(t, got.TimePricing)
require.Equal(t, "Asia/Shanghai", got.TimePricing.Timezone)
require.True(t, got.TimePricing.WeekdaysOnly)
require.Equal(t, 1.25, got.TimePricing.Periods[0].Multiplier)
}
func TestPricingToResponse_TimePricingNil(t *testing.T) {
got := pricingToResponse(&service.ChannelModelPricing{})
require.Nil(t, got.TimePricing)
}
// ---------------------------------------------------------------------------
// 3. SyncPricingModels handler
// ---------------------------------------------------------------------------
@@ -459,7 +549,7 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
svc := service.NewPricingService(nil, nil)
router := setupSyncPricingModelsRouter(svc)
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity"} {
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek", "minimax"} {
req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform="+platform, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
@@ -475,3 +565,53 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
require.NotNil(t, body.Data.Models, "models must not be null for platform=%s", platform)
}
}
func setupModelDefaultPricingRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
h := &ChannelHandler{billingService: service.NewBillingService(nil, nil)}
router.GET("/channels/model-pricing", h.GetModelDefaultPricing)
return router
}
func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) {
router := setupModelDefaultPricingRouter()
req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-fable-5-1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body struct {
Data struct {
Found bool `json:"found"`
CacheWritePrice float64 `json:"cache_write_price"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
require.True(t, body.Data.Found)
require.InDelta(t, 12.5e-6, body.Data.CacheWritePrice, 1e-12)
require.NotNil(t, body.Data.CacheWrite1hPrice)
require.InDelta(t, 20e-6, *body.Data.CacheWrite1hPrice, 1e-12)
require.NotNil(t, body.Data.MaxReasoningEffortMultiplier)
require.Equal(t, 3.0, *body.Data.MaxReasoningEffortMultiplier)
}
func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) {
router := setupModelDefaultPricingRouter()
req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-sonnet-4", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body struct {
Data struct {
Found bool `json:"found"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
require.True(t, body.Data.Found)
require.Nil(t, body.Data.CacheWrite1hPrice)
}
@@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/domain"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
@@ -39,10 +40,10 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
type channelMonitorCreateRequest struct {
Name string `json:"name" binding:"required,max=100"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax"`
APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
Endpoint string `json:"endpoint" binding:"required,max=500"`
APIKey string `json:"api_key" binding:"required,max=2000"`
Endpoint string `json:"endpoint" binding:"omitempty,max=500"`
APIKey string `json:"api_key" binding:"omitempty,max=2000"`
PrimaryModel string `json:"primary_model" binding:"max=200"`
ExtraModels []string `json:"extra_models"`
GroupName string `json:"group_name" binding:"max=100"`
@@ -53,11 +54,17 @@ type channelMonitorCreateRequest struct {
ExtraHeaders map[string]string `json:"extra_headers"`
BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
BodyOverride map[string]any `json:"body_override"`
// CheckMode: probe(默认)/ quota / quota_probe。quota 模式 endpoint/api_key
// 可空(条件必填校验在 service 层按模式分支)。
CheckMode string `json:"check_mode" binding:"omitempty,oneof=probe quota quota_probe"`
// AccountID: 配额模式关联的账号 ID。
AccountID *int64 `json:"account_id"`
}
type channelMonitorUpdateRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax"`
APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
@@ -72,6 +79,10 @@ type channelMonitorUpdateRequest struct {
ExtraHeaders *map[string]string `json:"extra_headers"`
BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
BodyOverride *map[string]any `json:"body_override"`
// CheckMode/AccountID:nil = 不更新;AccountID 指向 0 = 清空关联。
CheckMode *string `json:"check_mode" binding:"omitempty,oneof=probe quota quota_probe"`
AccountID *int64 `json:"account_id"`
}
type channelMonitorResponse struct {
@@ -101,25 +112,33 @@ type channelMonitorResponse struct {
ExtraHeaders map[string]string `json:"extra_headers"`
BodyOverrideMode string `json:"body_override_mode"`
BodyOverride map[string]any `json:"body_override"`
// 配额模式:check_mode + 关联账号 + 主模型最近配额快照
// (LatestQuota 由 List handler 批量聚合后填充;管理端不受 channel_monitor_show_quota 影响)。
CheckMode string `json:"check_mode"`
AccountID *int64 `json:"account_id"`
LatestQuota *domain.MonitorQuotaSnapshot `json:"latest_quota,omitempty"`
}
type channelMonitorCheckResultResponse struct {
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
}
type channelMonitorHistoryItemResponse struct {
ID int64 `json:"id"`
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
ID int64 `json:"id"`
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
}
// maskAPIKey 对 API Key 明文做脱敏:前 4 字符 + "***",长度 ≤ 4 时只显示 "***"。
@@ -163,7 +182,10 @@ func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse
ExtraHeaders: headers,
BodyOverrideMode: m.BodyOverrideMode,
BodyOverride: m.BodyOverride,
// PrimaryStatus / PrimaryLatencyMs / Availability7d 由 List handler 在批量聚合后填充。
CheckMode: m.CheckMode,
AccountID: m.AccountID,
// PrimaryStatus / PrimaryLatencyMs / Availability7d / LatestQuota
// 由 List handler 在批量聚合后填充。
}
if m.LastCheckedAt != nil {
s := m.LastCheckedAt.UTC().Format(time.RFC3339)
@@ -180,6 +202,7 @@ func checkResultToResponse(r *service.CheckResult) channelMonitorCheckResultResp
PingLatencyMs: r.PingLatencyMs,
Message: r.Message,
CheckedAt: r.CheckedAt.UTC().Format(time.RFC3339),
Quota: r.Quota,
}
}
@@ -192,6 +215,7 @@ func historyEntryToResponse(e *service.ChannelMonitorHistoryEntry) channelMonito
PingLatencyMs: e.PingLatencyMs,
Message: e.Message,
CheckedAt: e.CheckedAt.UTC().Format(time.RFC3339),
Quota: e.Quota,
}
}
@@ -270,6 +294,7 @@ func buildListItemResponse(m *service.ChannelMonitor, summary service.MonitorSta
resp.PrimaryStatus = summary.PrimaryStatus
resp.PrimaryLatencyMs = summary.PrimaryLatencyMs
resp.Availability7d = summary.Availability7d
resp.LatestQuota = summary.LatestQuota
resp.ExtraModelsStatus = make([]dto.ChannelMonitorExtraModelStatus, 0, len(summary.ExtraModels))
for _, e := range summary.ExtraModels {
resp.ExtraModelsStatus = append(resp.ExtraModelsStatus, dto.ChannelMonitorExtraModelStatus{
@@ -327,6 +352,8 @@ func (h *ChannelMonitorHandler) Create(c *gin.Context) {
ExtraHeaders: req.ExtraHeaders,
BodyOverrideMode: req.BodyOverrideMode,
BodyOverride: req.BodyOverride,
CheckMode: req.CheckMode,
AccountID: req.AccountID,
})
if err != nil {
response.ErrorFrom(c, err)
@@ -421,6 +448,8 @@ func (h *ChannelMonitorHandler) Update(c *gin.Context) {
ExtraHeaders: req.ExtraHeaders,
BodyOverrideMode: req.BodyOverrideMode,
BodyOverride: req.BodyOverride,
CheckMode: req.CheckMode,
AccountID: req.AccountID,
})
if err != nil {
response.ErrorFrom(c, err)
@@ -0,0 +1,69 @@
package admin
import (
"strconv"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// CNProviderHandler 暴露国产供应商(kimi/zhipu/deepseek)的额度与余额查询端点。
//
// - GET /admin/cn-providers/accounts/:id/quota Coding Plan 滚动窗口用量(kimi/zhipu)
// - GET /admin/cn-providers/accounts/:id/balance payg 账号余额(kimi/deepseek)
//
// 智谱(zhipu)无余额端点,故同一账号仅 quota 或 balance 其一可用:服务端按账号
// platform + account_mode 校验并返回明确错误(见 CNProvider*Service 的 load*Account)。
type CNProviderHandler struct {
quotaService *service.CNProviderQuotaService
balanceService *service.CNProviderBalanceService
}
func NewCNProviderHandler(
quotaService *service.CNProviderQuotaService,
balanceService *service.CNProviderBalanceService,
) *CNProviderHandler {
return &CNProviderHandler{
quotaService: quotaService,
balanceService: balanceService,
}
}
// QueryQuota 查询 Coding Plan 滚动窗口用量(5h + weekly)。
func (h *CNProviderHandler) QueryQuota(c *gin.Context) {
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid account ID")
return
}
if h == nil || h.quotaService == nil {
response.BadRequest(c, "cn provider quota service is not enabled")
return
}
result, err := h.quotaService.QueryUsage(c.Request.Context(), accountID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, result)
}
// QueryBalance 查询 payg 账号余额。
func (h *CNProviderHandler) QueryBalance(c *gin.Context) {
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid account ID")
return
}
if h == nil || h.balanceService == nil {
response.BadRequest(c, "cn provider balance service is not enabled")
return
}
result, err := h.balanceService.QueryBalance(c.Request.Context(), accountID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, result)
}
@@ -262,13 +262,18 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
return
}
}
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
if err != nil {
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
return
}
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
if err != nil {
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
return
}
trend, hit, err := h.getUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, billingType, upstreamModelMismatch)
trend, hit, err := h.getUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
if err != nil {
response.Error(c, 500, "Failed to get usage trend")
return
@@ -349,13 +354,18 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
return
}
}
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
if err != nil {
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
return
}
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
if err != nil {
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
return
}
stats, hit, err := h.getModelStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, modelSource, requestType, stream, billingType, upstreamModelMismatch)
stats, hit, err := h.getModelStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, modelSource, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
if err != nil {
response.Error(c, 500, "Failed to get model statistics")
return
@@ -426,13 +436,18 @@ func (h *DashboardHandler) GetGroupStats(c *gin.Context) {
return
}
}
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
if err != nil {
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
return
}
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
if err != nil {
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
return
}
stats, hit, err := h.getGroupStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, billingType, upstreamModelMismatch)
stats, hit, err := h.getGroupStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
if err != nil {
response.Error(c, 500, "Failed to get group statistics")
return
@@ -701,6 +716,14 @@ func (h *DashboardHandler) GetUserBreakdown(c *gin.Context) {
dim.Stream = &s
}
}
if v := strings.TrimSpace(c.Query("native_compaction_v2")); v != "" {
value, err := strconv.ParseBool(v)
if err != nil {
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
return
}
dim.NativeCompactionV2 = &value
}
if v := c.Query("billing_type"); v != "" {
if bt, err := strconv.ParseInt(v, 10, 8); err == nil {
btVal := int8(bt)
@@ -15,16 +15,19 @@ import (
type dashboardUsageRepoCapture struct {
service.UsageLogRepository
trendRequestType *int16
trendStream *bool
modelRequestType *int16
modelStream *bool
trendMismatch *bool
modelMismatch *bool
groupMismatch *bool
rankingLimit int
ranking []usagestats.UserSpendingRankingItem
rankingTotal float64
trendRequestType *int16
trendStream *bool
trendNativeCompaction *bool
modelRequestType *int16
modelStream *bool
modelNativeCompaction *bool
groupNativeCompaction *bool
trendMismatch *bool
modelMismatch *bool
groupMismatch *bool
rankingLimit int
ranking []usagestats.UserSpendingRankingItem
rankingTotal float64
}
func (s *dashboardUsageRepoCapture) GetUsageTrendWithUsageFilters(
@@ -35,6 +38,7 @@ func (s *dashboardUsageRepoCapture) GetUsageTrendWithUsageFilters(
) ([]usagestats.TrendDataPoint, error) {
s.trendRequestType = filters.RequestType
s.trendStream = filters.Stream
s.trendNativeCompaction = filters.NativeCompactionV2
s.trendMismatch = filters.UpstreamModelMismatch
return []usagestats.TrendDataPoint{}, nil
}
@@ -62,6 +66,7 @@ func (s *dashboardUsageRepoCapture) GetModelStatsWithUsageFiltersBySource(
) ([]usagestats.ModelStat, error) {
s.modelRequestType = filters.RequestType
s.modelStream = filters.Stream
s.modelNativeCompaction = filters.NativeCompactionV2
s.modelMismatch = filters.UpstreamModelMismatch
return []usagestats.ModelStat{}, nil
}
@@ -71,6 +76,7 @@ func (s *dashboardUsageRepoCapture) GetGroupStatsWithUsageFilters(
startTime, endTime time.Time,
filters usagestats.UsageLogFilters,
) ([]usagestats.GroupStat, error) {
s.groupNativeCompaction = filters.NativeCompactionV2
s.groupMismatch = filters.UpstreamModelMismatch
return []usagestats.GroupStat{}, nil
}
@@ -208,6 +214,48 @@ func TestDashboardModelStatsValidModelSource(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
}
func TestDashboardNativeCompactionFilterPropagatesAlongsideTransport(t *testing.T) {
resetDashboardReadCachesForTest()
repo := &dashboardUsageRepoCapture{}
router := newDashboardRequestTypeTestRouter(repo)
for _, path := range []string{
"/admin/dashboard/trend?request_type=stream&native_compaction_v2=true",
"/admin/dashboard/models?request_type=stream&native_compaction_v2=true",
"/admin/dashboard/groups?request_type=stream&native_compaction_v2=true",
} {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code, path)
}
require.NotNil(t, repo.trendNativeCompaction)
require.True(t, *repo.trendNativeCompaction)
require.NotNil(t, repo.modelNativeCompaction)
require.True(t, *repo.modelNativeCompaction)
require.NotNil(t, repo.groupNativeCompaction)
require.True(t, *repo.groupNativeCompaction)
require.NotNil(t, repo.trendRequestType)
require.Equal(t, int16(service.RequestTypeStream), *repo.trendRequestType)
}
func TestDashboardNativeCompactionFilterRejectsInvalidBoolean(t *testing.T) {
repo := &dashboardUsageRepoCapture{}
router := newDashboardRequestTypeTestRouter(repo)
for _, path := range []string{
"/admin/dashboard/trend?native_compaction_v2=invalid",
"/admin/dashboard/models?native_compaction_v2=invalid",
"/admin/dashboard/groups?native_compaction_v2=invalid",
} {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusBadRequest, rec.Code, path)
}
}
func TestDashboardModelAuditFilterPropagatesToTrendModelAndGroupQueries(t *testing.T) {
resetDashboardReadCachesForTest()
repo := &dashboardUsageRepoCapture{}
@@ -28,6 +28,7 @@ type dashboardTrendCacheKey struct {
Model string `json:"model"`
RequestType *int16 `json:"request_type"`
Stream *bool `json:"stream"`
NativeCompactionV2 *bool `json:"native_compaction_v2"`
BillingType *int8 `json:"billing_type"`
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
}
@@ -42,6 +43,7 @@ type dashboardModelGroupCacheKey struct {
ModelSource string `json:"model_source,omitempty"`
RequestType *int16 `json:"request_type"`
Stream *bool `json:"stream"`
NativeCompactionV2 *bool `json:"native_compaction_v2"`
BillingType *int8 `json:"billing_type"`
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
}
@@ -85,6 +87,7 @@ func (h *DashboardHandler) getUsageTrendCached(
model string,
requestType *int16,
stream *bool,
nativeCompactionV2 *bool,
billingType *int8,
upstreamModelMismatch *bool,
) ([]usagestats.TrendDataPoint, bool, error) {
@@ -99,13 +102,14 @@ func (h *DashboardHandler) getUsageTrendCached(
Model: model,
RequestType: requestType,
Stream: stream,
NativeCompactionV2: nativeCompactionV2,
BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
})
entry, hit, err := dashboardTrendCache.GetOrLoad(key, func() (any, error) {
return h.dashboardService.GetUsageTrendWithUsageFilters(ctx, startTime, endTime, granularity, usagestats.UsageLogFilters{
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
Model: model, RequestType: requestType, Stream: stream, BillingType: billingType,
Model: model, RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
})
})
@@ -123,6 +127,7 @@ func (h *DashboardHandler) getModelStatsCached(
modelSource string,
requestType *int16,
stream *bool,
nativeCompactionV2 *bool,
billingType *int8,
upstreamModelMismatch *bool,
) ([]usagestats.ModelStat, bool, error) {
@@ -136,13 +141,14 @@ func (h *DashboardHandler) getModelStatsCached(
ModelSource: usagestats.NormalizeModelSource(modelSource),
RequestType: requestType,
Stream: stream,
NativeCompactionV2: nativeCompactionV2,
BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
})
entry, hit, err := dashboardModelStatsCache.GetOrLoad(key, func() (any, error) {
return h.dashboardService.GetModelStatsWithUsageFiltersBySource(ctx, startTime, endTime, usagestats.UsageLogFilters{
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
RequestType: requestType, Stream: stream, BillingType: billingType,
RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
}, modelSource)
})
@@ -159,6 +165,7 @@ func (h *DashboardHandler) getGroupStatsCached(
userID, apiKeyID, accountID, groupID int64,
requestType *int16,
stream *bool,
nativeCompactionV2 *bool,
billingType *int8,
upstreamModelMismatch *bool,
) ([]usagestats.GroupStat, bool, error) {
@@ -171,13 +178,14 @@ func (h *DashboardHandler) getGroupStatsCached(
GroupID: groupID,
RequestType: requestType,
Stream: stream,
NativeCompactionV2: nativeCompactionV2,
BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
})
entry, hit, err := dashboardGroupStatsCache.GetOrLoad(key, func() (any, error) {
return h.dashboardService.GetGroupStatsWithUsageFilters(ctx, startTime, endTime, usagestats.UsageLogFilters{
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
RequestType: requestType, Stream: stream, BillingType: billingType,
RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
UpstreamModelMismatch: upstreamModelMismatch,
})
})
@@ -44,6 +44,7 @@ type dashboardSnapshotV2Filters struct {
Model string
RequestType *int16
Stream *bool
NativeCompactionV2 *bool
BillingType *int8
UpstreamModelMismatch *bool
}
@@ -59,6 +60,7 @@ type dashboardSnapshotV2CacheKey struct {
Model string `json:"model"`
RequestType *int16 `json:"request_type"`
Stream *bool `json:"stream"`
NativeCompactionV2 *bool `json:"native_compaction_v2"`
BillingType *int8 `json:"billing_type"`
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
IncludeStats bool `json:"include_stats"`
@@ -105,6 +107,7 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
Model: filters.Model,
RequestType: filters.RequestType,
Stream: filters.Stream,
NativeCompactionV2: filters.NativeCompactionV2,
BillingType: filters.BillingType,
UpstreamModelMismatch: filters.UpstreamModelMismatch,
IncludeStats: includeStats,
@@ -186,6 +189,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
filters.Model,
filters.RequestType,
filters.Stream,
filters.NativeCompactionV2,
filters.BillingType,
filters.UpstreamModelMismatch,
)
@@ -207,6 +211,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
usagestats.ModelSourceRequested,
filters.RequestType,
filters.Stream,
filters.NativeCompactionV2,
filters.BillingType,
filters.UpstreamModelMismatch,
)
@@ -227,6 +232,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
filters.GroupID,
filters.RequestType,
filters.Stream,
filters.NativeCompactionV2,
filters.BillingType,
filters.UpstreamModelMismatch,
)
@@ -296,6 +302,14 @@ func parseDashboardSnapshotV2Filters(c *gin.Context) (*dashboardSnapshotV2Filter
filters.Stream = &streamVal
}
if nativeCompactionV2Str := strings.TrimSpace(c.Query("native_compaction_v2")); nativeCompactionV2Str != "" {
value, err := strconv.ParseBool(nativeCompactionV2Str)
if err != nil {
return nil, err
}
filters.NativeCompactionV2 = &value
}
if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
v, err := strconv.ParseInt(billingTypeStr, 10, 8)
if err != nil {

Some files were not shown because too many files have changed in this diff Show More