Commit 5e060b22 authored by erio's avatar erio
Browse files

Merge remote-tracking branch 'upstream/main' into feat/channel-insights

# Conflicts:
#	backend/cmd/server/wire_gen.go
parents 6f04c25e 0a80ec80
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import { extractApiErrorMessage, extractI18nErrorMessage } from '@/utils/apiError'
export interface OpenAITokenInfo { export interface OpenAITokenInfo {
access_token?: string access_token?: string
...@@ -26,6 +28,7 @@ export type OpenAIOAuthPlatform = 'openai' ...@@ -26,6 +28,7 @@ export type OpenAIOAuthPlatform = 'openai'
export function useOpenAIOAuth() { export function useOpenAIOAuth() {
const appStore = useAppStore() const appStore = useAppStore()
const { t } = useI18n()
const endpointPrefix = '/admin/openai' const endpointPrefix = '/admin/openai'
// State // State
...@@ -78,7 +81,7 @@ export function useOpenAIOAuth() { ...@@ -78,7 +81,7 @@ export function useOpenAIOAuth() {
} }
return true return true
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to generate OpenAI auth URL' error.value = extractApiErrorMessage(err, t('admin.accounts.oauth.openai.failedToGenerateUrl'))
appStore.showError(error.value) appStore.showError(error.value)
return false return false
} finally { } finally {
...@@ -114,7 +117,12 @@ export function useOpenAIOAuth() { ...@@ -114,7 +117,12 @@ export function useOpenAIOAuth() {
const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload) const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload)
return tokenInfo as OpenAITokenInfo return tokenInfo as OpenAITokenInfo
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to exchange OpenAI auth code' error.value = extractI18nErrorMessage(
err,
t,
'admin.accounts.oauth.openai.errors',
t('admin.accounts.oauth.openai.failedToExchangeCode')
)
appStore.showError(error.value) appStore.showError(error.value)
return null return null
} finally { } finally {
...@@ -147,7 +155,12 @@ export function useOpenAIOAuth() { ...@@ -147,7 +155,12 @@ export function useOpenAIOAuth() {
) )
return tokenInfo as OpenAITokenInfo return tokenInfo as OpenAITokenInfo
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || err.message || 'Failed to validate refresh token' error.value = extractI18nErrorMessage(
err,
t,
'admin.accounts.oauth.openai.errors',
t('admin.accounts.oauth.openai.failedToValidateRT')
)
appStore.showError(error.value) appStore.showError(error.value)
return null return null
} finally { } finally {
......
...@@ -1014,6 +1014,8 @@ export default { ...@@ -1014,6 +1014,8 @@ export default {
description: 'Manage your account information and settings', description: 'Manage your account information and settings',
accountBalance: 'Account Balance', accountBalance: 'Account Balance',
concurrencyLimit: 'Concurrency Limit', concurrencyLimit: 'Concurrency Limit',
rpmLimit: 'RPM Limit',
rpmUnlimited: 'Unlimited',
memberSince: 'Member Since', memberSince: 'Member Since',
overviewTitle: 'Account Overview', overviewTitle: 'Account Overview',
overviewDescription: 'Check account status, profile sources, and common actions at a glance.', overviewDescription: 'Check account status, profile sources, and common actions at a glance.',
...@@ -1610,6 +1612,11 @@ export default { ...@@ -1610,6 +1612,11 @@ export default {
copyPassword: 'Copy password', copyPassword: 'Copy password',
creating: 'Creating...', creating: 'Creating...',
updating: 'Updating...', updating: 'Updating...',
form: {
rpmLimit: 'Requests Per Minute (RPM)',
rpmLimitPlaceholder: '0 = unlimited',
rpmLimitHint: 'Max requests per minute for this user; 0 = unlimited. Acts as a fallback only when the group has no rpm_limit set.'
},
columns: { columns: {
user: 'User', user: 'User',
id: 'ID', id: 'ID',
...@@ -1824,6 +1831,10 @@ export default { ...@@ -1824,6 +1831,10 @@ export default {
name: 'Name', name: 'Name',
platform: 'Platform', platform: 'Platform',
rateMultiplier: 'Rate Multiplier', rateMultiplier: 'Rate Multiplier',
rpmOverride: 'RPM Override',
rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited',
rateDefault: 'default',
rpmDefault: 'default',
type: 'Type', type: 'Type',
accounts: 'Accounts', accounts: 'Accounts',
capacity: 'Capacity', capacity: 'Capacity',
...@@ -1850,7 +1861,10 @@ export default { ...@@ -1850,7 +1861,10 @@ export default {
platform: 'Platform', platform: 'Platform',
rateMultiplier: 'Rate Multiplier', rateMultiplier: 'Rate Multiplier',
status: 'Status', status: 'Status',
exclusive: 'Exclusive Group' exclusive: 'Exclusive Group',
rpmLimit: 'Requests Per Minute (RPM)',
rpmLimitPlaceholder: '0 = unlimited',
rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).'
}, },
enterGroupName: 'Enter group name', enterGroupName: 'Enter group name',
optionalDescription: 'Optional description', optionalDescription: 'Optional description',
...@@ -1882,6 +1896,12 @@ export default { ...@@ -1882,6 +1896,12 @@ export default {
rateMultipliers: 'Rate Multipliers', rateMultipliers: 'Rate Multipliers',
rateMultipliersTitle: 'Group Rate Multipliers', rateMultipliersTitle: 'Group Rate Multipliers',
addUserRate: 'Add User Rate Multiplier', addUserRate: 'Add User Rate Multiplier',
rpmOverrides: 'RPM Overrides',
rpmOverridesTitle: 'Group RPM Overrides',
addUserRpm: 'Add User RPM Override',
noRpmOverrides: 'No users have an RPM override yet',
rpmSaved: 'RPM overrides saved',
groupRpmDefault: 'Group default RPM',
searchUserPlaceholder: 'Search user email...', searchUserPlaceholder: 'Search user email...',
noRateMultipliers: 'No user rate multipliers configured', noRateMultipliers: 'No user rate multipliers configured',
rateUpdated: 'Rate multiplier updated', rateUpdated: 'Rate multiplier updated',
...@@ -3058,6 +3078,13 @@ export default { ...@@ -3058,6 +3078,13 @@ export default {
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
authCodeHint: authCodeHint:
'You can copy the entire URL or just the code parameter value, the system will auto-detect', 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
failedToGenerateUrl: 'Failed to generate OpenAI auth URL',
failedToExchangeCode: 'Failed to exchange OpenAI auth code',
failedToValidateRT: 'Failed to validate refresh token',
errors: {
OPENAI_OAUTH_PROXY_REQUIRED:
'No proxy is configured and this server could not reach OpenAI directly, so the OpenAI OAuth request failed. Select a proxy that can access OpenAI and retry; if the authorization code has expired, regenerate the authorization URL.'
},
// Refresh Token auth // Refresh Token auth
refreshTokenAuth: 'Manual RT Input', refreshTokenAuth: 'Manual RT Input',
refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
...@@ -4806,6 +4833,8 @@ export default { ...@@ -4806,6 +4833,8 @@ export default {
defaultBalanceHint: 'Initial balance for new users', defaultBalanceHint: 'Initial balance for new users',
defaultConcurrency: 'Default Concurrency', defaultConcurrency: 'Default Concurrency',
defaultConcurrencyHint: 'Maximum concurrent requests for new users', defaultConcurrencyHint: 'Maximum concurrent requests for new users',
defaultUserRpmLimit: 'Default User RPM Limit',
defaultUserRpmLimitHint: 'Default max requests per minute for new users; 0 = unlimited. Only applied at new user creation.',
defaultSubscriptions: 'Default Subscriptions', defaultSubscriptions: 'Default Subscriptions',
defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered', defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered',
addDefaultSubscription: 'Add Default Subscription', addDefaultSubscription: 'Add Default Subscription',
......
...@@ -1018,6 +1018,8 @@ export default { ...@@ -1018,6 +1018,8 @@ export default {
description: '管理您的账户信息和设置', description: '管理您的账户信息和设置',
accountBalance: '账户余额', accountBalance: '账户余额',
concurrencyLimit: '并发限制', concurrencyLimit: '并发限制',
rpmLimit: 'RPM 限制',
rpmUnlimited: '不限制',
memberSince: '注册时间', memberSince: '注册时间',
overviewTitle: '账户总览', overviewTitle: '账户总览',
overviewDescription: '快速查看账号状态、资料来源与常用设置。', overviewDescription: '快速查看账号状态、资料来源与常用设置。',
...@@ -1709,7 +1711,10 @@ export default { ...@@ -1709,7 +1711,10 @@ export default {
balanceLabel: '余额', balanceLabel: '余额',
concurrencyLabel: '并发数', concurrencyLabel: '并发数',
statusLabel: '状态', statusLabel: '状态',
selectStatus: '选择状态' selectStatus: '选择状态',
rpmLimit: '每分钟请求数 (RPM)',
rpmLimitPlaceholder: '0 表示不限制',
rpmLimitHint: '该用户每分钟最大请求数,0 = 不限制;仅在所用分组未设置 rpm_limit 时作为兜底生效'
}, },
adjustBalance: '调整余额', adjustBalance: '调整余额',
adjustConcurrency: '调整并发数', adjustConcurrency: '调整并发数',
...@@ -1876,6 +1881,10 @@ export default { ...@@ -1876,6 +1881,10 @@ export default {
name: '名称', name: '名称',
platform: '平台', platform: '平台',
rateMultiplier: '费率倍数', rateMultiplier: '费率倍数',
rpmOverride: 'RPM 覆盖',
rpmOverrideHint: '该用户在此分组的 RPM 上限;留空 = 使用分组默认;0 = 不限制',
rateDefault: '默认',
rpmDefault: '默认',
exclusive: '独占', exclusive: '独占',
type: '类型', type: '类型',
priority: '优先级', priority: '优先级',
...@@ -1910,6 +1919,9 @@ export default { ...@@ -1910,6 +1919,9 @@ export default {
descriptionPlaceholder: '请输入描述(可选)', descriptionPlaceholder: '请输入描述(可选)',
rateMultiplierLabel: '费率倍数', rateMultiplierLabel: '费率倍数',
rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍',
rpmLimit: '每分钟请求数 (RPM)',
rpmLimitPlaceholder: '0 表示不限制',
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
exclusiveLabel: '专属分组', exclusiveLabel: '专属分组',
exclusiveHint: '专属分组,可以手动指定给用户', exclusiveHint: '专属分组,可以手动指定给用户',
platformLabel: '平台限制', platformLabel: '平台限制',
...@@ -1979,6 +1991,12 @@ export default { ...@@ -1979,6 +1991,12 @@ export default {
rateMultipliers: '专属倍率', rateMultipliers: '专属倍率',
rateMultipliersTitle: '分组专属倍率管理', rateMultipliersTitle: '分组专属倍率管理',
addUserRate: '添加用户专属倍率', addUserRate: '添加用户专属倍率',
rpmOverrides: '专属 RPM',
rpmOverridesTitle: '分组专属 RPM 管理',
addUserRpm: '添加用户专属 RPM',
noRpmOverrides: '暂无用户设置了专属 RPM',
rpmSaved: '专属 RPM 已保存',
groupRpmDefault: '分组默认 RPM',
searchUserPlaceholder: '搜索用户邮箱...', searchUserPlaceholder: '搜索用户邮箱...',
noRateMultipliers: '暂无用户设置了专属倍率', noRateMultipliers: '暂无用户设置了专属倍率',
rateUpdated: '专属倍率已更新', rateUpdated: '专属倍率已更新',
...@@ -3195,6 +3213,13 @@ export default { ...@@ -3195,6 +3213,13 @@ export default {
authCodePlaceholder: authCodePlaceholder:
'方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值',
authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别',
failedToGenerateUrl: '生成 OpenAI 授权链接失败',
failedToExchangeCode: 'OpenAI 授权码兑换失败',
failedToValidateRT: '验证 Refresh Token 失败',
errors: {
OPENAI_OAUTH_PROXY_REQUIRED:
'未设置代理,当前服务器无法直连 OpenAI,导致 OpenAI OAuth 请求失败。请先选择可访问 OpenAI 的代理后重试;如果授权码已失效,请重新生成授权链接。'
},
// Refresh Token auth // Refresh Token auth
refreshTokenAuth: '手动输入 RT', refreshTokenAuth: '手动输入 RT',
refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
...@@ -4971,6 +4996,8 @@ export default { ...@@ -4971,6 +4996,8 @@ export default {
defaultBalanceHint: '新用户的初始余额', defaultBalanceHint: '新用户的初始余额',
defaultConcurrency: '默认并发数', defaultConcurrency: '默认并发数',
defaultConcurrencyHint: '新用户的最大并发请求数', defaultConcurrencyHint: '新用户的最大并发请求数',
defaultUserRpmLimit: '默认用户 RPM 限制',
defaultUserRpmLimitHint: '新用户默认每分钟最大请求数,0 = 不限制;仅作用于新用户创建时初始化',
defaultSubscriptions: '默认订阅列表', defaultSubscriptions: '默认订阅列表',
defaultSubscriptionsHint: '新用户创建或注册时自动分配这些订阅', defaultSubscriptionsHint: '新用户创建或注册时自动分配这些订阅',
addDefaultSubscription: '添加默认订阅', addDefaultSubscription: '添加默认订阅',
......
...@@ -87,6 +87,7 @@ export interface User { ...@@ -87,6 +87,7 @@ export interface User {
role: 'admin' | 'user' // User role for authorization role: 'admin' | 'user' // User role for authorization
balance: number // User balance for API usage balance: number // User balance for API usage
concurrency: number // Allowed concurrent requests concurrency: number // Allowed concurrent requests
rpm_limit?: number // User-level RPM cap (0 = unlimited); effective as fallback when group has no rpm_limit
status: 'active' | 'disabled' // Account status status: 'active' | 'disabled' // Account status
allowed_groups: number[] | null // Allowed group IDs (null = all non-exclusive groups) allowed_groups: number[] | null // Allowed group IDs (null = all non-exclusive groups)
balance_notify_enabled: boolean balance_notify_enabled: boolean
...@@ -456,6 +457,7 @@ export interface Group { ...@@ -456,6 +457,7 @@ export interface Group {
description: string | null description: string | null
platform: GroupPlatform platform: GroupPlatform
rate_multiplier: number rate_multiplier: number
rpm_limit?: number // Group-level RPM cap (0 = unlimited); overrides user-level rpm_limit when set
is_exclusive: boolean is_exclusive: boolean
status: 'active' | 'inactive' status: 'active' | 'inactive'
subscription_type: SubscriptionType subscription_type: SubscriptionType
......
...@@ -308,6 +308,15 @@ ...@@ -308,6 +308,15 @@
t("admin.groups.rateMultipliers") t("admin.groups.rateMultipliers")
}}</span> }}</span>
</button> </button>
<button
@click="handleRPMOverrides(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-orange-600 dark:hover:bg-dark-700 dark:hover:text-orange-400"
>
<Icon name="bolt" size="sm" />
<span class="text-xs">{{
t("admin.groups.rpmOverrides")
}}</span>
</button>
<button <button
@click="handleDelete(row)" @click="handleDelete(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
...@@ -491,6 +500,18 @@ ...@@ -491,6 +500,18 @@
/> />
<p class="input-hint">{{ t("admin.groups.rateMultiplierHint") }}</p> <p class="input-hint">{{ t("admin.groups.rateMultiplierHint") }}</p>
</div> </div>
<div>
<label class="input-label">{{ t("admin.groups.form.rpmLimit") }}</label>
<input
v-model.number="createForm.rpm_limit"
type="number"
min="0"
step="1"
class="input"
:placeholder="t('admin.groups.form.rpmLimitPlaceholder')"
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<div <div
v-if="createForm.subscription_type !== 'subscription'" v-if="createForm.subscription_type !== 'subscription'"
data-tour="group-form-exclusive" data-tour="group-form-exclusive"
...@@ -1612,6 +1633,18 @@ ...@@ -1612,6 +1633,18 @@
data-tour="group-form-multiplier" data-tour="group-form-multiplier"
/> />
</div> </div>
<div>
<label class="input-label">{{ t("admin.groups.form.rpmLimit") }}</label>
<input
v-model.number="editForm.rpm_limit"
type="number"
min="0"
step="1"
class="input"
:placeholder="t('admin.groups.form.rpmLimitPlaceholder')"
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<div v-if="editForm.subscription_type !== 'subscription'"> <div v-if="editForm.subscription_type !== 'subscription'">
<div class="mb-1.5 flex items-center gap-1"> <div class="mb-1.5 flex items-center gap-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
...@@ -2689,6 +2722,14 @@ ...@@ -2689,6 +2722,14 @@
@close="showRateMultipliersModal = false" @close="showRateMultipliersModal = false"
@success="loadGroups" @success="loadGroups"
/> />
<!-- Group RPM Overrides Modal -->
<GroupRPMOverridesModal
:show="showRPMOverridesModal"
:group="rpmOverridesGroup"
@close="showRPMOverridesModal = false"
@success="loadGroups"
/>
</AppLayout> </AppLayout>
</template> </template>
...@@ -2711,6 +2752,7 @@ import Select from "@/components/common/Select.vue"; ...@@ -2711,6 +2752,7 @@ import Select from "@/components/common/Select.vue";
import PlatformIcon from "@/components/common/PlatformIcon.vue"; import PlatformIcon from "@/components/common/PlatformIcon.vue";
import Icon from "@/components/icons/Icon.vue"; import Icon from "@/components/icons/Icon.vue";
import GroupRateMultipliersModal from "@/components/admin/group/GroupRateMultipliersModal.vue"; import GroupRateMultipliersModal from "@/components/admin/group/GroupRateMultipliersModal.vue";
import GroupRPMOverridesModal from "@/components/admin/group/GroupRPMOverridesModal.vue";
import GroupCapacityBadge from "@/components/common/GroupCapacityBadge.vue"; import GroupCapacityBadge from "@/components/common/GroupCapacityBadge.vue";
import { VueDraggable } from "vue-draggable-plus"; import { VueDraggable } from "vue-draggable-plus";
import { createStableObjectKeyResolver } from "@/utils/stableObjectKey"; import { createStableObjectKeyResolver } from "@/utils/stableObjectKey";
...@@ -2951,6 +2993,8 @@ const editingGroup = ref<AdminGroup | null>(null); ...@@ -2951,6 +2993,8 @@ const editingGroup = ref<AdminGroup | null>(null);
const deletingGroup = ref<AdminGroup | null>(null); const deletingGroup = ref<AdminGroup | null>(null);
const showRateMultipliersModal = ref(false); const showRateMultipliersModal = ref(false);
const rateMultipliersGroup = ref<AdminGroup | null>(null); const rateMultipliersGroup = ref<AdminGroup | null>(null);
const showRPMOverridesModal = ref(false);
const rpmOverridesGroup = ref<AdminGroup | null>(null);
const sortableGroups = ref<AdminGroup[]>([]); const sortableGroups = ref<AdminGroup[]>([]);
const createMessagesDispatchDefaults = createDefaultMessagesDispatchFormState(); const createMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
const editMessagesDispatchDefaults = createDefaultMessagesDispatchFormState(); const editMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
...@@ -2990,6 +3034,8 @@ const createForm = reactive({ ...@@ -2990,6 +3034,8 @@ const createForm = reactive({
mcp_xml_inject: true, mcp_xml_inject: true,
// 从分组复制账号 // 从分组复制账号
copy_accounts_from_group_ids: [] as number[], copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
}); });
// 简单账号类型(用于模型路由选择) // 简单账号类型(用于模型路由选择)
...@@ -3271,6 +3317,8 @@ const editForm = reactive({ ...@@ -3271,6 +3317,8 @@ const editForm = reactive({
mcp_xml_inject: true, mcp_xml_inject: true,
// 从分组复制账号 // 从分组复制账号
copy_accounts_from_group_ids: [] as number[], copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
}); });
// 根据分组类型返回不同的删除确认消息 // 根据分组类型返回不同的删除确认消息
...@@ -3562,6 +3610,7 @@ const handleEdit = async (group: AdminGroup) => { ...@@ -3562,6 +3610,7 @@ const handleEdit = async (group: AdminGroup) => {
]; ];
editForm.mcp_xml_inject = group.mcp_xml_inject ?? true; editForm.mcp_xml_inject = group.mcp_xml_inject ?? true;
editForm.copy_accounts_from_group_ids = []; // 复制账号字段每次编辑时重置为空 editForm.copy_accounts_from_group_ids = []; // 复制账号字段每次编辑时重置为空
editForm.rpm_limit = group.rpm_limit ?? 0;
// 加载模型路由规则(异步加载账号名称) // 加载模型路由规则(异步加载账号名称)
editModelRoutingRules.value = await convertApiFormatToRoutingRules( editModelRoutingRules.value = await convertApiFormatToRoutingRules(
group.model_routing, group.model_routing,
...@@ -3670,6 +3719,11 @@ const handleRateMultipliers = (group: AdminGroup) => { ...@@ -3670,6 +3719,11 @@ const handleRateMultipliers = (group: AdminGroup) => {
showRateMultipliersModal.value = true; showRateMultipliersModal.value = true;
}; };
const handleRPMOverrides = (group: AdminGroup) => {
rpmOverridesGroup.value = group;
showRPMOverridesModal.value = true;
};
const handleDelete = (group: AdminGroup) => { const handleDelete = (group: AdminGroup) => {
deletingGroup.value = group; deletingGroup.value = group;
showDeleteDialog.value = true; showDeleteDialog.value = true;
......
...@@ -2170,6 +2170,24 @@ ...@@ -2170,6 +2170,24 @@
{{ t("admin.settings.defaults.defaultConcurrencyHint") }} {{ t("admin.settings.defaults.defaultConcurrencyHint") }}
</p> </p>
</div> </div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.defaults.defaultUserRpmLimit") }}
</label>
<input
v-model.number="form.default_user_rpm_limit"
type="number"
min="0"
step="1"
class="input"
placeholder="0"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.defaults.defaultUserRpmLimitHint") }}
</p>
</div>
</div> </div>
<div class="border-t border-gray-100 pt-4 dark:border-dark-700"> <div class="border-t border-gray-100 pt-4 dark:border-dark-700">
...@@ -4957,6 +4975,7 @@ const form = reactive<SettingsForm>({ ...@@ -4957,6 +4975,7 @@ const form = reactive<SettingsForm>({
default_concurrency: 1, default_concurrency: 1,
default_subscriptions: [], default_subscriptions: [],
force_email_on_third_party_signup: false, force_email_on_third_party_signup: false,
default_user_rpm_limit: 0,
site_name: "Sub2API", site_name: "Sub2API",
site_logo: "", site_logo: "",
site_subtitle: "Subscription to API Conversion Platform", site_subtitle: "Subscription to API Conversion Platform",
...@@ -5878,6 +5897,7 @@ async function saveSettings() { ...@@ -5878,6 +5897,7 @@ async function saveSettings() {
default_concurrency: form.default_concurrency, default_concurrency: form.default_concurrency,
default_subscriptions: normalizedDefaultSubscriptions, default_subscriptions: normalizedDefaultSubscriptions,
force_email_on_third_party_signup: form.force_email_on_third_party_signup, force_email_on_third_party_signup: form.force_email_on_third_party_signup,
default_user_rpm_limit: form.default_user_rpm_limit,
site_name: form.site_name, site_name: form.site_name,
site_logo: form.site_logo, site_logo: form.site_logo,
site_subtitle: form.site_subtitle, site_subtitle: form.site_subtitle,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment