mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge pull request #157 from AAEE86/master
feat: 新增 OAuth 密钥专用编辑对话框 & 统一预设模型管理
This commit is contained in:
@@ -0,0 +1,323 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:model-value="isOpen"
|
||||||
|
title="编辑账号"
|
||||||
|
description="修改 OAuth 账号配置"
|
||||||
|
:icon="SquarePen"
|
||||||
|
size="xl"
|
||||||
|
@update:model-value="handleDialogUpdate"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
class="space-y-3"
|
||||||
|
autocomplete="off"
|
||||||
|
@submit.prevent="handleSave"
|
||||||
|
>
|
||||||
|
<!-- 基本信息:账号名称 + 备注 -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label for="name">账号名称 *</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
required
|
||||||
|
placeholder="例如:主账号、备用账号"
|
||||||
|
maxlength="100"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label for="note">备注</Label>
|
||||||
|
<Input
|
||||||
|
id="note"
|
||||||
|
v-model="form.note"
|
||||||
|
placeholder="可选的备注信息"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置项 -->
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="internal_priority"
|
||||||
|
class="text-xs"
|
||||||
|
>优先级</Label>
|
||||||
|
<Input
|
||||||
|
id="internal_priority"
|
||||||
|
v-model.number="form.internal_priority"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="10"
|
||||||
|
class="h-8"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
越小越优先
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="rpm_limit"
|
||||||
|
class="text-xs"
|
||||||
|
>RPM 限制</Label>
|
||||||
|
<Input
|
||||||
|
id="rpm_limit"
|
||||||
|
:model-value="form.rpm_limit ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="10000"
|
||||||
|
placeholder="自适应"
|
||||||
|
class="h-8"
|
||||||
|
@update:model-value="(v) => form.rpm_limit = parseNullableNumberInput(v, { min: 1, max: 10000 })"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
留空自适应
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="cache_ttl_minutes"
|
||||||
|
class="text-xs"
|
||||||
|
>缓存 TTL</Label>
|
||||||
|
<Input
|
||||||
|
id="cache_ttl_minutes"
|
||||||
|
:model-value="form.cache_ttl_minutes ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="60"
|
||||||
|
class="h-8"
|
||||||
|
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
分钟,0禁用
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="max_probe_interval_minutes"
|
||||||
|
class="text-xs"
|
||||||
|
>熔断探测</Label>
|
||||||
|
<Input
|
||||||
|
id="max_probe_interval_minutes"
|
||||||
|
:model-value="form.max_probe_interval_minutes ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="2"
|
||||||
|
max="32"
|
||||||
|
placeholder="32"
|
||||||
|
class="h-8"
|
||||||
|
@update:model-value="(v) => form.max_probe_interval_minutes = parseNumberInput(v, { min: 2, max: 32 }) ?? 32"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
2-32分钟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自动获取模型 -->
|
||||||
|
<div class="space-y-3 py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
定时更新上游模型, 配合模型映射使用
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="showAutoFetchWarning"
|
||||||
|
class="text-xs text-amber-600 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
已配置的模型权限将在下次获取时被覆盖
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="form.auto_fetch_models" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型过滤规则(仅当开启自动获取时显示) -->
|
||||||
|
<div
|
||||||
|
v-if="form.auto_fetch_models"
|
||||||
|
class="space-y-2 pt-2 border-t border-border/40"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="text-xs">包含规则</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.model_include_patterns_text"
|
||||||
|
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||||
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="text-xs">排除规则</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.model_exclude_patterns_text"
|
||||||
|
placeholder="*-preview, *-beta"
|
||||||
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="saving || !canSave"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
|
||||||
|
import { SquarePen } from 'lucide-vue-next'
|
||||||
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useFormDialog } from '@/composables/useFormDialog'
|
||||||
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
|
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
||||||
|
import {
|
||||||
|
updateProviderKey,
|
||||||
|
type EndpointAPIKey,
|
||||||
|
type EndpointAPIKeyUpdate,
|
||||||
|
} from '@/api/endpoints'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean
|
||||||
|
editingKey: EndpointAPIKey | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
saved: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { success, error: showError } = useToast()
|
||||||
|
|
||||||
|
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
|
||||||
|
const showAutoFetchWarning = computed(() => {
|
||||||
|
if (!props.editingKey) return false
|
||||||
|
// 原本已启用,不需要警告
|
||||||
|
if (props.editingKey.auto_fetch_models) return false
|
||||||
|
// 现在未启用,不需要警告
|
||||||
|
if (!form.value.auto_fetch_models) return false
|
||||||
|
// 检查是否有已配置的模型权限
|
||||||
|
const allowedModels = props.editingKey.allowed_models
|
||||||
|
if (!allowedModels) return false
|
||||||
|
if (Array.isArray(allowedModels) && allowedModels.length === 0) return false
|
||||||
|
if (typeof allowedModels === 'object' && Object.keys(allowedModels).length === 0) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单是否可以保存
|
||||||
|
const canSave = computed(() => {
|
||||||
|
// 必须填写名称
|
||||||
|
if (!form.value.name.trim()) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = computed(() => props.open)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
internal_priority: 10,
|
||||||
|
rpm_limit: undefined as number | null | undefined,
|
||||||
|
cache_ttl_minutes: 5,
|
||||||
|
max_probe_interval_minutes: 32,
|
||||||
|
note: '',
|
||||||
|
auto_fetch_models: false,
|
||||||
|
model_include_patterns_text: '',
|
||||||
|
model_exclude_patterns_text: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
function resetForm() {
|
||||||
|
form.value = {
|
||||||
|
name: '',
|
||||||
|
internal_priority: 10,
|
||||||
|
rpm_limit: undefined,
|
||||||
|
cache_ttl_minutes: 5,
|
||||||
|
max_probe_interval_minutes: 32,
|
||||||
|
note: '',
|
||||||
|
auto_fetch_models: false,
|
||||||
|
model_include_patterns_text: '',
|
||||||
|
model_exclude_patterns_text: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载密钥数据
|
||||||
|
function loadKeyData() {
|
||||||
|
if (!props.editingKey) return
|
||||||
|
form.value = {
|
||||||
|
name: props.editingKey.name,
|
||||||
|
internal_priority: props.editingKey.internal_priority ?? 10,
|
||||||
|
rpm_limit: props.editingKey.rpm_limit ?? undefined,
|
||||||
|
cache_ttl_minutes: props.editingKey.cache_ttl_minutes ?? 5,
|
||||||
|
max_probe_interval_minutes: props.editingKey.max_probe_interval_minutes ?? 32,
|
||||||
|
note: props.editingKey.note || '',
|
||||||
|
auto_fetch_models: props.editingKey.auto_fetch_models ?? false,
|
||||||
|
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
|
||||||
|
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 useFormDialog 统一处理对话框逻辑
|
||||||
|
const { handleDialogUpdate, handleCancel } = useFormDialog({
|
||||||
|
isOpen: () => props.open,
|
||||||
|
entity: () => props.editingKey,
|
||||||
|
isLoading: saving,
|
||||||
|
onClose: () => emit('close'),
|
||||||
|
loadData: loadKeyData,
|
||||||
|
resetForm,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将逗号分隔的文本解析为数组(去空、去重)
|
||||||
|
function parsePatternText(text: string): string[] {
|
||||||
|
if (!text.trim()) return []
|
||||||
|
const patterns = text
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
return [...new Set(patterns)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!props.editingKey) {
|
||||||
|
showError('无法保存:缺少账号信息', '错误')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const updateData: EndpointAPIKeyUpdate = {
|
||||||
|
name: form.value.name,
|
||||||
|
internal_priority: form.value.internal_priority,
|
||||||
|
rpm_limit: form.value.rpm_limit,
|
||||||
|
cache_ttl_minutes: form.value.cache_ttl_minutes,
|
||||||
|
max_probe_interval_minutes: form.value.max_probe_interval_minutes,
|
||||||
|
note: form.value.note,
|
||||||
|
auto_fetch_models: form.value.auto_fetch_models,
|
||||||
|
model_include_patterns: parsePatternText(form.value.model_include_patterns_text),
|
||||||
|
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProviderKey(props.editingKey.id, updateData)
|
||||||
|
success('账号已更新', '成功')
|
||||||
|
emit('saved')
|
||||||
|
emit('close')
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = parseApiError(err, '保存失败')
|
||||||
|
showError(errorMessage, '错误')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -383,7 +383,6 @@
|
|||||||
<RefreshCw class="w-3.5 h-3.5" />
|
<RefreshCw class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="key.auth_type !== 'oauth'"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@@ -393,7 +392,6 @@
|
|||||||
<Shield class="w-3.5 h-3.5" />
|
<Shield class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="key.auth_type !== 'oauth'"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@@ -870,6 +868,15 @@
|
|||||||
@saved="handleKeyChanged"
|
@saved="handleKeyChanged"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- OAuth 密钥编辑对话框 -->
|
||||||
|
<OAuthKeyEditDialog
|
||||||
|
v-if="open"
|
||||||
|
:open="oauthKeyEditDialogOpen"
|
||||||
|
:editing-key="editingKey"
|
||||||
|
@close="oauthKeyEditDialogOpen = false"
|
||||||
|
@saved="handleKeyChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 模型权限对话框 -->
|
<!-- 模型权限对话框 -->
|
||||||
<KeyAllowedModelsEditDialog
|
<KeyAllowedModelsEditDialog
|
||||||
v-if="open"
|
v-if="open"
|
||||||
@@ -964,7 +971,8 @@ import {
|
|||||||
KeyAllowedModelsEditDialog,
|
KeyAllowedModelsEditDialog,
|
||||||
ModelsTab,
|
ModelsTab,
|
||||||
BatchAssignModelsDialog,
|
BatchAssignModelsDialog,
|
||||||
OAuthAccountDialog
|
OAuthAccountDialog,
|
||||||
|
OAuthKeyEditDialog
|
||||||
} from '@/features/providers/components'
|
} from '@/features/providers/components'
|
||||||
import ModelMappingTab from '@/features/providers/components/provider-tabs/ModelMappingTab.vue'
|
import ModelMappingTab from '@/features/providers/components/provider-tabs/ModelMappingTab.vue'
|
||||||
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
|
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
|
||||||
@@ -1032,6 +1040,7 @@ const endpointDialogOpen = ref(false)
|
|||||||
const keyFormDialogOpen = ref(false)
|
const keyFormDialogOpen = ref(false)
|
||||||
const keyPermissionsDialogOpen = ref(false)
|
const keyPermissionsDialogOpen = ref(false)
|
||||||
const oauthAccountDialogOpen = ref(false)
|
const oauthAccountDialogOpen = ref(false)
|
||||||
|
const oauthKeyEditDialogOpen = ref(false)
|
||||||
const currentEndpoint = ref<ProviderEndpoint | null>(null)
|
const currentEndpoint = ref<ProviderEndpoint | null>(null)
|
||||||
const editingKey = ref<EndpointAPIKey | null>(null)
|
const editingKey = ref<EndpointAPIKey | null>(null)
|
||||||
const deleteKeyConfirmOpen = ref(false)
|
const deleteKeyConfirmOpen = ref(false)
|
||||||
@@ -1097,6 +1106,7 @@ const hasBlockingDialogOpen = computed(() =>
|
|||||||
keyFormDialogOpen.value ||
|
keyFormDialogOpen.value ||
|
||||||
keyPermissionsDialogOpen.value ||
|
keyPermissionsDialogOpen.value ||
|
||||||
oauthAccountDialogOpen.value ||
|
oauthAccountDialogOpen.value ||
|
||||||
|
oauthKeyEditDialogOpen.value ||
|
||||||
deleteKeyConfirmOpen.value ||
|
deleteKeyConfirmOpen.value ||
|
||||||
modelFormDialogOpen.value ||
|
modelFormDialogOpen.value ||
|
||||||
batchAssignDialogOpen.value ||
|
batchAssignDialogOpen.value ||
|
||||||
@@ -1175,6 +1185,7 @@ watch(
|
|||||||
keyFormDialogOpen.value = false
|
keyFormDialogOpen.value = false
|
||||||
keyPermissionsDialogOpen.value = false
|
keyPermissionsDialogOpen.value = false
|
||||||
oauthAccountDialogOpen.value = false
|
oauthAccountDialogOpen.value = false
|
||||||
|
oauthKeyEditDialogOpen.value = false
|
||||||
deleteKeyConfirmOpen.value = false
|
deleteKeyConfirmOpen.value = false
|
||||||
batchAssignDialogOpen.value = false
|
batchAssignDialogOpen.value = false
|
||||||
antigravityQuotaDialogOpen.value = false
|
antigravityQuotaDialogOpen.value = false
|
||||||
@@ -1297,7 +1308,12 @@ function handleAddKeyToFirstEndpoint() {
|
|||||||
function handleEditKey(endpoint: ProviderEndpoint | undefined, key: EndpointAPIKey) {
|
function handleEditKey(endpoint: ProviderEndpoint | undefined, key: EndpointAPIKey) {
|
||||||
currentEndpoint.value = endpoint || null
|
currentEndpoint.value = endpoint || null
|
||||||
editingKey.value = key
|
editingKey.value = key
|
||||||
keyFormDialogOpen.value = true
|
// OAuth 密钥使用专门的编辑对话框
|
||||||
|
if (key.auth_type === 'oauth') {
|
||||||
|
oauthKeyEditDialogOpen.value = true
|
||||||
|
} else {
|
||||||
|
keyFormDialogOpen.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyPermissions(key: EndpointAPIKey) {
|
function handleKeyPermissions(key: EndpointAPIKey) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
|
|||||||
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
||||||
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
|
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
|
||||||
export { default as OAuthAccountDialog } from './OAuthAccountDialog.vue'
|
export { default as OAuthAccountDialog } from './OAuthAccountDialog.vue'
|
||||||
|
export { default as OAuthKeyEditDialog } from './OAuthKeyEditDialog.vue'
|
||||||
|
|
||||||
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'
|
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'
|
||||||
export { default as ProviderAuthDialog } from './ProviderAuthDialog.vue'
|
export { default as ProviderAuthDialog } from './ProviderAuthDialog.vue'
|
||||||
|
|||||||
@@ -18,37 +18,13 @@ from urllib.parse import urlencode
|
|||||||
from src.core.logger import logger
|
from src.core.logger import logger
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fixed model catalog
|
# Preset model catalog
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Codex upstream (chatgpt.com/backend-api/codex) has no /v1/models endpoint.
|
# Codex upstream (chatgpt.com/backend-api/codex) has no /v1/models endpoint.
|
||||||
# Return a static list of known models.
|
# We use the unified preset models registry from preset_models.py.
|
||||||
_CODEX_MODELS: list[dict[str, Any]] = [
|
from src.services.provider.preset_models import create_preset_models_fetcher
|
||||||
{
|
|
||||||
"id": "gpt-5.2",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "openai",
|
|
||||||
"display_name": "gpt-5.2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gpt-5.2-codex",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "openai",
|
|
||||||
"display_name": "gpt-5.2-codex",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
fetch_models_codex = create_preset_models_fetcher("codex")
|
||||||
async def fetch_models_codex(
|
|
||||||
ctx: Any,
|
|
||||||
timeout_seconds: float, # noqa: ARG001
|
|
||||||
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
|
||||||
"""Return a fixed model catalog for Codex.
|
|
||||||
|
|
||||||
Codex upstream does not expose a ``/v1/models`` endpoint, so we skip the
|
|
||||||
HTTP call entirely and return a hardcoded list.
|
|
||||||
"""
|
|
||||||
_ = ctx
|
|
||||||
return list(_CODEX_MODELS), [], True, None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -23,49 +23,13 @@ from src.services.provider.adapters.kiro.constants import (
|
|||||||
from src.services.provider.adapters.kiro.context import get_kiro_request_context
|
from src.services.provider.adapters.kiro.context import get_kiro_request_context
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fixed model catalog
|
# Preset model catalog
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Kiro upstream has no /v1/models endpoint. We return a static list matching
|
# Kiro upstream has no /v1/models endpoint. We use the unified preset models
|
||||||
# the models accepted by map_model() in converter.py.
|
# registry from preset_models.py.
|
||||||
_KIRO_MODELS: list[dict[str, Any]] = [
|
from src.services.provider.preset_models import create_preset_models_fetcher
|
||||||
{
|
|
||||||
"id": "claude-sonnet-4.5",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "anthropic",
|
|
||||||
"display_name": "Claude Sonnet 4.5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "claude-opus-4.5",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "anthropic",
|
|
||||||
"display_name": "Claude Opus 4.5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "claude-opus-4.6",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "anthropic",
|
|
||||||
"display_name": "Claude Opus 4.6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "claude-haiku-4.5",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "anthropic",
|
|
||||||
"display_name": "Claude Haiku 4.5",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
fetch_models_kiro = create_preset_models_fetcher("kiro")
|
||||||
async def fetch_models_kiro(
|
|
||||||
ctx: Any,
|
|
||||||
timeout_seconds: float, # noqa: ARG001
|
|
||||||
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
|
||||||
"""Return a fixed model catalog for Kiro.
|
|
||||||
|
|
||||||
Kiro upstream does not expose a ``/v1/models`` endpoint, so we skip the
|
|
||||||
HTTP call entirely and return a hardcoded list.
|
|
||||||
"""
|
|
||||||
_ = ctx # not needed — no upstream call
|
|
||||||
return list(_KIRO_MODELS), [], True, None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
158
src/services/provider/preset_models.py
Normal file
158
src/services/provider/preset_models.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
"""预设模型管理模块
|
||||||
|
|
||||||
|
为不支持自动获取模型的反代提供商(如 Kiro、Codex)提供统一的预设模型管理。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
1. 在 PRESET_MODELS 中定义各 provider_type 的预设模型列表
|
||||||
|
2. 在 plugin.py 中调用 create_preset_models_fetcher() 创建 fetcher 函数
|
||||||
|
3. 将 fetcher 注册到 UpstreamModelsFetcherRegistry
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 预设模型定义
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 各 provider_type 对应的预设模型列表
|
||||||
|
# 格式: provider_type -> list of model dicts
|
||||||
|
|
||||||
|
PRESET_MODELS: dict[str, list[dict[str, Any]]] = {
|
||||||
|
# Kiro (Claude CLI 反代)
|
||||||
|
"kiro": [
|
||||||
|
{
|
||||||
|
"id": "claude-sonnet-4.5",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "anthropic",
|
||||||
|
"display_name": "Claude Sonnet 4.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "claude-opus-4.5",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "anthropic",
|
||||||
|
"display_name": "Claude Opus 4.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "claude-opus-4.6",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "anthropic",
|
||||||
|
"display_name": "Claude Opus 4.6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "claude-haiku-4.5",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "anthropic",
|
||||||
|
"display_name": "Claude Haiku 4.5",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
# Codex (OpenAI CLI 反代)
|
||||||
|
"codex": [
|
||||||
|
{
|
||||||
|
"id": "gpt-5",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5-codex",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5 Codex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5-codex-mini",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5 Codex Mini",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.1",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.1-codex",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.1 Codex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.1-codex-mini",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.1 Codex Mini",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.1-codex-max",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.1 Codex Max",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.2",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.2-codex",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.2 Codex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-5.3-codex",
|
||||||
|
"object": "model",
|
||||||
|
"owned_by": "openai",
|
||||||
|
"display_name": "GPT-5.3 Codex",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fetcher 工厂函数
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def get_preset_models(provider_type: str) -> list[dict[str, Any]]:
|
||||||
|
"""获取指定 provider_type 的预设模型列表。"""
|
||||||
|
return list(PRESET_MODELS.get(provider_type.lower(), []))
|
||||||
|
|
||||||
|
|
||||||
|
def create_preset_models_fetcher(
|
||||||
|
provider_type: str,
|
||||||
|
) -> Any:
|
||||||
|
"""创建一个返回预设模型列表的 fetcher 函数。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider_type: 提供商类型(如 "kiro", "codex")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
符合 UpstreamModelsFetcherRegistry 签名的 async fetcher 函数
|
||||||
|
"""
|
||||||
|
models = get_preset_models(provider_type)
|
||||||
|
|
||||||
|
async def fetch_preset_models(
|
||||||
|
ctx: Any,
|
||||||
|
timeout_seconds: float, # noqa: ARG001
|
||||||
|
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
|
||||||
|
"""Return preset model catalog.
|
||||||
|
|
||||||
|
This provider does not expose a /v1/models endpoint, so we skip the
|
||||||
|
HTTP call entirely and return a hardcoded list.
|
||||||
|
"""
|
||||||
|
_ = ctx
|
||||||
|
_ = timeout_seconds
|
||||||
|
return list(models), [], True, None
|
||||||
|
|
||||||
|
return fetch_preset_models
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PRESET_MODELS",
|
||||||
|
"create_preset_models_fetcher",
|
||||||
|
"get_preset_models",
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user