Commit Graph
100 Commits
Author SHA1 Message Date
erio a9e5fc8539 fix(ui): floating point precision in perTokenToMTok conversion 2026-04-04 11:17:49 +08:00
erio 9b213115e7 fix: address audit findings - cache sync, validation, consistency
- clearCreditsExhausted: sync Redis scheduler cache after DB update
- Image billing mode UI: write to per_request_price instead of image_output_price
- OpenAI RecordUsage: use BillingModelSourceRequested constant, add s.cfg nil guard
- Fix i18n key path: admin.channels.perRequestPriceRequired → admin.channels.form.perRequestPriceRequired
2026-04-04 11:17:49 +08:00
erio 5534347328 test: add unit tests for channel platform matching, interval validation, credits check
- TestIsPlatformPricingMatch: 12 cases covering all platform combinations
- TestMatchingPlatforms: 4 cases for platform expansion
- TestGetChannelModelPricing_AntigravityCrossPlatform: antigravity sees anthropic pricing
- TestGetChannelModelPricing_AnthropicCannotSeeAntigravityPricing: no reverse leakage
- TestResolveChannelMapping_AntigravityCrossPlatform: antigravity uses anthropic mapping
- TestFilterValidIntervals: 8 cases for empty interval filtering
- TestHasEnoughCredits: 10 cases for credits balance threshold logic
- Extract hasEnoughCredits() pure function for testability
2026-04-04 11:17:49 +08:00
erio 2355029dc1 fix: validate empty intervals + antigravity platform pricing match
- Backend: reject intervals with all-null price fields on save
- Backend: filterValidIntervals skips empty intervals in pricing resolver
- Frontend: red border + asterisk on empty interval rows
- Backend: antigravity groups now match anthropic/gemini channel pricing
2026-04-04 11:17:49 +08:00
erio 8d25335b01 fix: antigravity groups now match anthropic/gemini channel pricing
Antigravity platform serves both Claude and Gemini models, but channel
pricing/mapping is configured under Anthropic/Gemini tabs. The cache
builder was using strict platform equality, causing antigravity groups
to miss all channel pricing entries, resulting in $0 billing.

Add isPlatformPricingMatch() to treat antigravity as superset of
anthropic+gemini for pricing and mapping cache indexing.
2026-04-04 11:17:48 +08:00
erio c0b5900a37 feat(ui): display three-level model mapping chain in usage logs
- Show channel + account mapping steps using model_mapping_chain field
- Add model_mapping_chain to AdminUsageLog TypeScript type
- Fallback to two-level display when chain is not available
- Fix cost nil guard in Anthropic/Antigravity RecordUsage paths
- Bump version to 0.1.105.31
2026-04-04 11:17:48 +08:00
erio 35a9290528 fix: add cost nil guard to Anthropic/Antigravity RecordUsage paths
- Apply same nil-pointer protection as OpenAI path
- Remove unused accessToken/proxyURL params from checkAccountCredits
2026-04-04 11:17:48 +08:00
erio c9145ad4d8 fix: golangci-lint test assertion and gofmt 2026-04-04 11:17:48 +08:00
erio 3851628a43 fix: resolve golangci-lint issues
- Fix errcheck: defer rows.Close() with nolint
- Fix errcheck: type assertion with ok check in channel cache
- Fix staticcheck ST1005: lowercase error string
- Fix staticcheck SA5011: nil check cost before use in openai gateway
- Fix gofmt: format chatcompletions_to_responses.go
2026-04-04 11:17:24 +08:00
erio d72ac92694 feat: image output token billing, channel-mapped billing source, credits balance precheck
- Parse candidatesTokensDetails from Gemini API to separate image/text output tokens
- Add image_output_tokens and image_output_cost to usage_log (migration 089)
- Support per-image-token pricing via output_cost_per_image_token from model pricing data
- Channel pricing ImageOutputPrice override works in token billing mode
- Auto-fill image_output_price in channel pricing form from model defaults
- Add "channel_mapped" billing model source as new default (migration 088)
- Bills by model name after channel mapping, before account mapping
- Fix channel cache error TTL sign error (115s → 5s)
- Fix Update channel only invalidating new groups, not removed groups
- Fix frontend model_mapping clearing sending undefined instead of {}
- Credits balance precheck via shared AccountUsageService cache before injection
- Skip credits injection for accounts with insufficient balance
- Don't mark credits exhausted for "exhausted your capacity on this model" 429s
2026-04-04 11:15:59 +08:00
erio 2555951be4 feat(channel): 渠道管理全链路集成 — 模型映射、定价、限制、用量统计
- 渠道模型映射:支持精确匹配和通配符映射,按平台隔离
- 渠道模型定价:支持 token/按次/图片三种计费模式,区间分层定价
- 模型限制:渠道可限制仅允许定价列表中的模型
- 计费模型来源:支持 requested/upstream 两种计费模型选择
- 用量统计:usage_logs 新增 channel_id/model_mapping_chain/billing_tier/billing_mode 字段
- Dashboard 支持 model_source 维度(requested/upstream/mapping)查看模型统计
- 全部 gateway handler 统一接入 ResolveChannelMappingAndRestrict
- 修复测试:同步 SoraGenerationRepository 接口、SQL INSERT 参数、scan 字段
2026-04-04 11:13:58 +08:00
erio 669bff78c4 fix(ui): 模型映射改用平台色字体,删除多余的边框色函数 2026-04-04 11:13:57 +08:00
erio c90d1f2527 fix(ui): 模型映射输入框改为平台色字体,保持默认边框 2026-04-04 11:13:57 +08:00
erio 40cebc250f feat(ui): 渠道表单模型标签和映射输入框显示平台对应颜色
- PricingEntryCard 折叠态模型 tag 按平台着色
- ModelTagInput 模型标签按平台着色
- 模型映射输入框边框按平台着色
2026-04-04 11:13:57 +08:00
erio ddd495fb48 feat(ui): 渠道列表状态列改为 Toggle 开关,支持直接启用/禁用 2026-04-04 11:13:57 +08:00
erio 58f2044637 fix(ui): 渠道定价折叠态模型名完整展示,不再截断
去掉 max-w-[120px] truncate 限制,改用 flex-wrap 允许换行,
充分利用空白区域展示完整模型名。
2026-04-04 11:13:57 +08:00
erio dfe3fdc1cc fix(channel): 模型限制以原始请求模型检查定价列表,而非映射后模型
开启 restrict_models 时,应用原始模型名查定价列表;
定价列表未命中即拒绝,不因通配符映射而绕过限制。
2026-04-04 11:13:57 +08:00
erio 705131e172 fix(channel): 前端重复模型校验改为按平台检查
后端 validateNoDuplicateModels 使用 platform:model 复合键,
前端之前跨平台扁平化检查导致不同平台下的同名模型误报重复。
2026-04-04 11:13:57 +08:00
erio 88759407c7 feat(channel): 模型映射源支持通配符匹配
与定价通配符一致,映射源支持 * 后缀通配符(最长前缀优先):
- `*` 匹配所有模型
- `claude-*` 匹配 claude- 开头的模型
- 精确匹配优先于通配符
2026-04-04 11:13:57 +08:00
erio 6c99cc611c fix(channel): 渠道表单校验增强 — 空模型定价报错 + 必填标记
- 保存时校验:定价条目有但模型列表为空时报错并跳转到对应平台 tab,
  不再静默跳过导致数据丢失
