Files
sub2api/backend/internal/server/api_contract_test.go
T
shaw dec47e8fae fix(profit-control): stop leaking profit policy, close veto livelock, restore passthrough turn pricing
审计修复,逐条如下。

H1 利润策略泄露给所有普通用户
  profit_control_enabled / profit_min_margin / profit_safety_buffer 从
  dto.Group 移到 dto.AdminGroup(后者内嵌前者),赋值相应从
  groupFromServiceBase 移到 GroupFromServiceAdmin;前端 TS 同步从 Group 移到
  AdminGroup。dto.Group 是 GET /api/v1/groups/available 的响应体,该响应本就带
  rate_multiplier,相乘即可反推运营方上游采购成本上限。
  api_contract_test.go 的 /groups/available golden JSON 回滚这三个字段,并把
  fixture 改成非零值(require.JSONEq 是精确比对,缺字段即失败)。
  新增 dto 层边界测试:普通用户 DTO 不含三字段、管理员 DTO 仍含。

M1 利润终检 continue 与 failover 503 退避互动产生活锁
  FailoverState 新增 profitVetoedAccountIDs / profitVetoCount 与
  RecordProfitVeto():加入排除集 + 计数,达 maxProfitVetoAttempts(10) 返回
  FailoverExhausted。HandleSelectionExhausted 的 503 清空分支改为清空后把利润
  否决的账号放回排除集;若排除集已全部由利润否决贡献,清空不会带来任何新候选,
  直接判定耗尽(否则 SwitchCount 永不前进、退避条件永远成立,每 2s 空转一轮)。
  五个 handler 否决点(gateway_handler ×2 / responses / chat_completions /
  gemini_v1beta)改为经 RecordProfitVeto 决策,耗尽时按无可用账号终止。
  回归测试钉死:503 之后持续利润否决必须有限步终止且不 spin;未启用利润控制的
  请求退避语义完全不变。

M2 排队等槽后才终检,延迟可放大到 N × WaitPlan.Timeout
  OpenAI 侧选号循环(自有 failedAccountIDs map,非 FailoverState)新增
  recordOpenAIProfitVeto + handleOpenAIProfitVetoExhausted,共用同一上限语义。
  覆盖 responses / messages-dispatch / chat_completions / alpha_search /
  embeddings / images / grok_media 七处,以及 WS 两处否决分支。

M4 ws_v2 透传 ingress 绕过 per-turn 重定价(选方案 B:最小止血)
  透传 relay 只回调 AfterTurn、没有任何 turn 起始回调,hooks.BeforeTurn 永远
  不触发,而 handler 把 turnPricingAt 初始化成建连时刻 ⇒ 透传连接全部 turn 按
  建连时刻的高峰因子结算,客户端峰前建连保活即可全程谷价——正是本 PR 想堵的
  漏洞。改为 openAIWSTurnPricing 零值起步、只由 BeforeTurn 冻结;透传路径保持
  零值,RecordUsage 回退记录时刻,与引入利润控制前的基线一致。
  未选方案 A(给透传补 turn 起始回调):passthrough_relay.go 是 #5167 刚修过的
  取消传播/close frame 时序敏感区;且 BeforeTurn 还承担 turn>1 的并发槽位抢占,
  接进去等于给透传连接引入 per-turn 抢槽,风险远超本次修复范围。透传仍有建连时
  的准入门,只是没有 turn 级复核,已在两处注释写明。
  测试:service 层钉死透传 ingress 不触发 BeforeTurn(含失败时的复核指引),
  handler 层钉死零值语义与逐 turn 覆盖。

M5 装门读分组走了带账号计数聚合的 GetByID
  SchedulerSnapshotService 新增 GetGroupByIDLite,openai/gateway 两处装门改用
  之。门只需要平台/倍率/利润/高峰字段,且该查询发生在「是否启用利润控制」判定
  之前,未启用的分组同样付代价。两个测试 stub 的 GetByID 改成 panic 守卫。

M6 认证快照注释与真实读取路径相反
  门解析优先取 ctxkey.Group,而它就是本快照物化出来的对象,直连流量走的正是这
  条路。改正注释,与 api_key_repo.go 投影处的说明对齐,避免后人照旧注释删列。

M3 rate_multiplier 为 nil 时利润门 fail-closed(不改行为,加护栏)
  保留 fail-closed。补 repository 层测试钉死账号调度快照的 full/metadata 两份
  payload 都必须保留 RateMultiplier(含 0 值),漏列在 CI 就红。

L1 迁移号注释 191 / 191-192 改为实际的 192/193。
L2 admin group Create 的利润配置预校验改用与 CreateGroup 一致的归一化平台
   (新增 service.NormalizeGroupPlatform,两边共用)。保留预校验而非删除:
   service 层返回的是无类型 error,经 ErrorFrom 会变成 500,删掉会把合法的
   400 降级成 500。
L3 前端利润校验的上界改为判定换算后的小数(后端按小数校验 [0,1)),
   99.999% 会四舍五入进位成 1.0 而被后端 400;i18n en/zh 同步改为 0-99.99。
L4 clampProfitControlThreshold / profitControlOverThreshold 抽为共用函数,
   线上装门/否决点与 profit-preview 不再各自实现,附边界语义测试。
L5 profit-preview 补「默认 D 有账号但最低有效 D 归零」的告警(两档都为 0 由
   既有告警覆盖,不重复)。
2026-08-01 22:39:33 +08:00

2889 lines
103 KiB
Go

