Conversation
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthrough该PR实现了提供商级别配额功能,包括后端配额评估逻辑、路由器集成、服务层适配和前端UI组件,支持按请求数、Token或成本限额,超额时停止路由。 Changes
Sequence DiagramsequenceDiagram
participant Client as 路由客户端
participant Router as Router.Match()
participant QuotaPkg as quota.EvaluateProviderQuota
participant UsageRepo as UsageStatsRepository
participant SettingRepo as SettingRepository
participant Domain as 数据模型
Client->>Router: 路由请求(提供商)
Router->>Router: 初始化配额缓存
loop 对每个候选提供商
Router->>QuotaPkg: EvaluateProviderQuota(provider, usageRepo, settingRepo)
activate QuotaPkg
QuotaPkg->>Domain: 验证配额配置和周期
alt 配额禁用或无限额
QuotaPkg-->>Router: 返回禁用状态
else 配额启用且有限额
QuotaPkg->>SettingRepo: Get(timezone_key)
SettingRepo-->>QuotaPkg: 返回时区设置
QuotaPkg->>QuotaPkg: 计算周期边界
QuotaPkg->>UsageRepo: GetSummary(tenantID, filter)
UsageRepo-->>QuotaPkg: 返回使用统计
QuotaPkg->>Domain: buildMetric(requests, tokens, cost)
Domain-->>QuotaPkg: 构建指标状态
QuotaPkg->>QuotaPkg: 聚合警告和超额标志
QuotaPkg-->>Router: 返回ProviderQuotaStatus
end
deactivate QuotaPkg
Router->>Router: 检查是否超额<br/>缓存结果
alt 配额超额
Router->>Router: 跳过该提供商
Router->>Router: 记录配额评估日志
else 配额未超额
Router->>Router: 继续该提供商<br/>用于匹配
end
end
Router-->>Client: 返回路由结果
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
web/src/pages/providers/components/provider-edit-flow.tsx (1)
386-442: 建议:handleSave与handleClone中构建 provider 数据的逻辑高度重复。两个函数中
clientBaseURL、clientMultiplier、cloak和quota的构建方式完全相同。可考虑提取为共享函数以减少维护成本。♻️ 可选的重构建议
// 提取共享的 config 构建逻辑 const buildProviderConfig = () => { const supportedClientTypes = formData.clients.filter((c) => c.enabled).map((c) => c.id); const clientBaseURL: Partial<Record<ClientType, string>> = {}; const clientMultiplier: Partial<Record<ClientType, number>> = {}; formData.clients.forEach((c) => { if (c.enabled && c.urlOverride) { clientBaseURL[c.id] = c.urlOverride; } if (c.enabled && c.multiplier !== 10000) { clientMultiplier[c.id] = c.multiplier; } }); return { supportedClientTypes, config: { disableErrorCooldown: !!formData.disableErrorCooldown, quota: buildQuotaConfig(), custom: { baseURL: formData.baseURL, apiKey: formData.apiKey || provider.config?.custom?.apiKey || '', clientBaseURL: Object.keys(clientBaseURL).length > 0 ? clientBaseURL : undefined, clientMultiplier: Object.keys(clientMultiplier).length > 0 ? clientMultiplier : undefined, cloak: /* ... cloak config ... */, }, }, }; };Also applies to: 444-526
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/providers/components/provider-edit-flow.tsx` around lines 386 - 442, handleSave and handleClone duplicate the logic for building provider payloads (clientBaseURL, clientMultiplier, cloak, quota, supportedClientTypes); extract that shared logic into a helper (e.g., buildProviderConfig or buildProviderPayload) that reads formData and calls buildQuotaConfig and parseSensitiveWords, returns { supportedClientTypes, config } (with custom.baseURL, apiKey fallback, clientBaseURL/clientMultiplier only when non-empty, and cloak only when needed). Replace the duplicated blocks in handleSave and handleClone to call this helper and use its return value when calling updateProvider.mutateAsync / createProvider.mutateAsync so behavior remains identical but maintenance is centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/quota/provider_quota.go`:
- Around line 133-151: The resolveLocation function currently falls back to UTC
on missing/invalid timezone, which shifts quota windows; change the fallback to
the system/default timezone used by the app (per internal/domain/model.go's
SettingKeyTimezone default "Asia/Shanghai") instead of hardcoding UTC: on nil
settingRepo, Get error, empty name, or LoadLocation error return the
system/default location (use time.Local or load "Asia/Shanghai") and the
corresponding name rather than time.UTC/"UTC" so quota cycles align with the
declared default timezone.
In `@web/src/locales/en.json`:
- Around line 1103-1119: The "quotaTitle" string currently reads "3. Provider
Quota" which conflicts with "provider.cloakTitle" ("3. Claude Cloaking"); update
the numbering to avoid duplicate "3." by either removing hardcoded numeric
prefixes across locale keys or renumbering the affected keys (change
"quotaTitle" -> "4. Provider Quota" and update subsequent keys like
"errorCooldownTitle" -> "5. Error Cooldown"); locate and adjust the locale keys
"quotaTitle" and "errorCooldownTitle" (and any other nearby numbered titles) or
remove numbering and have the component render section numbers instead.
In `@web/src/pages/providers/components/provider-row.tsx`:
- Around line 200-221: getGenericQuotaInfo currently ignores the
backend-calculated warning threshold and always assumes an 80% warning; update
getGenericQuotaInfo (and its return shape) to include and forward the backend's
computed warning flag by returning warning: !!mostUsed.metric?.warning (in
addition to percentage/label/resetTime/exceeded) so the UI can reuse the
backend's metric.warning instead of hardcoding 80% when deciding colors/alerts;
ensure references to Provider['quotaStatus'] and mostUsed.metric are used to
populate this new warning field.
---
Nitpick comments:
In `@web/src/pages/providers/components/provider-edit-flow.tsx`:
- Around line 386-442: handleSave and handleClone duplicate the logic for
building provider payloads (clientBaseURL, clientMultiplier, cloak, quota,
supportedClientTypes); extract that shared logic into a helper (e.g.,
buildProviderConfig or buildProviderPayload) that reads formData and calls
buildQuotaConfig and parseSensitiveWords, returns { supportedClientTypes, config
} (with custom.baseURL, apiKey fallback, clientBaseURL/clientMultiplier only
when non-empty, and cloak only when needed). Replace the duplicated blocks in
handleSave and handleClone to call this helper and use its return value when
calling updateProvider.mutateAsync / createProvider.mutateAsync so behavior
remains identical but maintenance is centralized.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8e0979c1-d33c-461b-8702-cfb0d249fbed
📒 Files selected for processing (13)
cmd/maxx/main.gointernal/core/database.gointernal/domain/model.gointernal/quota/provider_quota.gointernal/quota/provider_quota_test.gointernal/router/router.gointernal/service/admin.goweb/src/lib/transport/index.tsweb/src/lib/transport/types.tsweb/src/locales/en.jsonweb/src/locales/zh.jsonweb/src/pages/providers/components/provider-edit-flow.tsxweb/src/pages/providers/components/provider-row.tsx
📜 Review details
🔇 Additional comments (14)
internal/service/admin.go (4)
103-111: LGTM!GetProviders 现在正确地为每个 provider 附加配额状态。循环遍历并调用
attachProviderQuotaStatus的实现清晰简洁。
114-121: LGTM!GetProvider 同样正确地附加了配额状态,与 GetProviders 保持一致的处理模式。
139-149: LGTM!
attachProviderQuotaStatus实现合理:
- 完善的 nil 检查(provider、config、quota、usageStatsRepo)
- 错误仅记录日志而不阻断流程,符合状态增强的非关键性质
- 与
quota.EvaluateProviderQuota的接口契约一致
736-736: 格式调整,无功能变更。Also applies to: 804-808
web/src/pages/providers/components/provider-edit-flow.tsx (4)
277-287: LGTM!类型定义清晰完整,涵盖了配额功能所需的所有字段。默认值的选择合理(quotaPeriod 为 'day',warningThreshold 为 80%)。
319-333: LGTM!初始化逻辑正确处理了可选值,使用
??和条件表达式为未配置的字段提供合理默认值。
357-384: LGTM!辅助函数实现良好:
parsePositiveInt正确处理空值、NaN、Infinity 和非正数buildQuotaConfig在未启用且无限制时返回 undefined,避免发送空配置warningThresholdPercent钳制到 1-100 范围是合理的边界校验
745-850: LGTM!配额配置 UI 实现完善:
- 启用开关控制整体功能
- 周期选择器提供日/周/月选项
- 数值输入正确设置了
type="number"和min约束- 使用 i18n 进行国际化
- 提供了成本单位说明(nano USD)
internal/router/router.go (3)
42-43: LGTM!使用接口类型 (
quota.UsageSummaryProvider和quota.TimezoneSettingProvider) 而非具体仓库类型,便于测试和解耦。
54-74: LGTM!NewRouter 签名扩展正确。接受具体的 repository 类型并赋值给接口字段,利用了 Go 的隐式接口实现。根据 context snippet 2-4,
repository.UsageStatsRepository和repository.SystemSettingRepository均满足相应接口契约。
208-247: LGTM!配额评估逻辑实现合理:
quotaExceededCache避免同一 provider 重复评估- 先缓存结果再记录错误,顺序正确
exceeded := quotaStatus != nil && quotaStatus.Exceeded处理了 quotaStatus 可能为 nil 的情况- 采用 fail-open 策略:评估失败时不阻断路由,仅记录日志
web/src/lib/transport/types.ts (2)
9-42: LGTM!新增的配额相关类型与 Go 后端
internal/domain/model.go中的结构体定义一致:
- 字段名称和 JSON 标签匹配
periodStart/periodEnd使用string类型正确(Go 的time.Time序列化为 ISO 字符串)- 可选字段标记合理
符合文件头注释"与 Go internal/domain/model.go 保持同步"的要求。
103-111: LGTM!
ProviderConfig和Provider接口正确扩展了配额相关字段,与后端数据结构保持一致。Also applies to: 113-124
web/src/locales/zh.json (1)
1103-1119: LGTM!配额相关的本地化字符串完整且翻译准确:
- 章节编号从 "3. 错误冷冻" 更新为 "4. 错误冷冻",与新增的 "3. Provider 配额" 保持一致
quotaCostLimitHint清晰解释了 nano USD 单位- 所有 UI 元素都有对应的翻译
| func resolveLocation(settingRepo TimezoneSettingProvider) (*time.Location, string) { | ||
| if settingRepo == nil { | ||
| return time.UTC, "UTC" | ||
| } | ||
|
|
||
| name, err := settingRepo.Get(domain.SettingKeyTimezone) | ||
| if err != nil { | ||
| return time.UTC, "UTC" | ||
| } | ||
| name = strings.TrimSpace(name) | ||
| if name == "" { | ||
| return time.UTC, "UTC" | ||
| } | ||
|
|
||
| loc, err := time.LoadLocation(name) | ||
| if err != nil { | ||
| return time.UTC, "UTC" | ||
| } | ||
| return loc, name |
There was a problem hiding this comment.
默认时区回退成 UTC 会把配额周期算偏。
这里在设置缺失、读取失败或时区非法时一律回退到 UTC,但 internal/domain/model.go 对 SettingKeyTimezone 的注释已经声明默认值是 Asia/Shanghai。这会让新实例或设置暂时不可读时的日/周/月窗口整体偏移,进而提前或延后判定 provider 超额,直接影响路由结果。建议这里复用系统默认时区,而不是硬编码成 UTC。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/quota/provider_quota.go` around lines 133 - 151, The resolveLocation
function currently falls back to UTC on missing/invalid timezone, which shifts
quota windows; change the fallback to the system/default timezone used by the
app (per internal/domain/model.go's SettingKeyTimezone default "Asia/Shanghai")
instead of hardcoding UTC: on nil settingRepo, Get error, empty name, or
LoadLocation error return the system/default location (use time.Local or load
"Asia/Shanghai") and the corresponding name rather than time.UTC/"UTC" so quota
cycles align with the declared default timezone.
| "quotaTitle": "3. Provider Quota", | ||
| "enableQuota": "Enable Provider Quota", | ||
| "enableQuotaDesc": "Limit requests, tokens, or cost per provider. Exceeded providers are skipped until the period resets.", | ||
| "quotaPeriod": "Reset Period", | ||
| "quotaPeriodDay": "Daily", | ||
| "quotaPeriodWeek": "Weekly", | ||
| "quotaPeriodMonth": "Monthly", | ||
| "quotaWarningThreshold": "Warning Threshold (%)", | ||
| "quotaRequestLimit": "Request Limit", | ||
| "quotaTokenLimit": "Token Limit", | ||
| "quotaCostLimit": "Cost Limit (nano USD)", | ||
| "quotaCostLimitHint": "Cost uses nano USD (1 USD = 1,000,000,000 nano USD). Leave empty for unlimited.", | ||
| "quotaUnlimitedPlaceholder": "Empty = Unlimited", | ||
| "quotaRequestShort": "REQ", | ||
| "quotaTokenShort": "TKN", | ||
| "quotaCostShort": "COST", | ||
| "errorCooldownTitle": "4. Error Cooldown", |
There was a problem hiding this comment.
步骤编号和现有标题冲突。
Line 1103 把配额段落写成了 3. Provider Quota,但同一命名空间里 provider.cloakTitle 仍然是 3. Claude Cloaking。如果这两个区块会同时显示,界面上会出现两个“3.”。建议把这里和后续标题一起顺延,或者把编号移到组件里统一生成。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/locales/en.json` around lines 1103 - 1119, The "quotaTitle" string
currently reads "3. Provider Quota" which conflicts with "provider.cloakTitle"
("3. Claude Cloaking"); update the numbering to avoid duplicate "3." by either
removing hardcoded numeric prefixes across locale keys or renumbering the
affected keys (change "quotaTitle" -> "4. Provider Quota" and update subsequent
keys like "errorCooldownTitle" -> "5. Error Cooldown"); locate and adjust the
locale keys "quotaTitle" and "errorCooldownTitle" (and any other nearby numbered
titles) or remove numbering and have the component render section numbers
instead.
| function getGenericQuotaInfo( | ||
| status: Provider['quotaStatus'] | undefined, | ||
| ): { percentage: number; label: 'requests' | 'tokens' | 'cost'; resetTime: string; exceeded: boolean } | null { | ||
| if (!status || !status.enabled || !status.hasAnyLimit) return null; | ||
|
|
||
| const candidates = [ | ||
| { label: 'requests' as const, metric: status.requests }, | ||
| { label: 'tokens' as const, metric: status.tokens }, | ||
| { label: 'cost' as const, metric: status.cost }, | ||
| ].filter((item) => item.metric); | ||
|
|
||
| if (candidates.length === 0) return null; | ||
| const mostUsed = candidates.reduce((max, cur) => | ||
| (cur.metric?.usagePercent || 0) > (max.metric?.usagePercent || 0) ? cur : max, | ||
| ); | ||
|
|
||
| return { | ||
| percentage: Math.min(Math.round(mostUsed.metric?.usagePercent || 0), 100), | ||
| label: mostUsed.label, | ||
| resetTime: status.periodEnd, | ||
| exceeded: !!mostUsed.metric?.exceeded, | ||
| }; |
There was a problem hiding this comment.
通用配额条忽略了可配置的预警阈值。
这里把预警颜色写死成了 80%,但后端的 warningThresholdPercent 是可配置的,而且 metric.warning 已经算好了。阈值不是 80 时,UI 会把已经进入 warning 的 provider 继续显示成绿色,和实际状态不一致。
💡 建议改成直接复用后端计算结果
function getGenericQuotaInfo(
status: Provider['quotaStatus'] | undefined,
-): { percentage: number; label: 'requests' | 'tokens' | 'cost'; resetTime: string; exceeded: boolean } | null {
+): {
+ percentage: number;
+ label: 'requests' | 'tokens' | 'cost';
+ resetTime: string;
+ warning: boolean;
+ exceeded: boolean;
+} | null {
if (!status || !status.enabled || !status.hasAnyLimit) return null;
@@
return {
percentage: Math.min(Math.round(mostUsed.metric?.usagePercent || 0), 100),
label: mostUsed.label,
resetTime: status.periodEnd,
+ warning: !!mostUsed.metric?.warning,
exceeded: !!mostUsed.metric?.exceeded,
};
}
@@
'h-full rounded-full transition-all duration-1000',
genericQuotaInfo.exceeded
? 'bg-red-500'
- : genericQuotaInfo.percentage >= 80
+ : genericQuotaInfo.warning
? 'bg-amber-500'
: 'bg-emerald-500',
)}Also applies to: 441-465
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/providers/components/provider-row.tsx` around lines 200 - 221,
getGenericQuotaInfo currently ignores the backend-calculated warning threshold
and always assumes an 80% warning; update getGenericQuotaInfo (and its return
shape) to include and forward the backend's computed warning flag by returning
warning: !!mostUsed.metric?.warning (in addition to
percentage/label/resetTime/exceeded) so the UI can reuse the backend's
metric.warning instead of hardcoding 80% when deciding colors/alerts; ensure
references to Provider['quotaStatus'] and mostUsed.metric are used to populate
this new warning field.
Closes #56
Summary by CodeRabbit
发布说明