- 保存时校验:启用的平台必须至少选择一个分组
- 分组关联标签增加红色 * 必填标记
2026-04-04 11:13:57 +08:00
erio 3457bcbfcd fix(channel): 修复 invalidateCache 存入 typed nil 导致 loadCache panic
invalidateCache 存入 (*channelCache)(nil),类型断言 ok=true 但
指针为 nil,后续 cached.loadedAt 导致 nil pointer dereference。
在 loadCache 双重检查处增加 cached != nil 防御。
2026-04-04 11:13:56 +08:00
erio eb385457b2 fix(channel): 全平台渠道映射覆盖 + 公共函数抽取 + 死代码清理
- 4个缺失handler入口添加渠道映射+限制检查(ChatCompletions/Responses/Gemini)
- 模型限制错误信息优化,区分"模型不可用"和"无账号"
- OpenAI RecordUsage RequestedModel 改用 OriginalModel
- ResolveChannelMappingAndRestrict/ReplaceModelInBody 抽取到 ChannelService 消除跨service重复
- validateNoDuplicateModels 按 platform:model 去重
- 删除 Channel.ResolveMappedModel 死代码和 CalculateCostWithChannel Deprecated方法
- 移除冗余nil检查,抽取 validatePricingBillingMode 公共校验
2026-04-04 11:13:56 +08:00
erio 4ea8b4cb4f refactor(channel): 抽取渠道映射公共函数 + OpenAI映射到body + 空响应修复 + 清理日志
- 抽取 ResolveChannelMappingAndRestrict 统一入口(5处→1个方法)
- 抽取 BuildModelMappingChain 到 ChannelMappingResult 方法(5处→1行调用)
- OpenAI 三入口 Forward 前应用渠道映射到请求体
- OpenAI Responses/Messages 限制检查添加错误响应
- 清理前端 3 处 console.log 调试日志
2026-04-04 11:13:56 +08:00
erio 91bdcf8994 fix(channel): 模型限制用映射后模型检查 + 平台开关保留配置不删除
- OpenAI 网关三处 IsModelRestricted 改用 channelMapping.MappedModel
- 前端平台勾选改为 enabled 开关,取消勾选不清空配置数据
- formToAPI/校验只处理 enabled 的平台
2026-04-04 11:13:56 +08:00
erio 8d03c52e15 feat(channel): 通配符定价匹配 + OpenAI BillingModelSource + 按次价格校验 + 用户端计费模式展示
- 定价查找支持通配符(suffix *),最长前缀优先匹配
- 模型限制(restrict_models)同样支持通配符匹配
- OpenAI 网关接入渠道映射/BillingModelSource/模型限制
- 按次/图片计费模式创建时强制要求价格或层级(前后端)
- 用户使用记录列表增加计费模式 badge 列
2026-04-04 11:13:43 +08:00
erio 0fbc9a44d3 fix(billing): 按次计费回退到默认 PerRequestPrice
ResolvedPricing 新增 DefaultPerRequestPrice,当无层级匹配时使用渠道的默认按次价格
2026-04-04 11:12:47 +08:00
erio 632035aabd feat(billing): 网关计费迁移到 CalculateCostUnified + 模型限制错误统一
- GatewayService/OpenAIGatewayService 注入 ModelPricingResolver
- RecordUsage 从旧路径迁移到 CalculateCostUnified(支持 per_request/image 模式)
- 无渠道时自动回退旧路径,保持原有行为
- 长上下文双倍计费仅在无渠道定价时生效
- CostBreakdown 新增 BillingMode 字段,使用日志记录实际计费模式
- 模型限制错误改为与"无可用账号"相同的 503 响应
2026-04-04 11:12:21 +08:00
erio a51e0047b7 feat(usage): 使用记录增加计费模式字段 — 记录/展示/筛选 token/按次/图片
- DB: usage_logs 表新增 billing_mode VARCHAR(20) 列
- 后端: RecordUsage 写入时根据 image_count 判定计费模式
- 前端: 使用记录表格新增计费模式 badge 列 + 筛选下拉
2026-04-04 11:11:06 +08:00
erio 726730bb0e fix(channel): splice替换model_pricing条目 + 增强调试日志 2026-04-04 11:09:59 +08:00
erio faff1771c4 debug(channel): 添加 formToAPI 调试日志 + 移除 Sora 平台 2026-04-04 11:09:58 +08:00
erio b06cd06ec1 feat(channel): 平台配置改为勾选式,勾选后出现 Tab 但不自动跳转 2026-04-04 11:09:58 +08:00
erio 95751d8009 feat(channel): 对话框 Tab 布局 — 基础设置 + 平台独立 Tab + 固定高度 2026-04-04 11:09:58 +08:00
erio 14e565a004 fix(channel): 分组加载时序修复 — 预加载 + await 确保分组数据就绪 2026-04-04 11:09:58 +08:00
erio ce694701a9 fix(i18n): 渠道管理页面标题国际化 2026-04-04 11:09:57 +08:00
erio 12d03e4030 feat(channel): 模型价格自动填充 + 默认定价 API
- 新增 GET /admin/channels/model-pricing?model=xxx API
- 从 BillingService 查询 LiteLLM/Fallback 默认定价
- 前端添加模型时自动查询并填充价格($/MTok)
- 仅在所有价格字段为空时才自动填充,不覆盖手动配置
2026-04-04 11:09:28 +08:00
erio 0b1ce6be8f feat(channel): 缓存扁平化 + 网关映射集成 + 计费模式统一 + 模型限制
- 缓存按 (groupID, platform, model) 三维 key 扁平化,避免跨平台同名模型冲突
- buildCache 批量查询 group platform,按平台过滤展开定价和映射
- model_mapping 改为嵌套格式 {platform: {src: dst}}
- channel_model_pricing 新增 platform 列
- 前端按平台维度重构:每个平台独立配置分组/映射/定价
- 迁移 086: platform 列 + model_mapping 嵌套格式迁移
2026-04-04 11:09:28 +08:00
erio 28a6adaaa4 fix(channel): 分组紧凑布局+平台颜色名称+计费基准重命名+表单排序调整 2026-04-04 11:09:28 +08:00
erio 36990a0514 fix: revert ent schema change to fix runtime panic 2026-04-04 11:09:27 +08:00
erio ebac0dc628 feat(channel): 缓存扁平化 + 网关映射集成 + 计费模式统一 + 模型限制
- 缓存重构为 O(1) 哈希结构 (pricingByGroupModel, mappingByGroupModel)
- 渠道模型映射接入网关流程 (Forward 前应用, a→b→c 映射链)
- 新增 billing_model_source 配置 (请求模型/最终模型计费)
- usage_logs 新增 channel_id, model_mapping_chain, billing_tier 字段
- 每种计费模式统一支持默认价格 + 区间定价
- 渠道模型限制开关 (restrict_models)
- 分组按平台分类展示 + 彩色图标
- 必填字段红色星号 + 模型映射 UI
- 去除模型通配符支持
2026-04-04 11:09:01 +08:00
erio 29d58f2414 feat(channel): 模型映射 + 分组搜索 + 卡片折叠 + 冲突校验
- 渠道模型映射:新增 model_mapping JSONB 字段,在账号映射之前执行
- 分组选择:添加搜索过滤 + 平台图标
- 定价卡片:支持折叠/展开,已有数据默认折叠
- 模型冲突校验:前后端均禁止同一渠道内重复模型
- 迁移 083: channels 表添加 model_mapping 列
2026-04-04 11:06:36 +08:00
erio dca0054e93 feat(channel): 模型标签输入 + $/MTok 价格单位 + 左开右闭区间 + i18n
- 模型输入改为标签列表(输入回车添加,支持粘贴批量导入)
- 价格显示单位改为 $/MTok(每百万 token),提交时自动转换
- Token 模式增加图片输出价格字段(适配 Gemini 图片模型按 token 计费)
- 区间边界改为左开右闭 (min, max],右边界包含
- 默认价格作为未命中区间时的回退价格
- 添加完整中英文 i18n 翻译
2026-04-04 11:01:22 +08:00
erio 983fe58959 fix: CI lint/test fixes — gofmt, errcheck, handler test args 2026-04-04 11:01:22 +08:00
erio 91c9b8d062 feat(channel): 渠道管理系统 — 多模式定价 + 统一计费解析
Cherry-picked from release/custom-0.1.106: a9117600
2026-04-04 11:00:55 +08:00
erio bf24de88ed style: fix gofmt struct tag alignment 2026-04-03 02:00:21 +08:00
erio ff6d4ab39a chore: add lodash/lodash-es audit exception for GHSA-r5fr-rjxr-66jc 2026-04-03 01:53:17 +08:00
erio 66fde7a2e6 feat(redeem): support negative values for refund/deduction
Allow redeem codes with negative values to enable refund scenarios:
- Balance: negative value deducts balance (clamped to 0, never negative)
- Concurrency: negative value reduces concurrency (clamped to 0)
- Subscription: negative validity_days reduces remaining days; if
  remaining days <= 0, the subscription is canceled (set to expired)

