mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-13 22:50:19 +08:00
fix(providers): hide billing fields in provider form
This commit is contained in:
@@ -780,6 +780,14 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
|||||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
vec![
|
vec![
|
||||||
sample_provider("provider-openai", "openai", 10)
|
sample_provider("provider-openai", "openai", 10)
|
||||||
|
.with_billing_fields(
|
||||||
|
Some("free_tier".to_string()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(30),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.with_transport_fields(
|
.with_transport_fields(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -974,6 +982,7 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|provider| provider.id == "provider-openai")
|
.find(|provider| provider.id == "provider-openai")
|
||||||
.expect("provider should exist");
|
.expect("provider should exist");
|
||||||
|
assert_eq!(updated_provider.billing_type.as_deref(), Some("free_tier"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
updated_provider.request_timeout_secs,
|
updated_provider.request_timeout_secs,
|
||||||
Some(aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS as f64)
|
Some(aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS as f64)
|
||||||
@@ -1106,6 +1115,7 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
|||||||
.find(|provider| provider.id == "provider-existing")
|
.find(|provider| provider.id == "provider-existing")
|
||||||
.expect("existing provider should remain");
|
.expect("existing provider should remain");
|
||||||
assert_eq!(created.provider_type, "codex");
|
assert_eq!(created.provider_type, "codex");
|
||||||
|
assert_eq!(created.billing_type.as_deref(), Some("pay_as_you_go"));
|
||||||
assert_eq!(created.provider_priority, 0);
|
assert_eq!(created.provider_priority, 0);
|
||||||
assert_eq!(existing.provider_priority, 1);
|
assert_eq!(existing.provider_priority, 1);
|
||||||
assert_eq!(created.website.as_deref(), Some("https://codex.example"));
|
assert_eq!(created.website.as_deref(), Some("https://codex.example"));
|
||||||
|
|||||||
@@ -123,39 +123,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 计费与限流 / 请求配置 -->
|
<!-- 请求配置 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
<h3 class="text-sm font-medium border-b pb-2">
|
{{ legacyT('请求配置') }}
|
||||||
{{ legacyT('计费与限流') }}
|
</h3>
|
||||||
</h3>
|
|
||||||
<h3 class="text-sm font-medium border-b pb-2">
|
|
||||||
{{ legacyT('请求配置') }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label>{{ legacyT('计费类型') }}</Label>
|
|
||||||
<Select
|
|
||||||
v-model="form.billing_type"
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="monthly_quota">
|
|
||||||
{{ legacyT('月卡额度') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="pay_as_you_go">
|
|
||||||
{{ legacyT('按量付费') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="free_tier">
|
|
||||||
{{ legacyT('免费套餐') }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 超时配置 -->
|
<!-- 超时配置 -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
@@ -229,49 +201,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 月卡配置 -->
|
|
||||||
<div
|
|
||||||
v-if="form.billing_type === 'monthly_quota'"
|
|
||||||
class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50"
|
|
||||||
>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label class="text-xs">{{ legacyT('周期额度 (USD)') }}</Label>
|
|
||||||
<Input
|
|
||||||
:model-value="form.monthly_quota_usd ?? ''"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
@update:model-value="(v) => form.monthly_quota_usd = parseNumberInput(v, { allowFloat: true })"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label class="text-xs">{{ legacyT('重置周期 (天)') }}</Label>
|
|
||||||
<Input
|
|
||||||
:model-value="form.quota_reset_day ?? ''"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="365"
|
|
||||||
@update:model-value="(v) => form.quota_reset_day = parseNumberInput(v) ?? 30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label class="text-xs">
|
|
||||||
{{ legacyT('周期开始时间') }} <span class="text-red-500">*</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.quota_last_reset_at"
|
|
||||||
type="datetime-local"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label class="text-xs">{{ legacyT('过期时间') }}</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.quota_expires_at"
|
|
||||||
type="datetime-local"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 功能开关 -->
|
<!-- 功能开关 -->
|
||||||
@@ -426,7 +355,6 @@ import {
|
|||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { parseNumberInput } from '@/utils/form'
|
import { parseNumberInput } from '@/utils/form'
|
||||||
import { dateTimeLocalToRfc3339, formatDateTimeLocalInput } from '@/utils/date'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -468,12 +396,6 @@ const form = ref({
|
|||||||
provider_type: 'custom' as ProviderType,
|
provider_type: 'custom' as ProviderType,
|
||||||
description: '',
|
description: '',
|
||||||
website: '',
|
website: '',
|
||||||
// 计费配置
|
|
||||||
billing_type: 'pay_as_you_go' as 'monthly_quota' | 'pay_as_you_go' | 'free_tier',
|
|
||||||
monthly_quota_usd: undefined as number | undefined,
|
|
||||||
quota_reset_day: 30,
|
|
||||||
quota_last_reset_at: '', // 周期开始时间
|
|
||||||
quota_expires_at: '',
|
|
||||||
provider_priority: 100,
|
provider_priority: 100,
|
||||||
keep_priority_on_conversion: false, // 格式转换时是否保持优先级
|
keep_priority_on_conversion: false, // 格式转换时是否保持优先级
|
||||||
// 状态配置
|
// 状态配置
|
||||||
@@ -504,11 +426,6 @@ function resetForm() {
|
|||||||
provider_type: 'custom',
|
provider_type: 'custom',
|
||||||
description: '',
|
description: '',
|
||||||
website: '',
|
website: '',
|
||||||
billing_type: 'pay_as_you_go',
|
|
||||||
monthly_quota_usd: undefined,
|
|
||||||
quota_reset_day: 30,
|
|
||||||
quota_last_reset_at: '',
|
|
||||||
quota_expires_at: '',
|
|
||||||
provider_priority: defaultPriority.value,
|
provider_priority: defaultPriority.value,
|
||||||
keep_priority_on_conversion: false,
|
keep_priority_on_conversion: false,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -542,11 +459,6 @@ function loadProviderData() {
|
|||||||
provider_type: props.provider.provider_type || 'custom',
|
provider_type: props.provider.provider_type || 'custom',
|
||||||
description: props.provider.description || '',
|
description: props.provider.description || '',
|
||||||
website: props.provider.website || '',
|
website: props.provider.website || '',
|
||||||
billing_type: (props.provider.billing_type as 'monthly_quota' | 'pay_as_you_go' | 'free_tier') || 'pay_as_you_go',
|
|
||||||
monthly_quota_usd: props.provider.monthly_quota_usd || undefined,
|
|
||||||
quota_reset_day: props.provider.quota_reset_day || 30,
|
|
||||||
quota_last_reset_at: formatDateTimeLocalInput(props.provider.quota_last_reset_at),
|
|
||||||
quota_expires_at: formatDateTimeLocalInput(props.provider.quota_expires_at),
|
|
||||||
provider_priority: props.provider.provider_priority || 999,
|
provider_priority: props.provider.provider_priority || 999,
|
||||||
keep_priority_on_conversion: props.provider.keep_priority_on_conversion ?? false,
|
keep_priority_on_conversion: props.provider.keep_priority_on_conversion ?? false,
|
||||||
is_active: props.provider.is_active,
|
is_active: props.provider.is_active,
|
||||||
@@ -595,23 +507,6 @@ watch(() => form.value.provider_type, () => {
|
|||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// 月卡类型必须设置周期开始时间
|
|
||||||
if (form.value.billing_type === 'monthly_quota' && !form.value.quota_last_reset_at) {
|
|
||||||
showError(legacyT('月卡类型必须设置周期开始时间'), legacyT('验证失败'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotaLastResetAt = dateTimeLocalToRfc3339(form.value.quota_last_reset_at)
|
|
||||||
if (form.value.billing_type === 'monthly_quota' && !quotaLastResetAt) {
|
|
||||||
showError(legacyT('周期开始时间必须是合法时间'), legacyT('验证失败'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const quotaExpiresAt = dateTimeLocalToRfc3339(form.value.quota_expires_at)
|
|
||||||
if (form.value.quota_expires_at && !quotaExpiresAt) {
|
|
||||||
showError(legacyT('过期时间必须是合法时间'), legacyT('验证失败'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
||||||
@@ -620,11 +515,6 @@ const handleSubmit = async () => {
|
|||||||
provider_type: form.value.provider_type,
|
provider_type: form.value.provider_type,
|
||||||
description: form.value.description || undefined,
|
description: form.value.description || undefined,
|
||||||
website: form.value.website || undefined,
|
website: form.value.website || undefined,
|
||||||
billing_type: form.value.billing_type,
|
|
||||||
monthly_quota_usd: form.value.monthly_quota_usd,
|
|
||||||
quota_reset_day: form.value.quota_reset_day,
|
|
||||||
quota_last_reset_at: quotaLastResetAt,
|
|
||||||
quota_expires_at: quotaExpiresAt,
|
|
||||||
keep_priority_on_conversion: form.value.keep_priority_on_conversion,
|
keep_priority_on_conversion: form.value.keep_priority_on_conversion,
|
||||||
responses_websocket_enabled: form.value.responses_websocket_enabled,
|
responses_websocket_enabled: form.value.responses_websocket_enabled,
|
||||||
is_active: form.value.is_active,
|
is_active: form.value.is_active,
|
||||||
|
|||||||
+65
@@ -137,6 +137,36 @@ function clickButton(text: string) {
|
|||||||
button.click()
|
button.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const billingFieldNames = [
|
||||||
|
'billing_type',
|
||||||
|
'monthly_quota_usd',
|
||||||
|
'quota_reset_day',
|
||||||
|
'quota_last_reset_at',
|
||||||
|
'quota_expires_at',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function expectBillingConfigurationHidden() {
|
||||||
|
for (const text of [
|
||||||
|
'计费类型',
|
||||||
|
'月卡额度',
|
||||||
|
'按量付费',
|
||||||
|
'免费套餐',
|
||||||
|
'周期额度 (USD)',
|
||||||
|
'重置周期 (天)',
|
||||||
|
'周期开始时间',
|
||||||
|
'过期时间',
|
||||||
|
]) {
|
||||||
|
expect(document.body.textContent).not.toContain(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectBillingFieldsOmitted(payload: unknown) {
|
||||||
|
expect(payload).toEqual(expect.any(Object))
|
||||||
|
for (const field of billingFieldNames) {
|
||||||
|
expect(payload).not.toHaveProperty(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
endpointMocks.createProvider.mockReset()
|
endpointMocks.createProvider.mockReset()
|
||||||
endpointMocks.createProvider.mockResolvedValue({ id: 'provider-new', name: 'New Provider' })
|
endpointMocks.createProvider.mockResolvedValue({ id: 'provider-new', name: 'New Provider' })
|
||||||
@@ -226,6 +256,41 @@ describe('ProviderFormDialog transfer limits', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('ProviderFormDialog billing configuration', () => {
|
||||||
|
it('hides billing configuration and omits billing fields when creating', async () => {
|
||||||
|
mountDialog(null)
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
expectBillingConfigurationHidden()
|
||||||
|
|
||||||
|
await setInput('#name', 'New Provider')
|
||||||
|
clickButton('创建')
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
expect(endpointMocks.createProvider).toHaveBeenCalledTimes(1)
|
||||||
|
expectBillingFieldsOmitted(endpointMocks.createProvider.mock.calls[0]?.[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides existing monthly quota configuration and preserves it when editing', async () => {
|
||||||
|
mountDialog(makeProvider({
|
||||||
|
billing_type: 'monthly_quota',
|
||||||
|
monthly_quota_usd: 200,
|
||||||
|
quota_reset_day: 30,
|
||||||
|
quota_last_reset_at: '2026-08-01T00:00:00Z',
|
||||||
|
quota_expires_at: '2026-09-01T00:00:00Z',
|
||||||
|
}))
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
expectBillingConfigurationHidden()
|
||||||
|
|
||||||
|
clickButton('保存')
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
expect(endpointMocks.updateProvider).toHaveBeenCalledTimes(1)
|
||||||
|
expectBillingFieldsOmitted(endpointMocks.updateProvider.mock.calls[0]?.[1])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('ProviderFormDialog provider types', () => {
|
describe('ProviderFormDialog provider types', () => {
|
||||||
it('creates an experimental Claude Code provider from the add dialog', async () => {
|
it('creates an experimental Claude Code provider from the add dialog', async () => {
|
||||||
mountDialog(null)
|
mountDialog(null)
|
||||||
|
|||||||
@@ -68,14 +68,6 @@ import { BookOpen } from 'lucide-vue-next'
|
|||||||
<div class="space-y-4 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
|
<div class="space-y-4 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
|
||||||
<ul class="list-decimal pl-5 space-y-2">
|
<ul class="list-decimal pl-5 space-y-2">
|
||||||
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">提供商类型:</strong>自定义或反代;一般自定义即可,反代请进入反代章节。</li>
|
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">提供商类型:</strong>自定义或反代;一般自定义即可,反代请进入反代章节。</li>
|
||||||
<li>
|
|
||||||
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">计费类型:</strong>
|
|
||||||
<ul class="list-disc pl-5 mt-1 space-y-1">
|
|
||||||
<li>按量付费:持续使用</li>
|
|
||||||
<li>月卡额度:按周期(天)限额</li>
|
|
||||||
<li>免费套餐:不计入成本即倍率为0</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">最大重试次数:</strong>在缓存亲和调度模式下,首次请求失败后的重试次数。</li>
|
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">最大重试次数:</strong>在缓存亲和调度模式下,首次请求失败后的重试次数。</li>
|
||||||
<li>
|
<li>
|
||||||
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">超时时间:</strong>
|
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">超时时间:</strong>
|
||||||
|
|||||||
@@ -15,16 +15,12 @@
|
|||||||
|
|
||||||
## 2. 添加提供商
|
## 2. 添加提供商
|
||||||
1. **提供商类型**: 自定义或反代; 一般自定义即可, 反代请进入反代章节
|
1. **提供商类型**: 自定义或反代; 一般自定义即可, 反代请进入反代章节
|
||||||
2. **计费类型**:
|
2. **最大重试次数**
|
||||||
- 按量付费: 持续使用
|
|
||||||
- 月卡额度: 按周期(天)限额
|
|
||||||
- 免费套餐: 不计入成本即倍率为0
|
|
||||||
3. **最大重试次数**
|
|
||||||
- 在缓存亲和调度模式下, 首次请求失败后的重试次数。
|
- 在缓存亲和调度模式下, 首次请求失败后的重试次数。
|
||||||
4. **超时时间**
|
3. **超时时间**
|
||||||
- 流式首字超时时间: 流式请求收到首字前的超时时间
|
- 流式首字超时时间: 流式请求收到首字前的超时时间
|
||||||
- 非流请求超时时间: 非流请求的总超时时间
|
- 非流请求超时时间: 非流请求的总超时时间
|
||||||
5. **保持优先级**
|
4. **保持优先级**
|
||||||
- 通过格式转换的请求, 是否保持当前优先级。
|
- 通过格式转换的请求, 是否保持当前优先级。
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Reference in New Issue
Block a user