mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 优化端点管理表单和上游模型获取逻辑
- 端点表单布局优化,支持 provider website 作为默认 Base URL - 模型路由始终显示 provider_model_name - 无端点时显示添加按钮 - 上游模型获取只针对基础 API 格式,跳过 CLI 格式
This commit is contained in:
@@ -852,9 +852,9 @@ function getPriorityModeLabel(mode: string): string {
|
|||||||
return labels[mode] || mode
|
return labels[mode] || mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否存在模型映射转换
|
// 判断是否存在模型映射(始终显示 provider_model_name)
|
||||||
function hasModelMapping(provider: RoutingProviderInfo): boolean {
|
function hasModelMapping(provider: RoutingProviderInfo): boolean {
|
||||||
return provider.provider_model_name !== routingData.value?.global_model_name
|
return !!provider.provider_model_name
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Key 的 allowed_models 中匹配当前 GlobalModel 的所有模型名
|
// 获取 Key 的 allowed_models 中匹配当前 GlobalModel 的所有模型名
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
>
|
>
|
||||||
<!-- 编辑模式 -->
|
<!-- 编辑模式 -->
|
||||||
<template v-if="editingEndpointId === endpoint.id">
|
<template v-if="editingEndpointId === endpoint.id">
|
||||||
<div class="space-y-2">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm font-medium w-24 shrink-0">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
<span class="text-sm font-medium">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
||||||
<div class="flex items-center gap-1 ml-auto">
|
<div class="flex items-center gap-1 ml-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -48,21 +48,21 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="flex items-end gap-3">
|
||||||
<div class="space-y-1">
|
<div class="flex-1 min-w-0 space-y-1">
|
||||||
<Label class="text-xs text-muted-foreground">Base URL</Label>
|
<Label class="text-xs text-muted-foreground">Base URL</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="editingUrl"
|
v-model="editingUrl"
|
||||||
class="h-8 text-sm"
|
class="h-9"
|
||||||
placeholder="https://api.example.com"
|
:placeholder="provider?.website || 'https://api.example.com'"
|
||||||
@keyup.escape="cancelEdit"
|
@keyup.escape="cancelEdit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="w-52 shrink-0 space-y-1">
|
||||||
<Label class="text-xs text-muted-foreground">自定义路径 (可选)</Label>
|
<Label class="text-xs text-muted-foreground">自定义路径(可选)</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="editingPath"
|
v-model="editingPath"
|
||||||
class="h-8 text-sm"
|
class="h-9"
|
||||||
:placeholder="editingDefaultPath || '留空使用默认路径'"
|
:placeholder="editingDefaultPath || '留空使用默认路径'"
|
||||||
@keyup.escape="cancelEdit"
|
@keyup.escape="cancelEdit"
|
||||||
/>
|
/>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<!-- 请求头规则配置 -->
|
<!-- 请求头规则配置 -->
|
||||||
<Collapsible
|
<Collapsible
|
||||||
v-model:open="rulesExpanded"
|
v-model:open="rulesExpanded"
|
||||||
class="mt-2"
|
class="mt-1"
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger as-child>
|
<CollapsibleTrigger as-child>
|
||||||
<button
|
<button
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(rule, index) in editingRules"
|
v-for="(rule, index) in editingRules"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex items-start gap-2 p-2 rounded bg-muted/50"
|
class="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<!-- 操作类型选择 -->
|
<!-- 操作类型选择 -->
|
||||||
<Select
|
<Select
|
||||||
@@ -104,10 +104,13 @@
|
|||||||
:model-value="rule.action"
|
:model-value="rule.action"
|
||||||
@update:model-value="(v) => updateRuleAction(index, v as 'set' | 'drop' | 'rename')"
|
@update:model-value="(v) => updateRuleAction(index, v as 'set' | 'drop' | 'rename')"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-24 h-7 text-xs">
|
<SelectTrigger class="w-24 h-8 text-xs shrink-0">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent :side-offset="4">
|
<SelectContent
|
||||||
|
position="popper"
|
||||||
|
:side-offset="4"
|
||||||
|
>
|
||||||
<SelectItem value="set">
|
<SelectItem value="set">
|
||||||
覆写
|
覆写
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -120,89 +123,62 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<!-- set: key + value -->
|
<!-- set: key = value -->
|
||||||
<template v-if="rule.action === 'set'">
|
<template v-if="rule.action === 'set'">
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<Input
|
<Input
|
||||||
v-model="rule.key"
|
v-model="rule.key"
|
||||||
placeholder="Header 名称"
|
placeholder="Header 名称"
|
||||||
class="h-7 text-xs"
|
:class="`w-36 h-8 text-xs ${validateRuleKey(rule.key, index) ? 'border-destructive' : ''}`"
|
||||||
|
:title="validateRuleKey(rule.key, index) || ''"
|
||||||
/>
|
/>
|
||||||
<p
|
<span class="text-muted-foreground text-xs">=</span>
|
||||||
v-if="validateRuleKey(rule.key, index)"
|
|
||||||
class="text-xs text-destructive"
|
|
||||||
>
|
|
||||||
{{ validateRuleKey(rule.key, index) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
v-model="rule.value"
|
v-model="rule.value"
|
||||||
placeholder="Header 值"
|
placeholder="Header 值"
|
||||||
class="flex-1 h-7 text-xs"
|
class="flex-1 h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- drop: key only -->
|
<!-- drop: key -->
|
||||||
<template v-else-if="rule.action === 'drop'">
|
<template v-else-if="rule.action === 'drop'">
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<Input
|
<Input
|
||||||
v-model="rule.key"
|
v-model="rule.key"
|
||||||
placeholder="要删除的 Header 名称"
|
placeholder="要删除的 Header 名称"
|
||||||
class="h-7 text-xs"
|
:class="`flex-1 h-8 text-xs ${validateRuleKey(rule.key, index) ? 'border-destructive' : ''}`"
|
||||||
|
:title="validateRuleKey(rule.key, index) || ''"
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
v-if="validateRuleKey(rule.key, index)"
|
|
||||||
class="text-xs text-destructive"
|
|
||||||
>
|
|
||||||
{{ validateRuleKey(rule.key, index) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- rename: from + to -->
|
<!-- rename: from -> to -->
|
||||||
<template v-else-if="rule.action === 'rename'">
|
<template v-else-if="rule.action === 'rename'">
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<Input
|
<Input
|
||||||
v-model="rule.from"
|
v-model="rule.from"
|
||||||
placeholder="原 Header 名称"
|
placeholder="原名称"
|
||||||
class="h-7 text-xs"
|
:class="`flex-1 h-8 text-xs ${validateRenameFrom(rule.from, index) ? 'border-destructive' : ''}`"
|
||||||
|
:title="validateRenameFrom(rule.from, index) || ''"
|
||||||
/>
|
/>
|
||||||
<p
|
<ArrowRight class="w-4 h-4 shrink-0 text-muted-foreground" />
|
||||||
v-if="validateRenameFrom(rule.from, index)"
|
|
||||||
class="text-xs text-destructive"
|
|
||||||
>
|
|
||||||
{{ validateRenameFrom(rule.from, index) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ArrowRight class="w-4 h-4 shrink-0 text-muted-foreground mt-1.5" />
|
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<Input
|
<Input
|
||||||
v-model="rule.to"
|
v-model="rule.to"
|
||||||
placeholder="新 Header 名称"
|
placeholder="新名称"
|
||||||
class="h-7 text-xs"
|
:class="`flex-1 h-8 text-xs ${validateRenameTo(rule.to, index) ? 'border-destructive' : ''}`"
|
||||||
|
:title="validateRenameTo(rule.to, index) || ''"
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
v-if="validateRenameTo(rule.to, index)"
|
|
||||||
class="text-xs text-destructive"
|
|
||||||
>
|
|
||||||
{{ validateRenameTo(rule.to, index) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7 shrink-0"
|
class="h-8 w-8 shrink-0"
|
||||||
@click="removeRule(index)"
|
@click="removeRule(index)"
|
||||||
>
|
>
|
||||||
<X class="w-3 h-3" />
|
<X class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full h-7 text-xs"
|
class="w-full h-8 text-xs"
|
||||||
@click="addRule"
|
@click="addRule"
|
||||||
>
|
>
|
||||||
<Plus class="w-3 h-3 mr-1" />
|
<Plus class="w-3 h-3 mr-1" />
|
||||||
@@ -273,7 +249,7 @@
|
|||||||
>
|
>
|
||||||
<Label class="text-muted-foreground">添加新端点</Label>
|
<Label class="text-muted-foreground">添加新端点</Label>
|
||||||
<div class="flex items-end gap-3">
|
<div class="flex items-end gap-3">
|
||||||
<div class="w-32 shrink-0 space-y-1.5">
|
<div class="w-36 shrink-0 space-y-1.5">
|
||||||
<Label class="text-xs">API 格式</Label>
|
<Label class="text-xs">API 格式</Label>
|
||||||
<Select
|
<Select
|
||||||
v-model="newEndpoint.api_format"
|
v-model="newEndpoint.api_format"
|
||||||
@@ -293,31 +269,22 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 space-y-1.5">
|
<div class="flex-1 min-w-0 space-y-1.5">
|
||||||
<Label class="text-xs">Base URL</Label>
|
<Label class="text-xs">Base URL</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="newEndpoint.base_url"
|
v-model="newEndpoint.base_url"
|
||||||
placeholder="https://api.example.com"
|
:placeholder="provider?.website || 'https://api.example.com'"
|
||||||
class="h-9"
|
class="h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 shrink-0 space-y-1.5">
|
<div class="w-52 shrink-0 space-y-1.5">
|
||||||
<Label class="text-xs">自定义路径</Label>
|
<Label class="text-xs">自定义路径(可选)</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="newEndpoint.custom_path"
|
v-model="newEndpoint.custom_path"
|
||||||
:placeholder="newEndpointDefaultPath || '可选'"
|
:placeholder="newEndpointDefaultPath || '留空使用默认路径'"
|
||||||
class="h-9"
|
class="h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-9 shrink-0"
|
|
||||||
:disabled="!newEndpoint.api_format || !newEndpoint.base_url || addingEndpoint"
|
|
||||||
@click="handleAddEndpoint"
|
|
||||||
>
|
|
||||||
{{ addingEndpoint ? '添加中...' : '添加' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -337,6 +304,14 @@
|
|||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="availableFormats.length > 0"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!newEndpoint.api_format || (!newEndpoint.base_url?.trim() && !provider?.website?.trim()) || addingEndpoint"
|
||||||
|
@click="handleAddEndpoint"
|
||||||
|
>
|
||||||
|
{{ addingEndpoint ? '添加中...' : '添加' }}
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
@@ -710,21 +685,27 @@ async function saveEndpointUrl(endpoint: ProviderEndpoint) {
|
|||||||
|
|
||||||
// 添加端点
|
// 添加端点
|
||||||
async function handleAddEndpoint() {
|
async function handleAddEndpoint() {
|
||||||
if (!props.provider || !newEndpoint.value.api_format || !newEndpoint.value.base_url) return
|
if (!props.provider || !newEndpoint.value.api_format) return
|
||||||
|
|
||||||
|
// 如果没有输入 base_url,使用提供商的 website 作为默认值
|
||||||
|
const baseUrl = newEndpoint.value.base_url || props.provider.website
|
||||||
|
if (!baseUrl) {
|
||||||
|
showError('请输入 Base URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
addingEndpoint.value = true
|
addingEndpoint.value = true
|
||||||
try {
|
try {
|
||||||
await createEndpoint(props.provider.id, {
|
await createEndpoint(props.provider.id, {
|
||||||
provider_id: props.provider.id,
|
provider_id: props.provider.id,
|
||||||
api_format: newEndpoint.value.api_format,
|
api_format: newEndpoint.value.api_format,
|
||||||
base_url: newEndpoint.value.base_url,
|
base_url: baseUrl,
|
||||||
custom_path: newEndpoint.value.custom_path || undefined,
|
custom_path: newEndpoint.value.custom_path || undefined,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
})
|
})
|
||||||
success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`)
|
success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`)
|
||||||
// 重置表单,保留 URL
|
// 重置表单,保留 URL
|
||||||
const url = newEndpoint.value.base_url
|
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
||||||
newEndpoint.value = { api_format: '', base_url: url, custom_path: '' }
|
|
||||||
emit('endpointCreated')
|
emit('endpointCreated')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showError(error.response?.data?.detail || '添加失败', '错误')
|
showError(error.response?.data?.detail || '添加失败', '错误')
|
||||||
|
|||||||
@@ -213,9 +213,9 @@
|
|||||||
<!-- 自动获取模型 -->
|
<!-- 自动获取模型 -->
|
||||||
<div class="flex items-center justify-between py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
<div class="flex items-center justify-between py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<Label class="text-sm font-medium">自动获取模型</Label>
|
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
系统将定时从上游获取可用模型, 但无法默认接受提供商模型
|
定时更新上游模型, 配合模型映射使用
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="showAutoFetchWarning"
|
v-if="showAutoFetchWarning"
|
||||||
|
|||||||
@@ -95,10 +95,21 @@
|
|||||||
>{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
>{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
|
v-if="endpoints.length > 0"
|
||||||
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
|
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
|
||||||
title="编辑端点"
|
title="编辑端点"
|
||||||
@click="showAddEndpointDialog"
|
@click="showAddEndpointDialog"
|
||||||
>编辑</span>
|
>编辑</span>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 text-xs"
|
||||||
|
@click="showAddEndpointDialog"
|
||||||
|
>
|
||||||
|
<Plus class="w-3 h-3 mr-1" />
|
||||||
|
添加 API 端点
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ from src.utils.ssl_utils import get_ssl_context
|
|||||||
# 并发请求限制
|
# 并发请求限制
|
||||||
MAX_CONCURRENT_REQUESTS = 5
|
MAX_CONCURRENT_REQUESTS = 5
|
||||||
|
|
||||||
|
# 只对这些基础格式获取模型列表,CLI 格式使用相同的上游 API
|
||||||
|
MODEL_FETCH_FORMATS = [APIFormat.OPENAI, APIFormat.CLAUDE, APIFormat.GEMINI]
|
||||||
|
|
||||||
|
|
||||||
def _get_adapter_for_format(api_format: str) -> Optional[type]:
|
def _get_adapter_for_format(api_format: str) -> Optional[type]:
|
||||||
"""根据 API 格式获取对应的 Adapter 类"""
|
"""根据 API 格式获取对应的 Adapter 类"""
|
||||||
@@ -66,9 +69,9 @@ def build_all_format_configs(
|
|||||||
base_url = base_endpoint.base_url
|
base_url = base_endpoint.base_url
|
||||||
extra_headers = get_extra_headers_from_endpoint(base_endpoint)
|
extra_headers = get_extra_headers_from_endpoint(base_endpoint)
|
||||||
|
|
||||||
# 从所有 API 格式都尝试获取模型,然后聚合去重
|
# 只对基础 API 格式获取模型,CLI 格式使用相同的上游 API
|
||||||
endpoint_configs: list[dict] = []
|
endpoint_configs: list[dict] = []
|
||||||
for fmt in APIFormat:
|
for fmt in MODEL_FETCH_FORMATS:
|
||||||
fmt_value = fmt.value
|
fmt_value = fmt.value
|
||||||
# 如果该格式有专门的端点配置,使用其 base_url 和 headers
|
# 如果该格式有专门的端点配置,使用其 base_url 和 headers
|
||||||
if fmt_value in format_to_endpoint:
|
if fmt_value in format_to_endpoint:
|
||||||
|
|||||||
Reference in New Issue
Block a user