All deductions generate standard redeem code records for audit trail.
2026-04-03 01:50:26 +08:00
erio 9abdaed20c style: gofmt antigravity_internal500_penalty.go 2026-03-27 20:18:07 +08:00
erio eb94342f78 chore: adjust internal500 penalty durations to 30m / 2h 2026-03-27 20:11:24 +08:00
erio d563eb2336 test: add unit tests for INTERNAL 500 progressive penalty
Cover isAntigravityInternalServerError body matching,
applyInternal500Penalty tier escalation, handleInternal500RetryExhausted
nil-safety and error handling, and resetInternal500Counter paths.
2026-03-27 20:11:24 +08:00
erio 3ee6f085db refactor: extract internal500 penalty logic to dedicated file
Move constants, detection, and penalty functions from
antigravity_gateway_service.go to antigravity_internal500_penalty.go.
Fix gofmt alignment and replace hardcoded duration strings with
constant references.
2026-03-27 20:11:24 +08:00
erio 7cca69a136 fix: move internal500 counter reset to cover all success paths
Move the reset logic after urlFallbackLoop so it covers both direct
success and smart retry (429/503) success paths.
2026-03-27 20:11:24 +08:00
erio 093a5a260e feat(antigravity): progressive penalty for consecutive INTERNAL 500 errors
When an antigravity account returns 500 "Internal error encountered."
on all 3 retry attempts, increment a Redis counter and apply escalating
penalties:
- 1st round: temp unschedulable 10 minutes
- 2nd round: temp unschedulable 10 hours
- 3rd round: permanently mark as error