//go:build unit
package server_test
import (
"bytes"
"context"
"errors"
"io"
"math"
"net/http"
"net/http/httptest"
"sort"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler"
adminhandler "github.com/Wei-Shaw/sub2api/internal/handler/admin"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func TestAPIContracts(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
setup func(t *testing.T, deps *contractDeps)
method string
path string
body string
headers map[string]string
wantStatus int
wantJSON string
}{
{
name: "GET /api/v1/auth/me",
method: http.MethodGet,
path: "/api/v1/auth/me",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"id": 1,
"email": "alice@example.com",
"email_bound": true,
"username": "alice",
"role": "user",
"balance": 12.5,
"frozen_balance": 0,
"concurrency": 5,
"rpm_limit": 0,
"status": "active",
"allowed_groups": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z",
"balance_notify_enabled": false,
"balance_notify_threshold_type": "",
"balance_notify_threshold": null,
"balance_notify_extra_emails": null,
"total_recharged": 0,
"linuxdo_bound": false,
"oidc_bound": false,
"wechat_bound": false,
"dingtalk_bound": false,
"identities": {
"email": {
"provider": "email",
"provider_key": "email",
"bound": true,
"bound_count": 1,
"can_bind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
},
"linuxdo": {
"provider": "linuxdo",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"oidc": {
"provider": "oidc",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"wechat": {
"provider": "wechat",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"dingtalk": {
"provider": "dingtalk",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
}
},
"identity_bindings": {
"email": {
"provider": "email",
"provider_key": "email",
"bound": true,
"bound_count": 1,
"can_bind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
},
"linuxdo": {
"provider": "linuxdo",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"oidc": {
"provider": "oidc",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"wechat": {
"provider": "wechat",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"dingtalk": {
"provider": "dingtalk",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
}
},
"auth_bindings": {
"email": {
"provider": "email",
"provider_key": "email",
"bound": true,
"bound_count": 1,
"can_bind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
},
"linuxdo": {
"provider": "linuxdo",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"oidc": {
"provider": "oidc",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"wechat": {
"provider": "wechat",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
},
"dingtalk": {
"provider": "dingtalk",
"bound": false,
"bound_count": 0,
"can_bind": true,
"can_unbind": false,
"bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
}
},
"run_mode": "standard"
}
}`,
},
{
name: "POST /api/v1/keys",
method: http.MethodPost,
path: "/api/v1/keys",
body: `{"name":"Key One","custom_key":"sk_custom_1234567890"}`,
headers: map[string]string{
"Content-Type": "application/json",
},
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"id": 100,
"user_id": 1,
"key": "sk_custom_1234567890",
"name": "Key One",
"group_id": null,
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"last_used_at": null,
"last_used_ip": null,
"current_concurrency": 0,
"quota": 0,
"quota_used": 0,
"rate_limit_5h": 0,
"rate_limit_1d": 0,
"rate_limit_7d": 0,
"usage_5h": 0,
"usage_1d": 0,
"usage_7d": 0,
"window_5h_start": null,
"window_1d_start": null,
"window_7d_start": null,
"expires_at": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
}`,
},
{
name: "GET /api/v1/keys (paginated)",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
deps.apiKeyRepo.MustSeed(&service.APIKey{
ID: 100,
UserID: 1,
Key: "sk_custom_1234567890",
Name: "Key One",
Status: service.StatusActive,
CreatedAt: deps.now,
UpdatedAt: deps.now,
})
},
method: http.MethodGet,
path: "/api/v1/keys?page=1&page_size=10",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"items": [
{
"id": 100,
"user_id": 1,
"key": "sk_custom_1234567890",
"name": "Key One",
"group_id": null,
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"last_used_at": null,
"last_used_ip": null,
"current_concurrency": 0,
"quota": 0,
"quota_used": 0,
"rate_limit_5h": 0,
"rate_limit_1d": 0,
"rate_limit_7d": 0,
"usage_5h": 0,
"usage_1d": 0,
"usage_7d": 0,
"window_5h_start": null,
"window_1d_start": null,
"window_7d_start": null,
"expires_at": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
],
"total": 1,
"page": 1,
"page_size": 10,
"pages": 1
}
}`,
},
{
name: "GET /api/v1/groups/available",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
// 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count),
// 也不得包含利润控制配置——它与同响应的 rate_multiplier 相乘即可反推上游成本上限。
deps.groupRepo.SetActive([]service.Group{
{
ID: 10,
Name: "Group One",
Description: "desc",
Platform: service.PlatformAnthropic,
RateMultiplier: 1.5,
PeakRateMultiplier: 1.0,
IsExclusive: false,
Status: service.StatusActive,
SubscriptionType: service.SubscriptionTypeStandard,
ProfitControlEnabled: true,
ProfitMinMargin: 0.3,
ProfitSafetyBuffer: 0.05,
ModelRoutingEnabled: true,
ModelRouting: map[string][]int64{
"claude-3-*": []int64{101, 102},
},
AccountCount: 2,
CreatedAt: deps.now,
UpdatedAt: deps.now,
},
})
deps.userSubRepo.SetActiveByUserID(1, nil)
},
method: http.MethodGet,
path: "/api/v1/groups/available",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": [
{
"id": 10,
"name": "Group One",
"description": "desc",
"platform": "anthropic",
"rate_multiplier": 1.5,
"peak_rate_enabled": false,
"peak_start": "",
"peak_end": "",
"peak_rate_multiplier": 1,
"is_exclusive": false,
"status": "active",
"subscription_type": "standard",
"daily_limit_usd": null,
"weekly_limit_usd": null,
"monthly_limit_usd": null,
"image_price_1k": null,
"image_price_2k": null,
"image_price_4k": null,
"video_price_480p": null,
"video_price_720p": null,
"video_price_1080p": null,
"web_search_price_per_call": null,
"allow_image_generation": false,
"allow_batch_image_generation": false,
"batch_image_discount_multiplier": 0,
"batch_image_hold_multiplier": 0,
"image_rate_independent": false,
"image_rate_multiplier": 0,
"video_rate_independent": false,
"video_rate_multiplier": 0,
"claude_code_only": false,
"allow_messages_dispatch": false,
"allow_live": false,
"fallback_group_id": null,
"fallback_group_id_on_invalid_request": null,
"require_oauth_only": false,
"require_privacy_set": false,
"max_reasoning_effort": "",
"reasoning_effort_mappings": null,
"rpm_limit": 0,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
]
}`,
},
{
name: "GET /api/v1/subscriptions",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
// 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
deps.userSubRepo.SetByUserID(1, []service.UserSubscription{
{
ID: 501,
UserID: 1,
GroupID: 10,
StartsAt: deps.now,
ExpiresAt: time.Date(2099, 1, 2, 3, 4, 5, 0, time.UTC), // 使用未来日期避免 normalizeSubscriptionStatus 标记为过期
Status: service.SubscriptionStatusActive,
DailyUsageUSD: 1.23,
WeeklyUsageUSD: 2.34,
MonthlyUsageUSD: 3.45,
AssignedBy: ptr(int64(999)),
AssignedAt: deps.now,
Notes: "admin-note",
CreatedAt: deps.now,
UpdatedAt: deps.now,
},
})
},
method: http.MethodGet,
path: "/api/v1/subscriptions",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": [
{
"id": 501,
"user_id": 1,
"group_id": 10,
"starts_at": "2025-01-02T03:04:05Z",
"expires_at": "2099-01-02T03:04:05Z",
"status": "active",
"daily_window_start": null,
"weekly_window_start": null,
"monthly_window_start": null,
"daily_usage_usd": 1.23,
"weekly_usage_usd": 2.34,
"monthly_usage_usd": 3.45,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
]
}`,
},
{
name: "GET /api/v1/redeem/history",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
// 普通用户兑换历史不应包含 notes 等内部字段。
deps.redeemRepo.SetByUser(1, []service.RedeemCode{
{
ID: 900,
Code: "CODE-123",
Type: service.RedeemTypeBalance,
Value: 1.25,
Status: service.StatusUsed,
UsedBy: ptr(int64(1)),
UsedAt: ptr(deps.now),
Notes: "internal-note",
CreatedAt: deps.now,
},
})
},
method: http.MethodGet,
path: "/api/v1/redeem/history",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": [
{
"id": 900,
"code": "CODE-123",
"type": "balance",
"value": 1.25,
"status": "used",
"used_by": 1,
"used_at": "2025-01-02T03:04:05Z",
"created_at": "2025-01-02T03:04:05Z",
"group_id": null,
"validity_days": 0
}
]
}`,
},
{
name: "GET /api/v1/usage/stats",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
deps.usageRepo.SetUserLogs(1, []service.UsageLog{
{
ID: 1,
UserID: 1,
APIKeyID: 100,
AccountID: 200,
Model: "claude-3",
InputTokens: 10,
OutputTokens: 20,
CacheCreationTokens: 1,
CacheReadTokens: 2,
TotalCost: 0.5,
ActualCost: 0.5,
DurationMs: ptr(100),
CreatedAt: deps.now,
},
{
ID: 2,
UserID: 1,
APIKeyID: 100,
AccountID: 200,
Model: "claude-3",
InputTokens: 5,
OutputTokens: 15,
TotalCost: 0.25,
ActualCost: 0.25,
DurationMs: ptr(300),
CreatedAt: deps.now,
},
})
},
method: http.MethodGet,
path: "/api/v1/usage/stats?start_date=2025-01-01&end_date=2025-01-02",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"total_requests": 2,
"total_input_tokens": 15,
"total_output_tokens": 35,
"total_cache_tokens": 3,
"total_cache_creation_tokens": 1,
"total_cache_read_tokens": 2,
"total_tokens": 53,
"total_cost": 0.75,
"total_actual_cost": 0.75,
"average_duration_ms": 200
}
}`,
},
{
name: "GET /api/v1/usage (paginated)",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
deps.usageRepo.SetUserLogs(1, []service.UsageLog{
{
ID: 1,
UserID: 1,
APIKeyID: 100,
AccountID: 200,
AccountRateMultiplier: ptr(0.5),
RequestID: "req_123",
Model: "claude-3",
InputTokens: 10,
OutputTokens: 20,
CacheCreationTokens: 1,
CacheReadTokens: 2,
TotalCost: 0.5,
ActualCost: 0.5,
RateMultiplier: 1,
BillingType: service.BillingTypeBalance,
Stream: true,
DurationMs: ptr(100),
FirstTokenMs: ptr(50),
CreatedAt: deps.now,
},
})
},
method: http.MethodGet,
path: "/api/v1/usage?page=1&page_size=10",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"items": [
{
"id": 1,
"user_id": 1,
"api_key_id": 100,
"account_id": 200,
"request_id": "req_123",
"model": "claude-3",
"request_type": "stream",
"openai_ws_mode": false,
"group_id": null,
"subscription_id": null,
"input_tokens": 10,
"output_tokens": 20,
"cache_creation_tokens": 1,
"cache_read_tokens": 2,
"cache_creation_5m_tokens": 0,
"cache_creation_1h_tokens": 0,
"input_cost": 0,
"output_cost": 0,
"cache_creation_cost": 0,
"cache_read_cost": 0,
"total_cost": 0.5,
"actual_cost": 0.5,
"rate_multiplier": 1,
"long_context_billing_applied": false,
"billing_type": 0,
"stream": true,
"duration_ms": 100,
"first_token_ms": 50,
"image_count": 0,
"image_size": null,
"image_input_size": null,
"image_output_size": null,
"image_input_tokens": 0,
"image_input_cost": 0,
"image_output_tokens": 0,
"image_output_cost": 0,
"image_size_source": null,
"image_size_breakdown": null,
"media_type": null,
"cache_ttl_overridden": false,
"created_at": "2025-01-02T03:04:05Z",
"user_agent": null
}
],
"total": 1,
"page": 1,
"page_size": 10,
"pages": 1
}
}`,
},
{
name: "GET /api/v1/admin/settings",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
deps.settingRepo.SetAll(map[string]string{
service.SettingKeyRegistrationEnabled: "true",
service.SettingKeyEmailVerifyEnabled: "false",
service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
service.SettingKeyPromoCodeEnabled: "true",
service.SettingKeySMTPHost: "smtp.example.com",
service.SettingKeySMTPPort: "587",
service.SettingKeySMTPUsername: "user",
service.SettingKeySMTPPassword: "secret",
service.SettingKeySMTPFrom: "no-reply@example.com",
service.SettingKeySMTPFromName: "Sub2API",
service.SettingKeySMTPUseTLS: "true",
service.SettingKeyTurnstileEnabled: "true",
service.SettingKeyTurnstileSiteKey: "site-key",
service.SettingKeyTurnstileSecretKey: "secret-key",
service.SettingKeyOIDCConnectEnabled: "false",
service.SettingKeyOIDCConnectProviderName: "OIDC",
service.SettingKeyOIDCConnectClientID: "",
service.SettingKeyOIDCConnectIssuerURL: "",
service.SettingKeyOIDCConnectDiscoveryURL: "",
service.SettingKeyOIDCConnectAuthorizeURL: "",
service.SettingKeyOIDCConnectTokenURL: "",
service.SettingKeyOIDCConnectUserInfoURL: "",
service.SettingKeyOIDCConnectJWKSURL: "",
service.SettingKeyOIDCConnectScopes: "openid email profile",
service.SettingKeyOIDCConnectRedirectURL: "",
service.SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
service.SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
service.SettingKeyOIDCConnectUsePKCE: "true",
service.SettingKeyOIDCConnectValidateIDToken: "true",
service.SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
service.SettingKeyOIDCConnectClockSkewSeconds: "120",
service.SettingKeyOIDCConnectRequireEmailVerified: "false",
service.SettingKeyOIDCConnectUserInfoEmailPath: "",
service.SettingKeyOIDCConnectUserInfoIDPath: "",
service.SettingKeyOIDCConnectUserInfoUsernamePath: "",
service.SettingKeySiteName: "Sub2API",
service.SettingKeySiteLogo: "",
service.SettingKeySiteSubtitle: "Subtitle",
service.SettingKeyAPIBaseURL: "https://api.example.com",
service.SettingKeyContactInfo: "support",
service.SettingKeyDocURL: "https://docs.example.com",
service.SettingKeyDefaultConcurrency: "5",
service.SettingKeyDefaultBalance: "1.25",
service.SettingKeyTableDefaultPageSize: "20",
service.SettingKeyTablePageSizeOptions: "[10,20,50,100]",
service.SettingKeyOpsMonitoringEnabled: "false",
service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
service.SettingKeyOpsQueryModeDefault: "auto",
service.SettingKeyOpsMetricsIntervalSeconds: "60",
service.SettingPaymentVisibleMethodAlipaySource: service.VisibleMethodSourceEasyPayAlipay,
service.SettingPaymentVisibleMethodWxpaySource: service.VisibleMethodSourceOfficialWechat,
service.SettingPaymentVisibleMethodAlipayEnabled: "true",
service.SettingPaymentVisibleMethodWxpayEnabled: "false",
service.SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05",
"openai_advanced_scheduler_enabled": "true",
service.SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
service.SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
})
},
method: http.MethodGet,
path: "/api/v1/admin/settings",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"registration_enabled": true,
"email_verify_enabled": false,
"registration_email_suffix_whitelist": [],
"promo_code_enabled": true,
"password_reset_enabled": false,
"frontend_url": "",
"totp_enabled": false,
"totp_encryption_key_configured": false,
"passkey_enabled": false,
"passkey_configured": false,
"passkey_rp_id": "",
"passkey_rp_origins": [],
"session_binding_enabled": false,
"step_up_enabled": false,
"audit_log_retention_days": 180,
"login_agreement_enabled": false,
"login_agreement_mode": "modal",
"login_agreement_updated_at": "2026-03-31",
"login_agreement_documents": [
{"id": "terms", "title": "服务条款", "content_md": ""},
{"id": "usage-policy", "title": "使用政策", "content_md": ""},
{"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
{"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
],
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_username": "user",
"smtp_password_configured": true,
"smtp_from_email": "no-reply@example.com",
"smtp_from_name": "Sub2API",
"smtp_use_tls": true,
"turnstile_enabled": true,
"turnstile_site_key": "site-key",
"turnstile_secret_key_configured": true,
"linuxdo_connect_enabled": false,
"linuxdo_connect_client_id": "",
"linuxdo_connect_client_secret_configured": false,
"linuxdo_connect_redirect_url": "",
"dingtalk_connect_enabled": false,
"dingtalk_connect_bypass_registration": false,
"dingtalk_connect_client_id": "",
"dingtalk_connect_client_secret_configured": false,
"dingtalk_connect_redirect_url": "",
"dingtalk_connect_internal_corp_id": "",
"dingtalk_connect_corp_restriction_policy": "",
"dingtalk_connect_sync_corp_email": false,
"dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
"dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
"dingtalk_connect_sync_dept": false,
"dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
"dingtalk_connect_sync_dept_attr_name": "钉钉部门",
"dingtalk_connect_sync_display_name": false,
"dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
"dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
"oidc_connect_enabled": false,
"oidc_connect_provider_name": "OIDC",
"oidc_connect_client_id": "",
"oidc_connect_client_secret_configured": false,
"oidc_connect_issuer_url": "",
"oidc_connect_discovery_url": "",
"oidc_connect_authorize_url": "",
"oidc_connect_token_url": "",
"oidc_connect_userinfo_url": "",
"oidc_connect_jwks_url": "",
"oidc_connect_scopes": "openid email profile",
"oidc_connect_redirect_url": "",
"oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
"oidc_connect_token_auth_method": "client_secret_post",
"oidc_connect_use_pkce": true,
"oidc_connect_validate_id_token": true,
"oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
"oidc_connect_clock_skew_seconds": 120,
"oidc_connect_require_email_verified": false,
"oidc_connect_userinfo_email_path": "",
"oidc_connect_userinfo_id_path": "",
"oidc_connect_userinfo_username_path": "",
"github_oauth_enabled": false,
"github_oauth_client_id": "",
"github_oauth_client_secret_configured": false,
"github_oauth_redirect_url": "",
"github_oauth_frontend_redirect_url": "/auth/oauth/callback",
"google_oauth_enabled": false,
"google_oauth_client_id": "",
"google_oauth_client_secret_configured": false,
"google_oauth_redirect_url": "",
"google_oauth_frontend_redirect_url": "/auth/oauth/callback",
"ops_monitoring_enabled": false,
"ops_realtime_monitoring_enabled": true,
"ops_query_mode_default": "auto",
"ops_metrics_interval_seconds": 60,
"site_name": "Sub2API",
"site_logo": "",
"site_subtitle": "Subtitle",
"api_base_url": "https://api.example.com",
"api_key_acl_trust_forwarded_ip": false,
"forwarded_client_ip_headers": [],
"contact_info": "support",
"doc_url": "https://docs.example.com",
"auth_source_default_email_balance": 0,
"auth_source_default_email_concurrency": 5,
"auth_source_default_email_subscriptions": [],
"auth_source_default_email_grant_on_signup": false,
"auth_source_default_email_grant_on_first_bind": false,
"auth_source_default_github_balance": 0,
"auth_source_default_github_concurrency": 5,
"auth_source_default_github_subscriptions": [],
"auth_source_default_github_grant_on_signup": false,
"auth_source_default_github_grant_on_first_bind": false,
"auth_source_default_google_balance": 0,
"auth_source_default_google_concurrency": 5,
"auth_source_default_google_subscriptions": [],
"auth_source_default_google_grant_on_signup": false,
"auth_source_default_google_grant_on_first_bind": false,
"auth_source_default_linuxdo_balance": 0,
"auth_source_default_linuxdo_concurrency": 5,
"auth_source_default_linuxdo_subscriptions": [],
"auth_source_default_linuxdo_grant_on_signup": false,
"auth_source_default_linuxdo_grant_on_first_bind": false,
"auth_source_default_oidc_balance": 0,
"auth_source_default_oidc_concurrency": 5,
"auth_source_default_oidc_subscriptions": [],
"auth_source_default_oidc_grant_on_signup": false,
"auth_source_default_oidc_grant_on_first_bind": false,
"auth_source_default_wechat_balance": 0,
"auth_source_default_wechat_concurrency": 5,
"auth_source_default_wechat_subscriptions": [],
"auth_source_default_wechat_grant_on_signup": false,
"auth_source_default_wechat_grant_on_first_bind": false,
"auth_source_default_dingtalk_balance": 0,
"auth_source_default_dingtalk_concurrency": 5,
"auth_source_default_dingtalk_subscriptions": [],
"auth_source_default_dingtalk_grant_on_signup": false,
"auth_source_default_dingtalk_grant_on_first_bind": false,
"force_email_on_third_party_signup": false,
"default_concurrency": 5,
"default_balance": 1.25,
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
"auth_source_default_linuxdo_platform_quotas": null,
"auth_source_default_oidc_platform_quotas": null,
"auth_source_default_wechat_platform_quotas": null,
"auth_source_default_dingtalk_platform_quotas": null,
"affiliate_rebate_rate": 20,
"affiliate_rebate_freeze_hours": 0,
"affiliate_rebate_duration_days": 0,
"affiliate_rebate_per_invitee_cap": 0,
"affiliate_admin_recharge_enabled": false,
"default_user_rpm_limit": 0,
"default_subscriptions": [],
"enable_model_fallback": false,
"fallback_model_anthropic": "claude-3-5-sonnet-20241022",
"fallback_model_antigravity": "gemini-2.5-pro",
"fallback_model_gemini": "gemini-2.5-pro",
"fallback_model_openai": "gpt-4o",
"enable_identity_patch": true,
"identity_patch_prompt": "",
"invitation_code_enabled": false,
"home_content": "",
"hide_ccs_import_button": false,
"purchase_subscription_enabled": false,
"purchase_subscription_url": "",
"table_default_page_size": 20,
"table_page_size_options": [10, 20, 50, 100],
"min_claude_code_version": "",
"max_claude_code_version": "",
"min_codex_version": "",
"max_codex_version": "",
"codex_cli_only_blacklist": "",
"codex_cli_only_whitelist": "",
"compact_home_enabled": false,
"codex_cli_only_allow_app_server_clients": false,
"codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
"allow_ungrouped_key_scheduling": false,
"backend_mode_enabled": false,
"enable_cch_signing": false,
"enable_claude_oauth_system_prompt_injection": true,
"claude_oauth_system_prompt": "",
"claude_oauth_system_prompt_blocks": "",
"enable_anthropic_cache_ttl_1h_injection": false,
"rewrite_message_cache_control": false,
"enable_client_dateline_normalization": true,
"antigravity_user_agent_version": "",
"enable_fingerprint_unification": true,
"enable_metadata_passthrough": false,
"web_search_emulation_enabled": false,
"payment_visible_method_alipay_source": "easypay_alipay",
"payment_visible_method_wxpay_source": "official_wxpay",
"payment_visible_method_alipay_enabled": true,
"payment_visible_method_wxpay_enabled": false,
"openai_low_upstream_rate_priority_enabled": true,
"openai_oauth_scheduling_rate_multiplier": 0.05,
"openai_advanced_scheduler_enabled": true,
"openai_advanced_scheduler_sticky_weighted_enabled": false,
"openai_advanced_scheduler_subscription_priority_enabled": false,
"openai_advanced_scheduler_lb_top_k": "",
"openai_advanced_scheduler_weight_priority": "",
"openai_advanced_scheduler_weight_load": "",
"openai_advanced_scheduler_weight_queue": "",
"openai_advanced_scheduler_weight_error_rate": "",
"openai_advanced_scheduler_weight_ttft": "",
"openai_advanced_scheduler_weight_reset": "",
"openai_advanced_scheduler_weight_quota_headroom": "",
"openai_advanced_scheduler_weight_upstream_cost": "",
"openai_advanced_scheduler_weight_previous_response": "",
"openai_advanced_scheduler_weight_session_sticky": "",
"openai_advanced_scheduler_effective_lb_top_k": "7",
"openai_advanced_scheduler_effective_weight_priority": "1",
"openai_advanced_scheduler_effective_weight_load": "1",
"openai_advanced_scheduler_effective_weight_queue": "0.7",
"openai_advanced_scheduler_effective_weight_error_rate": "0.8",
"openai_advanced_scheduler_effective_weight_ttft": "0.5",
"openai_advanced_scheduler_effective_weight_reset": "0",
"openai_advanced_scheduler_effective_weight_quota_headroom": "0",
"openai_advanced_scheduler_effective_weight_upstream_cost": "0",
"openai_advanced_scheduler_effective_weight_previous_response": "5",
"openai_advanced_scheduler_effective_weight_session_sticky": "3",
"openai_codex_user_agent": "",
"openai_fast_policy_settings": {
"rules": []
},
"custom_menu_items": [],
"custom_endpoints": [],
"payment_enabled": false,
"payment_min_amount": 0,
"payment_max_amount": 0,
"payment_daily_limit": 0,
"payment_order_timeout_minutes": 0,
"payment_max_pending_orders": 0,
"payment_balance_disabled": false,
"payment_balance_recharge_multiplier": 0,
"payment_subscription_usd_to_cny_rate": 0,
"payment_recharge_fee_rate": 0,
"payment_load_balance_strategy": "",
"payment_product_name_prefix": "",
"payment_product_name_suffix": "",
"payment_help_image_url": "",
"payment_help_text": "",
"payment_enabled_types": null,
"payment_cancel_rate_limit_enabled": false,
"payment_cancel_rate_limit_max": 0,
"payment_cancel_rate_limit_window": 0,
"payment_cancel_rate_limit_unit": "",
"payment_cancel_rate_limit_window_mode": "",
"payment_alipay_force_qrcode": false,
"payment_alipay_mobile_precreate_deep_link": false,
"balance_low_notify_enabled": false,
"account_quota_notify_enabled": false,
"subscription_expiry_notify_enabled": true,
"balance_low_notify_threshold": 0,
"balance_low_notify_recharge_url": "",
"account_quota_notify_emails": [],
"channel_monitor_enabled": true,
"channel_monitor_default_interval_seconds": 60,
"available_channels_enabled": false,
"model_plaza_enabled": false,
"model_plaza_require_auth": false,
"model_plaza_description": "",
"risk_control_enabled": false,
"cyber_session_block_enabled": false,
"cyber_session_block_ttl_seconds": 3600,
"affiliate_enabled": false,
"wechat_connect_enabled": false,
"wechat_connect_app_id": "",
"wechat_connect_app_secret_configured": false,
"wechat_connect_mode": "open",
"wechat_connect_open_enabled": false,
"wechat_connect_open_app_id": "",
"wechat_connect_open_app_secret_configured": false,
"wechat_connect_mp_enabled": false,
"wechat_connect_mp_app_id": "",
"wechat_connect_mp_app_secret_configured": false,
"wechat_connect_mobile_enabled": false,
"wechat_connect_mobile_app_id": "",
"wechat_connect_mobile_app_secret_configured": false,
"wechat_connect_redirect_url": "",
"wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
"wechat_connect_scopes": "snsapi_login",
"allow_user_view_error_requests": false
}
}`,
},
{
name: "GET /api/v1/admin/settings falls back to config oauth defaults",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
deps.cfg.OIDC = config.OIDCConnectConfig{
Enabled: true,
ProviderName: "ConfigOIDC",
ClientID: "oidc-config-client",
ClientSecret: "oidc-config-secret",
IssuerURL: "https://issuer.example.com",
RedirectURL: "https://api.example.com/api/v1/auth/oauth/oidc/callback",
FrontendRedirectURL: "/auth/oidc/callback",
Scopes: "openid email profile",
TokenAuthMethod: "client_secret_post",
UsePKCE: true,
ValidateIDToken: true,
AllowedSigningAlgs: "RS256,ES256,PS256",
ClockSkewSeconds: 120,
}
deps.cfg.WeChat = config.WeChatConnectConfig{
Enabled: true,
OpenEnabled: true,
OpenAppID: "wx-open-config",
OpenAppSecret: "wx-open-secret",
Mode: "open",
Scopes: "snsapi_login",
FrontendRedirectURL: "/auth/wechat/callback",
}
deps.settingRepo.SetAll(map[string]string{
service.SettingKeyRegistrationEnabled: "true",
service.SettingKeyEmailVerifyEnabled: "false",
service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
})
},
method: http.MethodGet,
path: "/api/v1/admin/settings",
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"registration_enabled": true,
"email_verify_enabled": false,
"registration_email_suffix_whitelist": [],
"promo_code_enabled": true,
"password_reset_enabled": false,
"frontend_url": "",
"invitation_code_enabled": false,
"totp_enabled": false,
"totp_encryption_key_configured": false,
"passkey_enabled": false,
"passkey_configured": false,
"passkey_rp_id": "",
"passkey_rp_origins": [],
"session_binding_enabled": false,
"step_up_enabled": false,
"audit_log_retention_days": 180,
"login_agreement_enabled": false,
"login_agreement_mode": "modal",
"login_agreement_updated_at": "2026-03-31",
"login_agreement_documents": [
{"id": "terms", "title": "服务条款", "content_md": ""},
{"id": "usage-policy", "title": "使用政策", "content_md": ""},
{"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
{"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
],
"smtp_host": "",
"smtp_port": 587,
"smtp_username": "",
"smtp_password_configured": false,
"smtp_from_email": "",
"smtp_from_name": "",
"smtp_use_tls": false,
"turnstile_enabled": false,
"turnstile_site_key": "",
"turnstile_secret_key_configured": false,
"linuxdo_connect_enabled": false,
"linuxdo_connect_client_id": "",
"linuxdo_connect_client_secret_configured": false,
"linuxdo_connect_redirect_url": "",
"dingtalk_connect_enabled": false,
"dingtalk_connect_bypass_registration": false,
"dingtalk_connect_client_id": "",
"dingtalk_connect_client_secret_configured": false,
"dingtalk_connect_redirect_url": "",
"dingtalk_connect_internal_corp_id": "",
"dingtalk_connect_corp_restriction_policy": "",
"dingtalk_connect_sync_corp_email": false,
"dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
"dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
"dingtalk_connect_sync_dept": false,
"dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
"dingtalk_connect_sync_dept_attr_name": "钉钉部门",
"dingtalk_connect_sync_display_name": false,
"dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
"dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
"oidc_connect_enabled": true,
"oidc_connect_provider_name": "ConfigOIDC",
"oidc_connect_client_id": "oidc-config-client",
"oidc_connect_client_secret_configured": true,
"oidc_connect_issuer_url": "https://issuer.example.com",
"oidc_connect_discovery_url": "",
"oidc_connect_authorize_url": "",
"oidc_connect_token_url": "",
"oidc_connect_userinfo_url": "",
"oidc_connect_jwks_url": "",
"oidc_connect_scopes": "openid email profile",
"oidc_connect_redirect_url": "https://api.example.com/api/v1/auth/oauth/oidc/callback",
"oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
"oidc_connect_token_auth_method": "client_secret_post",
"oidc_connect_use_pkce": true,
"oidc_connect_validate_id_token": true,
"oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
"oidc_connect_clock_skew_seconds": 120,
"oidc_connect_require_email_verified": false,
"oidc_connect_userinfo_email_path": "",
"oidc_connect_userinfo_id_path": "",
"oidc_connect_userinfo_username_path": "",
"github_oauth_enabled": false,
"github_oauth_client_id": "",
"github_oauth_client_secret_configured": false,
"github_oauth_redirect_url": "",
"github_oauth_frontend_redirect_url": "/auth/oauth/callback",
"google_oauth_enabled": false,
"google_oauth_client_id": "",
"google_oauth_client_secret_configured": false,
"google_oauth_redirect_url": "",
"google_oauth_frontend_redirect_url": "/auth/oauth/callback",
"site_name": "Sub2API",
"site_logo": "",
"site_subtitle": "Subscription to API Conversion Platform",
"api_base_url": "",
"api_key_acl_trust_forwarded_ip": false,
"forwarded_client_ip_headers": [],
"contact_info": "",
"doc_url": "",
"home_content": "",
"hide_ccs_import_button": false,
"purchase_subscription_enabled": false,
"purchase_subscription_url": "",
"table_default_page_size": 20,
"table_page_size_options": [10, 20, 50],
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
"auth_source_default_linuxdo_platform_quotas": null,
"auth_source_default_oidc_platform_quotas": null,
"auth_source_default_wechat_platform_quotas": null,
"auth_source_default_dingtalk_platform_quotas": null,
"custom_menu_items": [],
"custom_endpoints": [],
"default_concurrency": 0,
"default_balance": 0,
"affiliate_rebate_rate": 20,
"affiliate_rebate_freeze_hours": 0,
"affiliate_rebate_duration_days": 0,
"affiliate_rebate_per_invitee_cap": 0,
"affiliate_admin_recharge_enabled": false,
"default_user_rpm_limit": 0,
"default_subscriptions": [],
"enable_model_fallback": false,
"fallback_model_anthropic": "claude-3-5-sonnet-20241022",
"fallback_model_openai": "gpt-4o",
"fallback_model_gemini": "gemini-2.5-pro",
"fallback_model_antigravity": "gemini-2.5-pro",
"enable_identity_patch": true,
"identity_patch_prompt": "",
"ops_monitoring_enabled": false,
"ops_realtime_monitoring_enabled": true,
"ops_query_mode_default": "auto",
"ops_metrics_interval_seconds": 60,
"min_claude_code_version": "",
"max_claude_code_version": "",
"allow_ungrouped_key_scheduling": false,
"backend_mode_enabled": false,
"enable_fingerprint_unification": true,
"enable_metadata_passthrough": false,
"enable_cch_signing": false,
"enable_claude_oauth_system_prompt_injection": true,
"claude_oauth_system_prompt": "",
"claude_oauth_system_prompt_blocks": "",
"enable_anthropic_cache_ttl_1h_injection": false,
"rewrite_message_cache_control": false,
"enable_client_dateline_normalization": true,
"antigravity_user_agent_version": "",
"min_codex_version": "",
"max_codex_version": "",
"codex_cli_only_blacklist": "",
"codex_cli_only_whitelist": "",
"compact_home_enabled": false,
"codex_cli_only_allow_app_server_clients": false,
"codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
"web_search_emulation_enabled": false,
"payment_visible_method_alipay_source": "",
"payment_visible_method_wxpay_source": "",
"payment_visible_method_alipay_enabled": false,
"payment_visible_method_wxpay_enabled": false,
"openai_low_upstream_rate_priority_enabled": false,
"openai_oauth_scheduling_rate_multiplier": 1,
"openai_advanced_scheduler_enabled": false,
"openai_advanced_scheduler_sticky_weighted_enabled": false,
"openai_advanced_scheduler_subscription_priority_enabled": false,
"openai_advanced_scheduler_lb_top_k": "",
"openai_advanced_scheduler_weight_priority": "",
"openai_advanced_scheduler_weight_load": "",
"openai_advanced_scheduler_weight_queue": "",
"openai_advanced_scheduler_weight_error_rate": "",
"openai_advanced_scheduler_weight_ttft": "",
"openai_advanced_scheduler_weight_reset": "",
"openai_advanced_scheduler_weight_quota_headroom": "",
"openai_advanced_scheduler_weight_upstream_cost": "",
"openai_advanced_scheduler_weight_previous_response": "",
"openai_advanced_scheduler_weight_session_sticky": "",
"openai_advanced_scheduler_effective_lb_top_k": "7",
"openai_advanced_scheduler_effective_weight_priority": "1",
"openai_advanced_scheduler_effective_weight_load": "1",
"openai_advanced_scheduler_effective_weight_queue": "0.7",
"openai_advanced_scheduler_effective_weight_error_rate": "0.8",
"openai_advanced_scheduler_effective_weight_ttft": "0.5",
"openai_advanced_scheduler_effective_weight_reset": "0",
"openai_advanced_scheduler_effective_weight_quota_headroom": "0",
"openai_advanced_scheduler_effective_weight_upstream_cost": "0",
"openai_advanced_scheduler_effective_weight_previous_response": "5",
"openai_advanced_scheduler_effective_weight_session_sticky": "3",
"openai_codex_user_agent": "",
"openai_fast_policy_settings": {
"rules": []
},
"payment_enabled": false,
"payment_min_amount": 0,
"payment_max_amount": 0,
"payment_daily_limit": 0,
"payment_order_timeout_minutes": 0,
"payment_max_pending_orders": 0,
"payment_enabled_types": null,
"payment_balance_disabled": false,
"payment_balance_recharge_multiplier": 0,
"payment_subscription_usd_to_cny_rate": 0,
"payment_recharge_fee_rate": 0,
"payment_load_balance_strategy": "",
"payment_product_name_prefix": "",
"payment_product_name_suffix": "",
"payment_help_image_url": "",
"payment_help_text": "",
"payment_cancel_rate_limit_enabled": false,
"payment_cancel_rate_limit_max": 0,
"payment_cancel_rate_limit_window": 0,
"payment_cancel_rate_limit_unit": "",
"payment_cancel_rate_limit_window_mode": "",
"payment_alipay_force_qrcode": false,
"payment_alipay_mobile_precreate_deep_link": false,
"balance_low_notify_enabled": false,
"account_quota_notify_enabled": false,
"subscription_expiry_notify_enabled": true,
"balance_low_notify_threshold": 0,
"balance_low_notify_recharge_url": "",
"account_quota_notify_emails": [],
"channel_monitor_enabled": true,
"channel_monitor_default_interval_seconds": 60,
"available_channels_enabled": false,
"model_plaza_enabled": false,
"model_plaza_require_auth": false,
"model_plaza_description": "",
"risk_control_enabled": false,
"cyber_session_block_enabled": false,
"cyber_session_block_ttl_seconds": 3600,
"affiliate_enabled": false,
"wechat_connect_enabled": true,
"wechat_connect_app_id": "wx-open-config",
"wechat_connect_app_secret_configured": true,
"wechat_connect_mode": "open",
"wechat_connect_open_enabled": true,
"wechat_connect_open_app_id": "wx-open-config",
"wechat_connect_open_app_secret_configured": true,
"wechat_connect_mp_enabled": false,
"wechat_connect_mp_app_id": "wx-open-config",
"wechat_connect_mp_app_secret_configured": true,
"wechat_connect_mobile_enabled": false,
"wechat_connect_mobile_app_id": "wx-open-config",
"wechat_connect_mobile_app_secret_configured": true,
"wechat_connect_redirect_url": "",
"wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
"wechat_connect_scopes": "snsapi_login",
"auth_source_default_email_balance": 0,
"auth_source_default_email_concurrency": 5,
"auth_source_default_email_subscriptions": [],
"auth_source_default_email_grant_on_signup": false,
"auth_source_default_email_grant_on_first_bind": false,
"auth_source_default_github_balance": 0,
"auth_source_default_github_concurrency": 5,
"auth_source_default_github_subscriptions": [],
"auth_source_default_github_grant_on_signup": false,
"auth_source_default_github_grant_on_first_bind": false,
"auth_source_default_google_balance": 0,
"auth_source_default_google_concurrency": 5,
"auth_source_default_google_subscriptions": [],
"auth_source_default_google_grant_on_signup": false,
"auth_source_default_google_grant_on_first_bind": false,
"auth_source_default_linuxdo_balance": 0,
"auth_source_default_linuxdo_concurrency": 5,
"auth_source_default_linuxdo_subscriptions": [],
"auth_source_default_linuxdo_grant_on_signup": false,
"auth_source_default_linuxdo_grant_on_first_bind": false,
"auth_source_default_oidc_balance": 0,
"auth_source_default_oidc_concurrency": 5,
"auth_source_default_oidc_subscriptions": [],
"auth_source_default_oidc_grant_on_signup": false,
"auth_source_default_oidc_grant_on_first_bind": false,
"auth_source_default_wechat_balance": 0,
"auth_source_default_wechat_concurrency": 5,
"auth_source_default_wechat_subscriptions": [],
"auth_source_default_wechat_grant_on_signup": false,
"auth_source_default_wechat_grant_on_first_bind": false,
"auth_source_default_dingtalk_balance": 0,
"auth_source_default_dingtalk_concurrency": 5,
"auth_source_default_dingtalk_subscriptions": [],
"auth_source_default_dingtalk_grant_on_signup": false,
"auth_source_default_dingtalk_grant_on_first_bind": false,
"force_email_on_third_party_signup": false,
"allow_user_view_error_requests": false
}
}`,
},
{
name: "POST /api/v1/admin/accounts/bulk-update",
method: http.MethodPost,
path: "/api/v1/admin/accounts/bulk-update",
body: `{"account_ids":[101,102],"schedulable":false}`,
headers: map[string]string{
"Content-Type": "application/json",
},
wantStatus: http.StatusOK,
wantJSON: `{
"code": 0,
"message": "success",
"data": {
"success": 2,
"failed": 0,
"success_ids": [101, 102],
"failed_ids": [],
"results": [
{"account_id": 101, "success": true},
{"account_id": 102, "success": true}
]
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps := newContractDeps(t)
if tt.setup != nil {
tt.setup(t, deps)
}
status, body := doRequest(t, deps.router, tt.method, tt.path, tt.body, tt.headers)
require.Equal(t, tt.wantStatus, status)
require.JSONEq(t, tt.wantJSON, body)
})
}
}
type contractDeps struct {
now time.Time
router http.Handler
cfg *config.Config
apiKeyRepo *stubApiKeyRepo
groupRepo *stubGroupRepo
userSubRepo *stubUserSubscriptionRepo
usageRepo *stubUsageLogRepo
settingRepo *stubSettingRepo
redeemRepo *stubRedeemCodeRepo
}
func newContractDeps(t *testing.T) *contractDeps {
t.Helper()
now := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
userRepo := &stubUserRepo{
users: map[int64]*service.User{
1: {
ID: 1,
Email: "alice@example.com",
Username: "alice",
Notes: "hello",
Role: service.RoleUser,
Balance: 12.5,
Concurrency: 5,
Status: service.StatusActive,
AllowedGroups: nil,
CreatedAt: now,
UpdatedAt: now,
},
},
}
apiKeyRepo := newStubApiKeyRepo(now)
apiKeyCache := stubApiKeyCache{}
groupRepo := &stubGroupRepo{}
userSubRepo := &stubUserSubscriptionRepo{}
accountRepo := stubAccountRepo{}
proxyRepo := stubProxyRepo{}
redeemRepo := &stubRedeemCodeRepo{}
cfg := &config.Config{
Default: config.DefaultConfig{
APIKeyPrefix: "sk-",
},
RunMode: config.RunModeStandard,
}
userService := service.NewUserService(userRepo, nil, nil, nil)
apiKeyService := service.NewAPIKeyService(apiKeyRepo, userRepo, groupRepo, userSubRepo, nil, apiKeyCache, cfg)
usageRepo := newStubUsageLogRepo()
usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil, nil, cfg)
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil, nil)
redeemHandler := handler.NewRedeemHandler(redeemService)
settingRepo := newStubSettingRepo()
settingService := service.NewSettingService(settingRepo, cfg)
adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil, nil)
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
usageHandler := handler.NewUsageHandler(usageService, apiKeyService, nil, nil)
adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil, nil, nil, nil, nil)
adminAccountHandler := adminhandler.NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
jwtAuth := func(c *gin.Context) {
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
UserID: 1,
Concurrency: 5,
})
c.Set(string(middleware.ContextKeyUserRole), service.RoleUser)
c.Next()
}
adminAuth := func(c *gin.Context) {
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
UserID: 1,
Concurrency: 5,
})
c.Set(string(middleware.ContextKeyUserRole), service.RoleAdmin)
c.Next()
}
r := gin.New()
v1 := r.Group("/api/v1")
v1Auth := v1.Group("")
v1Auth.Use(jwtAuth)
v1Auth.GET("/auth/me", authHandler.GetCurrentUser)
v1Keys := v1.Group("")
v1Keys.Use(jwtAuth)
v1Keys.GET("/keys", apiKeyHandler.List)
v1Keys.POST("/keys", apiKeyHandler.Create)
v1Keys.GET("/groups/available", apiKeyHandler.GetAvailableGroups)
v1Usage := v1.Group("")
v1Usage.Use(jwtAuth)
v1Usage.GET("/usage", usageHandler.List)
v1Usage.GET("/usage/stats", usageHandler.Stats)
v1Subs := v1.Group("")
v1Subs.Use(jwtAuth)
v1Subs.GET("/subscriptions", subscriptionHandler.List)
v1Redeem := v1.Group("")
v1Redeem.Use(jwtAuth)
v1Redeem.GET("/redeem/history", redeemHandler.GetHistory)
v1Admin := v1.Group("/admin")
v1Admin.Use(adminAuth)
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate)
return &contractDeps{
now: now,
router: r,
cfg: cfg,
apiKeyRepo: apiKeyRepo,
groupRepo: groupRepo,
userSubRepo: userSubRepo,
usageRepo: usageRepo,
settingRepo: settingRepo,
redeemRepo: redeemRepo,
}
}
func doRequest(t *testing.T, router http.Handler, method, path, body string, headers map[string]string) (int, string) {
t.Helper()
req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
for k, v := range headers {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
respBody, err := io.ReadAll(w.Result().Body)
require.NoError(t, err)
return w.Result().StatusCode, string(respBody)
}
func ptr[T any](v T) *T { return &v }
type stubUserRepo struct {
users map[int64]*service.User
}
func (r *stubUserRepo) Create(ctx context.Context, user *service.User) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) CreateWithEmailAliasGuard(ctx context.Context, user *service.User) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) GetByID(ctx context.Context, id int64) (*service.User, error) {
user, ok := r.users[id]
if !ok {
return nil, service.ErrUserNotFound
}
clone := *user
return &clone, nil
}
func (r *stubUserRepo) GetByEmail(ctx context.Context, email string) (*service.User, error) {
for _, user := range r.users {
if user.Email == email {
clone := *user
return &clone, nil
}
}
return nil, service.ErrUserNotFound
}
func (r *stubUserRepo) GetFirstAdmin(ctx context.Context) (*service.User, error) {
for _, user := range r.users {
if user.Role == service.RoleAdmin && user.Status == service.StatusActive {
clone := *user
return &clone, nil
}
}
return nil, service.ErrUserNotFound
}
func (r *stubUserRepo) Update(ctx context.Context, user *service.User, fields service.UserUpdateFields) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) GetUserAvatar(ctx context.Context, userID int64) (*service.UserAvatar, error) {
return nil, nil
}
func (r *stubUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
return nil, errors.New("not implemented")
}
func (r *stubUserRepo) DeleteUserAvatar(ctx context.Context, userID int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUserRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUserRepo) UpdateBalance(ctx context.Context, id int64, amount float64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) DeductBalance(ctx context.Context, id int64, amount float64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) AdjustBalance(ctx context.Context, id int64, delta float64) (service.BalanceChange, error) {
return service.BalanceChange{}, errors.New("not implemented")
}
func (r *stubUserRepo) SetBalance(ctx context.Context, id int64, value float64) (service.BalanceChange, error) {
return service.BalanceChange{}, errors.New("not implemented")
}
func (r *stubUserRepo) UpdateConcurrency(ctx context.Context, id int64, amount int) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) BatchSetConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
func (r *stubUserRepo) BatchAddConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
func (r *stubUserRepo) BatchUpdateLimits(context.Context, []int64, *int, *int) (int, error) {
return 0, nil
}
func (r *stubUserRepo) ExistsByEmail(ctx context.Context, email string) (bool, error) {
return false, errors.New("not implemented")
}
func (r *stubUserRepo) ExistsByEmailAlias(ctx context.Context, email string) (bool, error) {
return false, errors.New("not implemented")
}
func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) {
return 0, errors.New("not implemented")
}
func (r *stubUserRepo) RemoveGroupFromUserAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) AddGroupToAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) ListUserAuthIdentities(ctx context.Context, userID int64) ([]service.UserAuthIdentityRecord, error) {
return nil, nil
}
func (r *stubUserRepo) UnbindUserAuthProvider(context.Context, int64, string) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error) {
return map[int64]*time.Time{}, nil
}
func (r *stubUserRepo) GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error) {
return nil, nil
}
func (r *stubUserRepo) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
return nil
}
func (r *stubUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) EnableTotp(ctx context.Context, userID int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) DisableTotp(ctx context.Context, userID int64) error {
return errors.New("not implemented")
}
func (r *stubUserRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.User, error) {
panic("unexpected GetByIDIncludeDeleted call")
}
type stubApiKeyCache struct{}
func (stubApiKeyCache) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) {
return 0, nil
}
func (stubApiKeyCache) IncrementCreateAttemptCount(ctx context.Context, userID int64) error {
return nil
}
func (stubApiKeyCache) DeleteCreateAttemptCount(ctx context.Context, userID int64) error {
return nil
}
func (stubApiKeyCache) IncrementDailyUsage(ctx context.Context, apiKey string) error {
return nil
}
func (stubApiKeyCache) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error {
return nil
}
func (stubApiKeyCache) GetAuthCache(ctx context.Context, key string) (*service.APIKeyAuthCacheEntry, error) {
return nil, nil
}
func (stubApiKeyCache) SetAuthCache(ctx context.Context, key string, entry *service.APIKeyAuthCacheEntry, ttl time.Duration) error {
return nil
}
func (stubApiKeyCache) DeleteAuthCache(ctx context.Context, key string) error {
return nil
}
func (stubApiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error {
return nil
}
func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error {
return nil
}
type stubGroupRepo struct {
active []service.Group
}
func (r *stubGroupRepo) SetActive(groups []service.Group) {
r.active = append([]service.Group(nil), groups...)
}
func (stubGroupRepo) Create(ctx context.Context, group *service.Group) error {
return errors.New("not implemented")
}
func (stubGroupRepo) GetByID(ctx context.Context, id int64) (*service.Group, error) {
return nil, service.ErrGroupNotFound
}
func (stubGroupRepo) GetByIDLite(ctx context.Context, id int64) (*service.Group, error) {
return nil, service.ErrGroupNotFound
}
func (stubGroupRepo) Update(ctx context.Context, group *service.Group) error {
return errors.New("not implemented")
}
func (stubGroupRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (stubGroupRepo) DeleteCascade(ctx context.Context, id int64) ([]int64, error) {
return nil, errors.New("not implemented")
}
func (stubGroupRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Group, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) {
return append([]service.Group(nil), r.active...), nil
}
func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) {
out := make([]service.Group, 0, len(r.active))
for i := range r.active {
g := r.active[i]
if g.Platform == platform {
out = append(out, g)
}
}
return out, nil
}
func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) {
return false, errors.New("not implemented")
}
func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, int64, error) {
return 0, 0, errors.New("not implemented")
}
func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
return 0, errors.New("not implemented")
}
func (stubGroupRepo) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error {
return errors.New("not implemented")
}
func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) {
return nil, errors.New("not implemented")
}
func (stubGroupRepo) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error {
return nil
}
func (stubGroupRepo) FindByDuplicateOperationID(ctx context.Context, operationID string) (*service.Group, error) {
return nil, nil
}
func (stubGroupRepo) CreateFromSource(ctx context.Context, group *service.Group, sourceGroupID int64) error {
return errors.New("not implemented")
}
type stubAccountRepo struct {
bulkUpdateIDs []int64
}
func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) CreateWithAccountGroups(ctx context.Context, account *service.Account, groups []service.AccountGroup) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) GetByID(ctx context.Context, id int64) (*service.Account, error) {
return nil, service.ErrAccountNotFound
}
func (s *stubAccountRepo) GetByIDs(ctx context.Context, ids []int64) ([]*service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ExistsByID(ctx context.Context, id int64) (bool, error) {
return false, errors.New("not implemented")
}
func (s *stubAccountRepo) GetByCRSAccountID(ctx context.Context, crsAccountID string) (*service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) FindByExtraField(ctx context.Context, key string, value any) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) Update(ctx context.Context, account *service.Account) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) UpdateWithAccountBillingSettings(
ctx context.Context,
account *service.Account,
probeEnabled *bool,
rateSyncEnabled *bool,
rateMultiplier *float64,
) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Account, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListAllWithFilters(context.Context, string, string, string, string, int64, string) ([]service.Account, error) {
return nil, nil
}
func (s *stubAccountRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListActive(ctx context.Context) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListOAuthRefreshCandidates(ctx context.Context) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) SetError(ctx context.Context, id int64, errorMsg string) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ClearError(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
return 0, errors.New("not implemented")
}
func (s *stubAccountRepo) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ListShadowsByParent(ctx context.Context, parentID int64) ([]*service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulable(ctx context.Context) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatforms(ctx context.Context, groupID int64, platforms []string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableUngroupedByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListSchedulableUngroupedByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]service.Account, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) SetModelRateLimit(ctx context.Context, id int64, scope string, resetAt time.Time, reason ...string) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) SetOverloaded(ctx context.Context, id int64, until time.Time) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) SetTempUnschedulable(ctx context.Context, id int64, until time.Time, reason string) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ClearTempUnschedulable(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ClearRateLimit(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ClearModelRateLimits(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) UpdateSessionWindowEnd(ctx context.Context, id int64, end time.Time) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) ResetQuotaUsed(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
s.bulkUpdateIDs = append([]int64{}, ids...)
return int64(len(ids)), nil
}
func (s *stubAccountRepo) ListCRSAccountIDs(ctx context.Context) (map[string]int64, error) {
return nil, errors.New("not implemented")
}
func (s *stubAccountRepo) RevertProxyFallback(ctx context.Context, accountID int64) error {
return nil
}
type stubProxyRepo struct{}
func (stubProxyRepo) Create(ctx context.Context, proxy *service.Proxy) error {
return errors.New("not implemented")
}
func (stubProxyRepo) GetByID(ctx context.Context, id int64) (*service.Proxy, error) {
return nil, service.ErrProxyNotFound
}
func (stubProxyRepo) ListByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
return nil, errors.New("not implemented")
}
func (stubProxyRepo) Update(ctx context.Context, proxy *service.Proxy) error {
return errors.New("not implemented")
}
func (stubProxyRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (stubProxyRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Proxy, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubProxyRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.Proxy, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubProxyRepo) ListWithFiltersAndAccountCount(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubProxyRepo) ListActive(ctx context.Context) ([]service.Proxy, error) {
return nil, errors.New("not implemented")
}
func (stubProxyRepo) ListActiveWithAccountCount(ctx context.Context) ([]service.ProxyWithAccountCount, error) {
return nil, errors.New("not implemented")
}
func (stubProxyRepo) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) {
return false, errors.New("not implemented")
}
func (stubProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
return 0, errors.New("not implemented")
}
func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]service.ProxyAccountSummary, error) {
return nil, errors.New("not implemented")
}
func (stubProxyRepo) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) {
return 0, nil
}
func (stubProxyRepo) ListAllForFallback(ctx context.Context) ([]service.Proxy, error) {
return nil, nil
}
func (stubProxyRepo) CountExpired(ctx context.Context) (int64, error) {
return 0, nil
}
func (stubProxyRepo) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) {
return 0, nil
}
type stubRedeemCodeRepo struct {
byUser map[int64][]service.RedeemCode
}
func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) {
if r.byUser == nil {
r.byUser = make(map[int64][]service.RedeemCode)
}
r.byUser[userID] = append([]service.RedeemCode(nil), codes...)
}
func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
return errors.New("not implemented")
}
func (stubRedeemCodeRepo) CreateBatch(ctx context.Context, codes []service.RedeemCode) error {
return errors.New("not implemented")
}
func (stubRedeemCodeRepo) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
return nil, service.ErrRedeemCodeNotFound
}
func (stubRedeemCodeRepo) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
return nil, service.ErrRedeemCodeNotFound
}
func (stubRedeemCodeRepo) Update(ctx context.Context, code *service.RedeemCode) error {
return errors.New("not implemented")
}
func (stubRedeemCodeRepo) BatchUpdate(ctx context.Context, ids []int64, fields service.RedeemCodeBatchUpdateFields) (int64, error) {
return int64(len(ids)), nil
}
func (stubRedeemCodeRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (stubRedeemCodeRepo) Use(ctx context.Context, id, userID int64) error {
return errors.New("not implemented")
}
func (stubRedeemCodeRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
if r.byUser == nil {
return nil, nil
}
codes := r.byUser[userID]
if limit > 0 && len(codes) > limit {
codes = codes[:limit]
}
return append([]service.RedeemCode(nil), codes...), nil
}
func (stubRedeemCodeRepo) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubRedeemCodeRepo) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
return 0, errors.New("not implemented")
}
type stubUserSubscriptionRepo struct {
byUser map[int64][]service.UserSubscription
activeByUser map[int64][]service.UserSubscription
}
func (r *stubUserSubscriptionRepo) SetByUserID(userID int64, subs []service.UserSubscription) {
if r.byUser == nil {
r.byUser = make(map[int64][]service.UserSubscription)
}
r.byUser[userID] = append([]service.UserSubscription(nil), subs...)
}
func (r *stubUserSubscriptionRepo) SetActiveByUserID(userID int64, subs []service.UserSubscription) {
if r.activeByUser == nil {
r.activeByUser = make(map[int64][]service.UserSubscription)
}
r.activeByUser[userID] = append([]service.UserSubscription(nil), subs...)
}
func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSubscription) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSubscription) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
if r.byUser == nil {
return nil, nil
}
return append([]service.UserSubscription(nil), r.byUser[userID]...), nil
}
func (r *stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
if r.activeByUser == nil {
return nil, nil
}
return append([]service.UserSubscription(nil), r.activeByUser[userID]...), nil
}
func (stubUserSubscriptionRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, platform, sortBy, sortOrder string) ([]service.UserSubscription, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) UpdateStatus(ctx context.Context, subscriptionID int64, status string) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ActivateWindows(ctx context.Context, id int64, start time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ResetUsageWindows(ctx context.Context, id int64, resetDaily, resetWeekly, resetMonthly bool, newWindowStart time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ResetDailyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ResetWeeklyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ResetMonthlyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) IncrementUsage(ctx context.Context, id int64, costUSD float64) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) {
return 0, errors.New("not implemented")
}
type stubApiKeyRepo struct {
now time.Time
nextID int64
byID map[int64]*service.APIKey
byKey map[string]*service.APIKey
}
func newStubApiKeyRepo(now time.Time) *stubApiKeyRepo {
return &stubApiKeyRepo{
now: now,
nextID: 100,
byID: make(map[int64]*service.APIKey),
byKey: make(map[string]*service.APIKey),
}
}
func (r *stubApiKeyRepo) MustSeed(key *service.APIKey) {
if key == nil {
return
}
clone := *key
r.byID[clone.ID] = &clone
r.byKey[clone.Key] = &clone
}
func (r *stubApiKeyRepo) Create(ctx context.Context, key *service.APIKey) error {
if key == nil {
return errors.New("nil key")
}
if key.ID == 0 {
key.ID = r.nextID
r.nextID++
}
if key.CreatedAt.IsZero() {
key.CreatedAt = r.now
}
if key.UpdatedAt.IsZero() {
key.UpdatedAt = r.now
}
clone := *key
r.byID[clone.ID] = &clone
r.byKey[clone.Key] = &clone
return nil
}
func (r *stubApiKeyRepo) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
key, ok := r.byID[id]
if !ok {
return nil, service.ErrAPIKeyNotFound
}
clone := *key
return &clone, nil
}
func (r *stubApiKeyRepo) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
key, ok := r.byID[id]
if !ok {
return "", 0, service.ErrAPIKeyNotFound
}
return key.Key, key.UserID, nil
}
func (r *stubApiKeyRepo) GetByKey(ctx context.Context, key string) (*service.APIKey, error) {
found, ok := r.byKey[key]
if !ok {
return nil, service.ErrAPIKeyNotFound
}
clone := *found
return &clone, nil
}
func (r *stubApiKeyRepo) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) {
return r.GetByKey(ctx, key)
}
func (r *stubApiKeyRepo) Update(ctx context.Context, key *service.APIKey, _ service.APIKeyUpdateFields) error {
if key == nil {
return errors.New("nil key")
}
if _, ok := r.byID[key.ID]; !ok {
return service.ErrAPIKeyNotFound
}
if key.UpdatedAt.IsZero() {
key.UpdatedAt = r.now
}
clone := *key
r.byID[clone.ID] = &clone
r.byKey[clone.Key] = &clone
return nil
}
func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error {
key, ok := r.byID[id]
if !ok {
return service.ErrAPIKeyNotFound
}
delete(r.byID, id)
delete(r.byKey, key.Key)
return nil
}
func (r *stubApiKeyRepo) DeleteWithAudit(ctx context.Context, id int64) error {
return r.Delete(ctx, id)
}
func (r *stubApiKeyRepo) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, _ service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
ids := make([]int64, 0, len(r.byID))
for id := range r.byID {
if r.byID[id].UserID == userID {
ids = append(ids, id)
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] > ids[j] })
start := params.Offset()
if start > len(ids) {
start = len(ids)
}
end := start + params.Limit()
if end > len(ids) {
end = len(ids)
}
out := make([]service.APIKey, 0, end-start)
for _, id := range ids[start:end] {
clone := *r.byID[id]
out = append(out, clone)
}
total := int64(len(ids))
pageSize := params.Limit()
pages := int(math.Ceil(float64(total) / float64(pageSize)))
if pages < 1 {
pages = 1
}
return out, &pagination.PaginationResult{
Total: total,
Page: params.Page,
PageSize: pageSize,
Pages: pages,
}, nil
}
func (r *stubApiKeyRepo) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
if len(apiKeyIDs) == 0 {
return []int64{}, nil
}
seen := make(map[int64]struct{}, len(apiKeyIDs))
out := make([]int64, 0, len(apiKeyIDs))
for _, id := range apiKeyIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
key, ok := r.byID[id]
if ok && key.UserID == userID {
out = append(out, id)
}
}
return out, nil
}
func (r *stubApiKeyRepo) CountByUserID(ctx context.Context, userID int64) (int64, error) {
var count int64
for _, key := range r.byID {
if key.UserID == userID {
count++
}
}
return count, nil
}
func (r *stubApiKeyRepo) ExistsByKey(ctx context.Context, key string) (bool, error) {
_, ok := r.byKey[key]
return ok, nil
}
func (r *stubApiKeyRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubApiKeyRepo) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) {
return nil, errors.New("not implemented")
}
func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
return 0, errors.New("not implemented")
}
func (r *stubApiKeyRepo) UpdateGroupIDByUserAndGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (int64, error) {
var updated int64
for id, key := range r.byID {
if key.UserID != userID || key.GroupID == nil || *key.GroupID != oldGroupID {
continue
}
clone := *key
gid := newGroupID
clone.GroupID = &gid
r.byID[id] = &clone
r.byKey[clone.Key] = &clone
updated++
}
return updated, nil
}
func (r *stubApiKeyRepo) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
return 0, errors.New("not implemented")
}
func (r *stubApiKeyRepo) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
return nil, errors.New("not implemented")
}
func (r *stubApiKeyRepo) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
return nil, errors.New("not implemented")
}
func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
return 0, errors.New("not implemented")
}
func (r *stubApiKeyRepo) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
key, ok := r.byID[id]
if !ok {
return service.ErrAPIKeyNotFound
}
ts := usedAt
key.LastUsedAt = &ts
key.UpdatedAt = usedAt
clone := *key
r.byID[id] = &clone
r.byKey[clone.Key] = &clone
return nil
}
func (r *stubApiKeyRepo) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
return nil
}
func (r *stubApiKeyRepo) ResetRateLimitWindows(ctx context.Context, id int64) error {
return nil
}
func (r *stubApiKeyRepo) GetRateLimitData(ctx context.Context, id int64) (*service.APIKeyRateLimitData, error) {
return nil, nil
}
type stubUsageLogRepo struct {
userLogs map[int64][]service.UsageLog
}
func newStubUsageLogRepo() *stubUsageLogRepo {
return &stubUsageLogRepo{userLogs: make(map[int64][]service.UsageLog)}
}
func (r *stubUsageLogRepo) SetUserLogs(userID int64, logs []service.UsageLog) {
r.userLogs[userID] = logs
}
func (r *stubUsageLogRepo) Create(ctx context.Context, log *service.UsageLog) (bool, error) {
return false, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
logs := r.userLogs[userID]
total := int64(len(logs))
out := paginateLogs(logs, params)
return out, paginationResult(total, params), nil
}
func (r *stubUsageLogRepo) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
logs := r.userLogs[userID]
return logs, paginationResult(int64(len(logs)), pagination.PaginationParams{Page: 1, PageSize: 100}), nil
}
func (r *stubUsageLogRepo) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
return nil, nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.TrendDataPoint, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.ModelStat, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUpstreamEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetGroupStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.GroupStat, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserSpendingRanking(ctx context.Context, startTime, endTime time.Time, limit int) (*usagestats.UserSpendingRankingResponse, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
logs := r.userLogs[userID]
if len(logs) == 0 {
return &usagestats.UsageStats{}, nil
}
var totalRequests int64
var totalInputTokens int64
var totalOutputTokens int64
var totalCacheTokens int64
var totalCacheCreationTokens int64
var totalCacheReadTokens int64
var totalCost float64
var totalActualCost float64
var totalDuration int64
var durationCount int64
for _, log := range logs {
totalRequests++
totalInputTokens += int64(log.InputTokens)
totalOutputTokens += int64(log.OutputTokens)
totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
totalCacheCreationTokens += int64(log.CacheCreationTokens)
totalCacheReadTokens += int64(log.CacheReadTokens)
totalCost += log.TotalCost
totalActualCost += log.ActualCost
if log.DurationMs != nil {
totalDuration += int64(*log.DurationMs)
durationCount++
}
}
var avgDuration float64
if durationCount > 0 {
avgDuration = float64(totalDuration) / float64(durationCount)
}
return &usagestats.UsageStats{
TotalRequests: totalRequests,
TotalInputTokens: totalInputTokens,
TotalOutputTokens: totalOutputTokens,
TotalCacheTokens: totalCacheTokens,
TotalCacheCreationTokens: totalCacheCreationTokens,
TotalCacheReadTokens: totalCacheReadTokens,
TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
TotalCost: totalCost,
TotalActualCost: totalActualCost,
AverageDurationMs: avgDuration,
}, nil
}
func (r *stubUsageLogRepo) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) ([]map[string]any, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetBatchUserUsageStats(ctx context.Context, userIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchUserUsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchAPIKeyUsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
logs := r.userLogs[filters.UserID]
// Apply filters
var filtered []service.UsageLog
for _, log := range logs {
// Apply APIKeyID filter
if filters.APIKeyID > 0 && log.APIKeyID != filters.APIKeyID {
continue
}
// Apply Model filter
if filters.Model != "" && stubUsageLogFilterModel(log, filters.ModelFilterSource) != filters.Model {
continue
}
// Apply Stream filter
if filters.Stream != nil && log.Stream != *filters.Stream {
continue
}
// Apply BillingType filter
if filters.BillingType != nil && log.BillingType != *filters.BillingType {
continue
}
// Apply time range filters
if filters.StartTime != nil && log.CreatedAt.Before(*filters.StartTime) {
continue
}
if filters.EndTime != nil && log.CreatedAt.After(*filters.EndTime) {
continue
}
filtered = append(filtered, log)
}
total := int64(len(filtered))
out := paginateLogs(filtered, params)
return out, paginationResult(total, params), nil
}
func stubUsageLogFilterModel(log service.UsageLog, source string) string {
if source == usagestats.ModelSourceRequested && log.RequestedModel != "" {
return log.RequestedModel
}
return log.Model
}
func (r *stubUsageLogRepo) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) {
return nil, errors.New("not implemented")
}
func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
logs, _, err := r.ListWithFilters(ctx, pagination.PaginationParams{Page: 1, PageSize: 100000}, filters)
if err != nil {
return nil, err
}
var totalRequests int64
var totalInputTokens int64
var totalOutputTokens int64
var totalCacheTokens int64
var totalCacheCreationTokens int64
var totalCacheReadTokens int64
var totalCost float64
var totalActualCost float64
var totalDuration int64
var durationCount int64
for _, log := range logs {
totalRequests++
totalInputTokens += int64(log.InputTokens)
totalOutputTokens += int64(log.OutputTokens)
totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
totalCacheCreationTokens += int64(log.CacheCreationTokens)
totalCacheReadTokens += int64(log.CacheReadTokens)
totalCost += log.TotalCost
totalActualCost += log.ActualCost
if log.DurationMs != nil {
totalDuration += int64(*log.DurationMs)
durationCount++
}
}
var avgDuration float64
if durationCount > 0 {
avgDuration = float64(totalDuration) / float64(durationCount)
}
return &usagestats.UsageStats{
TotalRequests: totalRequests,
TotalInputTokens: totalInputTokens,
TotalOutputTokens: totalOutputTokens,
TotalCacheTokens: totalCacheTokens,
TotalCacheCreationTokens: totalCacheCreationTokens,
TotalCacheReadTokens: totalCacheReadTokens,
TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
TotalCost: totalCost,
TotalActualCost: totalActualCost,
AverageDurationMs: avgDuration,
Endpoints: []usagestats.EndpointStat{},
}, nil
}
func (r *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
return nil, errors.New("not implemented")
}
type stubSettingRepo struct {
all map[string]string
}
func newStubSettingRepo() *stubSettingRepo {
return &stubSettingRepo{all: make(map[string]string)}
}
func (r *stubSettingRepo) SetAll(values map[string]string) {
r.all = make(map[string]string, len(values))
for k, v := range values {
r.all[k] = v
}
}
func (r *stubSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) {
value, ok := r.all[key]
if !ok {
return nil, service.ErrSettingNotFound
}
return &service.Setting{Key: key, Value: value}, nil
}
func (r *stubSettingRepo) GetValue(ctx context.Context, key string) (string, error) {
value, ok := r.all[key]
if !ok {
return "", service.ErrSettingNotFound
}
return value, nil
}
func (r *stubSettingRepo) Set(ctx context.Context, key, value string) error {
r.all[key] = value
return nil
}
func (r *stubSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
out := make(map[string]string, len(keys))
for _, key := range keys {
out[key] = r.all[key]
}
return out, nil
}
func (r *stubSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error {
for k, v := range settings {
r.all[k] = v
}
return nil
}
func (r *stubSettingRepo) GetAll(ctx context.Context) (map[string]string, error) {
out := make(map[string]string, len(r.all))
for k, v := range r.all {
out[k] = v
}
return out, nil
}
func (r *stubSettingRepo) Delete(ctx context.Context, key string) error {
delete(r.all, key)
return nil
}
func paginateLogs(logs []service.UsageLog, params pagination.PaginationParams) []service.UsageLog {
start := params.Offset()
if start > len(logs) {
start = len(logs)
}
end := start + params.Limit()
if end > len(logs) {
end = len(logs)
}
out := make([]service.UsageLog, 0, end-start)
out = append(out, logs[start:end]...)
return out
}
func paginationResult(total int64, params pagination.PaginationParams) *pagination.PaginationResult {
pageSize := params.Limit()
pages := int(math.Ceil(float64(total) / float64(pageSize)))
if pages < 1 {
pages = 1
}
return &pagination.PaginationResult{
Total: total,
Page: params.Page,
PageSize: pageSize,
Pages: pages,
}
}
// Ensure compile-time interface compliance.
var (
_ service.UserRepository = (*stubUserRepo)(nil)
_ service.APIKeyRepository = (*stubApiKeyRepo)(nil)
_ service.APIKeyCache = (*stubApiKeyCache)(nil)
_ service.GroupRepository = (*stubGroupRepo)(nil)
_ service.UserSubscriptionRepository = (*stubUserSubscriptionRepo)(nil)
_ service.UsageLogRepository = (*stubUsageLogRepo)(nil)
_ service.SettingRepository = (*stubSettingRepo)(nil)
)