feat(provider,adapter): Codex 默认 body_rules 按 provider_type 维度注册,adapter 全链路传递 provider_type

- 新增 register_provider_default_body_rules 注册机制,将 Codex 特有的
  body_rules 从 EndpointDefinition 全局默认移至 codex plugin 按
  (provider_type, endpoint_sig) 维度注册
- handler adapter 的 build_endpoint_url/build_request_body/get_cli_extra_headers
  增加 provider_type 参数,Codex 判断优先使用 provider_type 而非 URL 匹配
- 前端 getDefaultBodyRules API 支持 provider_type 参数,缓存 key 区分不同
  provider 类型;Codex 路径判断同样优先使用 provider_type
- ProviderDetailDrawer 将 mapping-preview 拆为独立加载,不阻塞首屏渲染
- PoolManagement 补全 KeyFormDialog 缺失的 endpoint/available-api-formats props
- 固定类型 Provider 创建时自动填充 provider-scoped 默认 body_rules
This commit is contained in:
fawney19
2026-03-05 19:04:21 +08:00
parent 694167f78f
commit da915208a8
15 changed files with 214 additions and 54 deletions

View File

@@ -72,7 +72,9 @@ export async function deleteEndpoint(endpointId: string): Promise<{ message: str
/**
* 获取指定 API 格式的默认请求体规则
*/
export async function getDefaultBodyRules(apiFormat: string): Promise<{ api_format: string; body_rules: BodyRule[] }> {
const response = await client.get(`/api/admin/endpoints/defaults/${encodeURIComponent(apiFormat)}/body-rules`)
export async function getDefaultBodyRules(apiFormat: string, providerType?: string): Promise<{ api_format: string; body_rules: BodyRule[] }> {
const params: Record<string, string> = {}
if (providerType) params.provider_type = providerType
const response = await client.get(`/api/admin/endpoints/defaults/${encodeURIComponent(apiFormat)}/body-rules`, { params })
return response.data
}

View File

@@ -1280,30 +1280,30 @@ const deleteConfirmDescription = computed(() => {
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
if (!apiFormat) return []
if (!force && defaultBodyRulesLoaded.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
const providerType = (props.provider?.provider_type || '').toLowerCase()
// 缓存 key 需要包含 provider_type不同类型的 provider 有不同的默认规则
const cacheKey = providerType ? `${apiFormat}:${providerType}` : apiFormat
if (!force && defaultBodyRulesLoaded.value[cacheKey]) {
return defaultBodyRulesByFormat.value[cacheKey] || []
}
if (loadingDefaultBodyRulesByFormat.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
if (loadingDefaultBodyRulesByFormat.value[cacheKey]) {
return defaultBodyRulesByFormat.value[cacheKey] || []
}
loadingDefaultBodyRulesByFormat.value[apiFormat] = true
loadingDefaultBodyRulesByFormat.value[cacheKey] = true
try {
const response = await getDefaultBodyRules(apiFormat)
const normalized = response.api_format || apiFormat
const response = await getDefaultBodyRules(apiFormat, providerType || undefined)
const rules = response.body_rules || []
defaultBodyRulesByFormat.value[normalized] = rules
defaultBodyRulesByFormat.value[apiFormat] = rules
defaultBodyRulesLoaded.value[normalized] = true
defaultBodyRulesLoaded.value[apiFormat] = true
defaultBodyRulesByFormat.value[cacheKey] = rules
defaultBodyRulesLoaded.value[cacheKey] = true
return rules
} catch (error: unknown) {
defaultBodyRulesByFormat.value[apiFormat] = []
defaultBodyRulesLoaded.value[apiFormat] = true
defaultBodyRulesByFormat.value[cacheKey] = []
defaultBodyRulesLoaded.value[cacheKey] = true
log.warn('加载默认请求体规则失败', apiFormat, error)
return []
} finally {
loadingDefaultBodyRulesByFormat.value[apiFormat] = false
loadingDefaultBodyRulesByFormat.value[cacheKey] = false
}
}
@@ -1317,7 +1317,11 @@ function getDefaultPath(apiFormat: string, baseUrl?: string): string {
const format = apiFormats.value.find(f => f.value === apiFormat)
const defaultPath = format?.default_path || ''
// Codex 端点使用 /responses 而非 /v1/responses
if (apiFormat === 'openai:cli' && baseUrl && isCodexUrl(baseUrl)) {
const providerType = (props.provider?.provider_type || '').toLowerCase()
const isCodex = providerType
? providerType === 'codex'
: (!!baseUrl && isCodexUrl(baseUrl))
if (apiFormat === 'openai:cli' && isCodex) {
return '/responses'
}
return defaultPath

View File

@@ -1274,6 +1274,8 @@ watch(
[() => props.providerId, () => props.open],
async ([newId, newOpen], [_oldId, oldOpen]) => {
if (newOpen && newId) {
// mapping-preview 较慢,不阻塞首屏渲染
void loadMappingPreview()
await Promise.all([
loadProvider(),
loadEndpoints(),
@@ -1958,6 +1960,7 @@ async function handleBatchAssignChanged() {
// 处理模型映射变更
async function handleModelMappingChanged() {
void loadMappingPreview()
await loadEndpoints()
emit('refresh')
}
@@ -2621,18 +2624,17 @@ async function loadEndpoints() {
const requestId = ++endpointsLoadRequestId
try {
// 并行加载端点列表、Provider 级别的 keysmodels 和映射预览
const [endpointsList, providerKeysResult, modelsResult, mappingPreviewResult] = await Promise.all([
// 并行加载端点列表、Provider 级别的 keysmodels
// mapping-preview 较慢,拆到 loadMappingPreview 独立加载,不阻塞首屏
const [endpointsList, providerKeysResult, modelsResult] = await Promise.all([
getProviderEndpoints(props.providerId),
getProviderKeys(props.providerId).catch(() => []),
getProviderModels(props.providerId).catch(() => []),
getProviderMappingPreview(props.providerId).catch(() => null),
])
if (requestId !== endpointsLoadRequestId) return
providerKeys.value = providerKeysResult
providerModels.value = modelsResult
providerMappingPreview.value = mappingPreviewResult
// 按 API 格式排序
endpoints.value = endpointsList.sort((a, b) => {
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
@@ -2648,6 +2650,16 @@ async function loadEndpoints() {
}
}
// 加载映射预览(独立于 loadEndpoints不阻塞首屏渲染
async function loadMappingPreview() {
if (!props.providerId) return
try {
providerMappingPreview.value = await getProviderMappingPreview(props.providerId)
} catch {
providerMappingPreview.value = null
}
}
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.open) {

View File

@@ -1083,9 +1083,11 @@
<KeyFormDialog
v-if="selectedProviderId"
:open="keyFormDialogOpen"
:endpoint="null"
:provider-type="selectedProviderData?.provider_type || selectedProviderType"
:editing-key="editingKey"
:provider-id="selectedProviderId"
:available-api-formats="selectedProviderData?.api_formats || []"
@close="closeKeyFormDialog"
@saved="handleDialogSaved"
/>