Counter resets on any successful response (< 400).
2026-03-27 20:11:24 +08:00
erio 21b6f2d593 fix(antigravity): correctly mark credits exhausted on "Resource has been exhausted" 429
shouldMarkCreditsExhausted was blocked by isURLLevelRateLimit check when
credit overages retry returned "Resource has been exhausted (e.g. check quota).",
causing credits to never be marked as exhausted. This led to an infinite loop
where each request injected credits, bypassed model rate limits, and failed again.

- Remove isURLLevelRateLimit guard from shouldMarkCreditsExhausted (only called
  for credit retry responses — if credits retry fails, mark exhausted)
- Add "resource has been exhausted" to creditsExhaustedKeywords
- Update tests to match corrected behavior
2026-03-20 00:04:01 +08:00
erio 528ff5d28c fix(antigravity): fast-fail on proxy unavailable, temp-unschedule account
## Problem

When a proxy is unreachable, token refresh retries up to 4 times with
30s timeout each, causing requests to hang for ~2 minutes before
failing with a generic 502 error. The failed account is not marked,
so subsequent requests keep hitting it.

## Changes

### Proxy connection fast-fail
- Set TCP dial timeout to 5s and TLS handshake timeout to 5s on
  antigravity client, so proxy connectivity issues fail within 5s
  instead of 30s
- Reduce overall HTTP client timeout from 30s to 10s
- Export `IsConnectionError` for service-layer use
- Detect proxy connection errors in `RefreshToken` and return
  immediately with "proxy unavailable" error (no retries)

### Token refresh temp-unschedulable
- Add 8s context timeout for token refresh on request path
- Mark account as temp-unschedulable for 10min when refresh fails
  (both background `TokenRefreshService` and request-path
  `GetAccessToken`)
- Sync temp-unschedulable state to Redis cache for immediate
  scheduler effect
- Inject `TempUnschedCache` into `AntigravityTokenProvider`

### Account failover
- Return `UpstreamFailoverError` on `GetAccessToken` failure in
  `Forward`/`ForwardGemini` to trigger handler-level account switch
  instead of returning 502 directly

### Proxy probe alignment
- Apply same 5s dial/TLS timeout to shared `httpclient` pool
- Reduce proxy probe timeout from 30s to 10s
2026-03-19 23:48:37 +08:00
erio a20c211162 perf(frontend): add virtual scrolling to DataTable
Replace direct row rendering with @tanstack/vue-virtual. The table
now only renders visible rows (~20) via padding <tr> placeholders,
eliminating the rendering bottleneck when displaying 100+ rows with
heavy cell components.

Key changes:
- DataTable.vue: integrate useVirtualizer (always-on), virtual row
  template with measureElement for variable row heights, defineExpose
  virtualizer/sortedData for external access, overflow-y/flex CSS
- useSwipeSelect.ts: dual-mode support via optional
  SwipeSelectVirtualContext — data-driven row index lookup and
  selection range when virtualizer is present, original DOM-based
  path preserved for callers that don't pass virtualContext
2026-03-18 23:11:49 +08:00
erio af96c8ea53 feat: map claude-haiku-4-5 variants to claude-sonnet-4-6
Update model mapping target for claude-haiku-4-5 and
claude-haiku-4-5-20251001 from claude-sonnet-4-5 to claude-sonnet-4-6.
Includes migration script, default constants, and test updates.
2026-03-18 15:03:24 +08:00
erio a6f99cf534 refactor(antigravity): unify TestConnection with dispatch retry loop
TestConnection now reuses antigravityRetryLoop instead of a standalone
HTTP loop, gaining credits overages, smart retry, and 429/503 backoff
for free. AccountSwitchError is caught and surfaced as a friendly
message. Also populates RateLimitedModel in TempUnscheduled switch error.

Test fixes:
- Use RATE_LIMIT_EXCEEDED in 503 short-delay test to avoid 60x1s timeout
- Clamp waitDuration=0 instead of 999s to avoid 15s max-wait timeout
- Enhance mockSmartRetryUpstream with repeatLast and body caching
2026-03-17 01:47:08 +08:00
erio a120a6bc32 fix(ui): remove redundant sub-table header in user breakdown
The expanded user breakdown rows already align with the parent table
columns (Requests, Token, Actual, Standard), so the repeated sub-header
wastes vertical space. Remove the <thead> from UserBreakdownSubTable.
2026-03-17 00:49:43 +08:00
erio d557d1a190 fix(ui): restore original max-h-48 height for distribution tables 2026-03-17 00:47:45 +08:00
erio e0286e5085 test(dashboard): add unit tests for user-breakdown API
Handler tests (9 cases): group_id/model/endpoint filters, default
endpoint_type, custom limit, limit clamping, response format,
empty result, no-filter pass-through.

Repository test: resolveEndpointColumn mapping for inbound/upstream/path.
2026-03-17 00:47:33 +08:00
erio 4b41e898a4 feat(dashboard): add per-user drill-down for group, model, and endpoint distributions
Click on a group name, model name, or endpoint name in the distribution
tables to expand and show per-user usage breakdown (requests, tokens,
actual cost, standard cost).

Backend: new GET /admin/dashboard/user-breakdown API with group_id,
model, endpoint, endpoint_type filters.
Frontend: clickable rows with expand/collapse sub-table in all three
distribution charts.
2026-03-17 00:47:20 +08:00
erio d45cc23171 fix(i18n): correct usage label from "Total" to "Last 30d"
The usage stats query defaults to a 30-day rolling window, but the
UI label said "Total"/"累计" implying lifetime aggregation. Rename
to "Last 30d"/"近30天" so the label matches the actual query semantics.

Closes #1060
2026-03-16 18:25:41 +08:00
erio 71f72e167e chore(antigravity): bump default User-Agent version to 1.20.5 2026-03-16 15:47:32 +08:00
erio 67c0506290 fix(billing): add window expiration check to Redis rate limit Lua script
The updateRateLimitUsageScript Lua script previously performed
unconditional HINCRBYFLOAT on all usage counters without checking
whether the rate limit window had expired. This caused usage to
accumulate across window boundaries in Redis while the DB correctly
reset on expiration, leading to incorrect 429 rate limiting that
could persist for up to 24 hours.

The Lua script now checks each window timestamp before incrementing:
- If the window has expired, usage is reset to the current cost and
  the window timestamp is updated (matching DB-side semantics)
- If the window is still valid, usage is accumulated normally

This also resolves the async race condition where stale HINCRBYFLOAT
tasks from the worker queue could pollute a freshly rebuilt cache
after invalidation, since the script now self-corrects expired windows.

Closes #1049
2026-03-16 13:39:50 +08:00
erio 552a4b998a fix: resolve golangci-lint issues (gofmt, errcheck)
- Fix gofmt alignment in admin_service.go and trailing newline in
  antigravity_credits_overages.go
- Suppress errcheck for fmt.Sscanf in client.go GetMinimumAmount
2026-03-16 05:15:27 +08:00
erio 0d2061b268 fix: remove ClaudeMax references not yet in upstream/main
Remove SimulateClaudeMaxEnabled field and related logic from
admin_service.go, and remove applyClaudeMaxCacheBillingPolicyToUsage,
applyClaudeMaxNonStreamingRewrite, setupClaudeMaxStreamingHook calls
from antigravity_gateway_service.go. These symbols are not yet
available in upstream/main.
2026-03-16 05:01:42 +08:00
erio 8a260defc2 refactor: replace sync.Map credits state with AICredits rate limit key
Replace process-memory sync.Map + per-model runtime state with a single
"AICredits" key in model_rate_limits, making credits exhaustion fully
isomorphic with model-level rate limiting.

Scheduler: rate-limited accounts with overages enabled + credits available
are now scheduled instead of excluded.

Forwarding: when model is rate-limited + credits available, inject credits
proactively without waiting for a 429 round trip.

Storage: credits exhaustion stored as model_rate_limits["AICredits"] with
5h duration, reusing SetModelRateLimit/isRateLimitActiveForKey.

Frontend: show credits_active (yellow ) when model rate-limited but
credits available, credits_exhausted (red) when AICredits key active.

Tests: add unit tests for shouldMarkCreditsExhausted, injectEnabledCreditTypes,
clearCreditsExhausted, and update existing overages tests.
2026-03-16 04:58:58 +08:00
erio 044d3a013d fix: suppress SA4006 unused value warning in Path A branch 2026-03-16 01:38:06 +08:00
erio 1fc9dd7b68 feat: unified OAuth token refresh API with distributed locking
Introduce OAuthRefreshAPI as the single entry point for all OAuth token
refresh operations, eliminating the race condition where background
refresh and inline refresh could simultaneously use the same
refresh_token (fixes #1035).

Key changes:
- Add OAuthRefreshExecutor interface extending TokenRefresher with CacheKey
- Add OAuthRefreshAPI.RefreshIfNeeded with lock → DB re-read → double-check flow
- Add ProviderRefreshPolicy / BackgroundRefreshPolicy strategy types
- Simplify all 4 TokenProviders to delegate to OAuthRefreshAPI
- Rewrite TokenRefreshService.refreshWithRetry to use unified API path
- Add MergeCredentials and BuildClaudeAccountCredentials helpers
- Add 40 unit tests covering all new and modified code paths
2026-03-16 01:31:54 +08:00
erio bdbc8fa08f fix(ops): align constant declarations for gofmt compliance 2026-03-15 18:55:14 +08:00
erio 63f3af0f94 fix(ops): match "insufficient account balance" in error filter
The upstream Gemini API returns "Insufficient account balance" which
doesn't contain the substring "insufficient balance". Add explicit
match for the full phrase to ensure the filter works correctly.
2026-03-15 18:45:48 +08:00
erio 6177fa5dd8 fix(i18n): correct insufficient balance error hint text
Remove misleading "upstream" wording - the error is about client API key
user balance, not upstream account balance.
2026-03-15 17:41:51 +08:00
erio cfe72159d0 feat(ops): add ignore insufficient balance errors toggle and extract error constants
- Add 5th error filter switch IgnoreInsufficientBalanceErrors to suppress
  upstream insufficient balance / insufficient_quota errors from ops log
- Extract hardcoded error strings into package-level constants for
  shouldSkipOpsErrorLog, normalizeOpsErrorType, classifyOpsPhase, and
  classifyOpsIsBusinessLimited
- Define ErrNoAvailableAccounts sentinel error and replace all
  errors.New("no available accounts") call sites
- Update tests to use require.ErrorIs with the sentinel error
2026-03-15 17:26:18 +08:00
erio 5899784aa4 fix(billing): allow clearing group quota limits and treat 0 as zero-limit
Previously, v-model.number produced "" when input was cleared, causing
JSON decode errors on the backend. Also, normalizeLimit treated 0 as
"unlimited" which prevented setting a zero quota. Now "" is converted
to null (unlimited) in frontend, and 0 is preserved as a valid limit.

Closes Wei-Shaw/sub2api#1021
2026-03-15 16:15:15 +08:00
erio 9e8959c56d fix(billing): treat nil rate limit window as expired to prevent usage accumulation
When Redis cache is populated from DB with a NULL window_1d_start, the
Lua increment script only updates usage counters without setting window
timestamps. IsWindowExpired(nil) previously returned false, so the
accumulated usage was never reset across time windows, effectively
turning usage_1d into a lifetime counter. Once this exceeded
rate_limit_1d the key was incorrectly blocked with "日限额已用完".

Fixes Wei-Shaw/sub2api#1022
2026-03-15 14:04:13 +08:00
erio f59b66b7d4 fix(ops): tune aggregation constants to prevent PG overload
Increase MAX(bucket_start) query timeout from 3s to 5s to reduce
timeout-induced fallbacks. Shrink backfill window from 30 days to
1 hour so that fallback recomputation stays lightweight instead of
scanning the entire retention range.
2026-03-14 18:47:37 +08:00
erio 91e4d95660 docs: add iframe integration feature and ecosystem projects section 2026-03-14 03:16:07 +08:00
erio 45456fa24c fix: restore OAuth 401 temp-unschedulable for Gemini, update Antigravity tests
The 403 detection PR changed the 401 handler condition from
`account.Type == AccountTypeOAuth` to
`account.Type == AccountTypeOAuth && account.Platform == PlatformOpenAI`,
which accidentally excluded Gemini OAuth from the temp-unschedulable path.

Fix: use `!= PlatformAntigravity` instead, preserving Gemini behavior
while correctly excluding Antigravity (whose 401 is handled by
applyErrorPolicy's temp_unschedulable_rules).

Update tests to reflect Antigravity's new 401 semantics:
- HandleUpstreamError: Antigravity OAuth 401 now uses SetError
- CheckErrorPolicy: Antigravity 401 second hit stays TempUnscheduled
- DB fallback: split into Gemini (escalates) and Antigravity (stays temp)
2026-03-14 02:21:22 +08:00
erio 05edb5514b feat(redeem): support subscription type in create-and-redeem API
Add group_id and validity_days fields to CreateAndRedeemCodeRequest,
enabling subscription-type redemption codes to be created and redeemed
in a single API call.

- Type defaults to "balance" when omitted for backward compatibility
- Subscription type requires group_id (non-nil) and validity_days (>0)
- Existing balance/concurrency callers are unaffected
2026-03-13 21:26:46 +08:00
erio 6344fa2a86 feat(antigravity): add 403 forbidden status detection, classification and display
Backend:
- Detect and classify 403 responses into three types:
  validation (account needs Google verification),
  violation (terms of service / banned),
  forbidden (generic 403)
- Extract verification/appeal URLs from 403 response body
  (structured JSON parsing with regex fallback)
- Add needs_verify, is_banned, needs_reauth, error_code fields
  to UsageInfo (omitempty for zero impact on other platforms)
- Handle 403 in request path: classify and permanently set account error
- Save validation_url in error_message for degraded path recovery
- Enrich usage with account error on both success and degraded paths
- Add singleflight dedup for usage requests with independent context
- Differentiate cache TTL: success/403 → 3min, errors → 1min
- Return degraded UsageInfo instead of HTTP 500 on quota fetch errors

Frontend:
- Display forbidden status badges with color coding (red for banned,
  amber for needs verification, gray for generic)
- Show clickable verification/appeal URL links
- Display needs_reauth and degraded error states in usage cell
- Add Antigravity tier label badge next to platform type

Tests:
- Comprehensive unit tests for classifyForbiddenType (7 cases)
- Unit tests for extractValidationURL (8 cases including unicode escapes)
- Integration test for FetchQuota forbidden path
2026-03-13 18:22:45 +08:00
erio d648811233 feat(groups): add rate multipliers management modal
Add a dedicated modal in group management for viewing, adding, editing,
and deleting per-user rate multipliers within a group.

Backend:
- GET /admin/groups/:id/rate-multipliers - list entries with user details
- PUT /admin/groups/:id/rate-multipliers - batch sync (full replace)
- DELETE /admin/groups/:id/rate-multipliers - clear all entries
- Repository: GetByGroupID, SyncGroupRateMultipliers methods on
  user_group_rate_multipliers table (same table as user-side rates)

Frontend:
- New GroupRateMultipliersModal component with:
  - User search and add with email autocomplete
  - Editable rate column with local edit mode (cancel/save)
  - Batch adjust: multiply all rates by a factor
  - Clear all (local operation, requires save to persist)
  - Pagination (10/20/50 per page)
  - Platform icon with brand colors in group info bar
  - Unsaved changes indicator with revert option
- Unit tests for all three backend endpoints
2026-03-12 23:37:36 +08:00
erio bcaae2eb91 fix: use shared max_line_size config for OpenAI Responses SSE scanner
Two SSE scanners in openai_gateway_messages.go were hardcoded to 1MB
while all other scanners use defaultMaxLineSize (500MB) with config
override. This caused Responses API streams to fail on large payloads.
2026-03-10 02:50:04 +08:00
erioandClaude Opus 4.6 acefa1da12 fix(frontend): stack quota badges vertically in capacity column
QuotaBadge components (daily/weekly/total) were wrapped in a
horizontal flex container, making them visually inconsistent with
other capacity badges (concurrency, window cost, sessions, RPM)
which stack vertically. Remove the wrapper so all badges align
consistently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:18:12 +08:00
erioandClaude Opus 4.6 a88698f3fc feat: cleanup stale concurrency slots on startup
When the service restarts, concurrency slots from the old process
remain in Redis, causing phantom occupancy. On startup, scan all
concurrency sorted sets and remove members with non-current process
prefix, then clear orphaned wait queue counters.

Uses Go-side SCAN to discover keys (compatible with Redis client
prefix hooks in tests), then passes them to a Lua script for
atomic member-level cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:55:18 +08:00
erioandClaude Opus 4.6 ebc6755b33 feat(frontend): pass locale to iframe embedded pages via lang parameter
Embedded pages (purchase subscription, custom pages) now receive the
current user locale through a `lang` URL parameter, allowing iframe
content to match the user's language preference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:38:23 +08:00
erio eb6f5c6927 test: update UserAgent version assertion to match 1.20.4 2026-03-09 08:59:21 +08:00
erio 7ccb4c8ea3 chore: bump Antigravity user agent version to 1.20.4 2026-03-09 08:59:21 +08:00
erio 4ce986d47d fix: also update viper default max_line_size from 40MB to 500MB
The viper config default (config.go) was overriding the constant
in gateway_service.go. Both must be updated to take effect.
2026-03-09 08:56:54 +08:00
erio 91ef085d7d fix: increase SSE scanner max line size from 40MB to 500MB
4K image base64 data can exceed 40MB limit, causing "bufio.Scanner:
token too long" errors. Scanner is adaptive (starts at 64KB, grows
as needed), so increasing the cap has no impact on normal responses.
2026-03-09 08:56:54 +08:00
erio 4cd6d86426 feat(frontend): add API Key quota progress bars to usage window column
Display daily/weekly/total quota utilization as progress bars in the
usage window column for API Key accounts, providing visual feedback
consistent with other account types (OAuth/Gemini).

- Daily quota: "1d" label with reset countdown
- Weekly quota: "7d" label with reset countdown
- Total quota: "total" label (no reset)
2026-03-08 21:35:26 +08:00
erio 0be003377f feat(frontend): add swipe-to-select for admin tables
Squash of all swipe-select commits for clean rebase.
2026-03-08 21:07:43 +08:00
erioandClaude Opus 4.6 2475d4a205 feat: add marquee selection box overlay during drag-to-select
Show a semi-transparent blue rectangle overlay while dragging to
select rows, matching the project's primary color theme with dark
mode support. The box spans the full table width from drag start
to current mouse position.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:45:22 +08:00
erio be75fc3474 ci: retrigger CI checks 2026-03-08 01:42:18 +08:00
erioandClaude Opus 4.6 d22e62ac8a fix(test): add allow_messages_dispatch to group API contract test
The recent upstream commit added allow_messages_dispatch to the Group
DTO but did not update the API contract test expectation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:28:22 +08:00
erioandClaude Opus 4.6 1ee17383f8 feat(account): add daily/weekly periodic quota limits for API Key accounts
Extend the existing total quota limit with daily and weekly periodic
dimensions. Each dimension is independently configurable and uses lazy
reset — when the period expires, usage is automatically reset to zero on
the next increment. Any dimension exceeding its limit will pause the
account from scheduling.

Backend:
- Add GetQuotaDailyLimit/Used, GetQuotaWeeklyLimit/Used, HasAnyQuotaLimit
- Rewrite IncrementQuotaUsed with atomic CTE SQL for 3-dimension update
- Rewrite ResetQuotaUsed to clear all dimensions and period timestamps
- Update postUsageBilling to use HasAnyQuotaLimit()
- Preserve daily/weekly used values on account edit

Frontend:
- Refactor QuotaLimitCard from single v-model to 3-dimension props
- Add QuotaBadge component for compact D/W/$ display
- Update AccountCapacityCell with per-dimension badges
- Update Create/Edit modals with daily/weekly quota fields
- Update AccountActionMenu hasQuotaLimit to check all dimensions
- Add i18n strings for daily/weekly/total quota labels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:06:59 +08:00
erio c28f691f32 fix(openai): remove misplaced passthrough check from isModelSupportedByAccount
isModelSupportedByAccount 不被 OpenAI 调度路径调用,
OpenAI /responses 和 /chat/completions 走的是
openai_account_scheduler.go,透传短路已在 PR #806 的
第二个 commit 中正确添加到该文件。

此处的检查是多余的死代码,因为 OpenAI 账号不会走到
isModelSupportedByAccount 的这个分支。
2026-03-06 14:32:08 +08:00
erio 79ae15d5e8 fix: OpenAI passthrough accounts bypass model mapping check
透传模式账号仅替换认证,应允许所有模型通过。之前调度阶段的
isModelSupportedByAccount 不感知透传模式,导致 model_mapping
中未配置的新模型(如 gpt-5.4)被拒绝返回 503。
2026-03-06 14:01:47 +08:00
erio c87e6526c1 fix: use real Concurrency instead of LoadFactor for metrics and monitoring
LoadFactor should only affect scheduling weight, not load rate reporting.
2026-03-06 05:18:45 +08:00
erio af3a5076d6 fix: add load_factor upper bound validation to BulkUpdateAccounts 2026-03-06 05:17:52 +08:00
erio 18f2e21414 fix: use HTML-safe expressions for @input handlers in Vue templates
Replace `<` comparisons with Math.max/ternary+>= to avoid Vue template
parser treating `<` as HTML tag start in attribute values.
2026-03-06 05:07:52 +08:00