mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 候选排序引入 API 格式偏好, 追踪页面展示完整格式转换信息
- 候选排序优先同 kind (chat/cli) 再同 family, 替代原有固定顺序 - 不再隐藏 format_conversion_disabled 候选, 保留完整追踪链路 - DecisionTrace 新增 provider/endpoint/key 格式转换相关字段 - 前端追踪面板新增 Key 支持端点和转换策略展示
This commit is contained in:
@@ -10,13 +10,18 @@ export interface CandidateRecord {
|
||||
provider_website?: string // Provider 官网
|
||||
provider_priority?: number
|
||||
provider_keep_priority_on_conversion?: boolean
|
||||
provider_enable_format_conversion?: boolean
|
||||
endpoint_id?: string
|
||||
endpoint_name?: string // 端点显示名称(api_format)
|
||||
endpoint_api_family?: string
|
||||
endpoint_kind?: string
|
||||
endpoint_format_acceptance_config?: Record<string, unknown> | null
|
||||
key_id?: string
|
||||
key_name?: string // 密钥名称
|
||||
key_account_label?: string // 更适合展示的测试账号标签(优先 OAuth 邮箱)
|
||||
key_preview?: string // 密钥脱敏预览(如 sk-***abc),OAuth 类型不返回
|
||||
key_auth_type?: string // 密钥认证类型(api_key, service_account, oauth 等)
|
||||
key_api_formats?: unknown
|
||||
key_internal_priority?: number
|
||||
key_global_priority_by_format?: Record<string, number> | null
|
||||
key_oauth_plan_type?: string // OAuth 账号套餐类型(free/plus/team/enterprise)
|
||||
|
||||
@@ -250,6 +250,30 @@
|
||||
>{{ currentAttempt.key_preview }}</code>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentAttemptKeyFormatsDisplay"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">支持端点</span>
|
||||
<span class="info-value info-value-stacked">
|
||||
<code class="format-code">{{ currentAttemptKeyFormatsDisplay }}</code>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
同一 Key 的不同 endpoint 会分别参与候选与转换判定
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentAttemptConversionInfo"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">转换策略</span>
|
||||
<span class="info-value info-value-stacked">
|
||||
<code class="format-code">{{ currentAttemptConversionInfo.summary }}</code>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ currentAttemptConversionInfo.hint }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentAttempt.extra_data?.proxy"
|
||||
class="info-item"
|
||||
@@ -1122,6 +1146,31 @@ const normalizeFormatSignature = (value: string): string => {
|
||||
return value.trim().toLowerCase()
|
||||
}
|
||||
|
||||
const extractObject = (value: unknown): Record<string, unknown> | null => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
const extractStringList = (value: unknown): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map(item => typeof item === 'string' ? item.trim() : '')
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const raw = value.trim()
|
||||
if (!raw) return []
|
||||
try {
|
||||
return extractStringList(JSON.parse(raw))
|
||||
} catch {
|
||||
return [raw]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const normalizePriorityNumber = (value: unknown): number | null => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return Math.trunc(value)
|
||||
@@ -1163,14 +1212,22 @@ const resolveProviderApiFormat = (attempt: CandidateRecord): string => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveTransportDiagnostics = (attempt: CandidateRecord): Record<string, unknown> | null => {
|
||||
const extra = extractObject(attempt.extra_data)
|
||||
return extractObject(extra?.transport_diagnostics)
|
||||
}
|
||||
|
||||
const resolveEndpointFormatAcceptanceConfig = (attempt: CandidateRecord): Record<string, unknown> | null => {
|
||||
const fromAttempt = extractObject(attempt.endpoint_format_acceptance_config)
|
||||
if (fromAttempt) return fromAttempt
|
||||
const transport = resolveTransportDiagnostics(attempt)
|
||||
return extractObject(transport?.endpoint_format_acceptance_config)
|
||||
}
|
||||
|
||||
const currentAttemptFormatDisplay = computed(() => {
|
||||
const attempt = currentAttempt.value
|
||||
if (!attempt) return ''
|
||||
const extra = (
|
||||
attempt.extra_data && typeof attempt.extra_data === 'object' && !Array.isArray(attempt.extra_data)
|
||||
? attempt.extra_data
|
||||
: {}
|
||||
) as Record<string, unknown>
|
||||
const extra = extractObject(attempt.extra_data) ?? {}
|
||||
|
||||
const providerRaw = typeof extra.provider_api_format === 'string' ? extra.provider_api_format : ''
|
||||
const clientRawFromExtra = typeof extra.client_api_format === 'string' ? extra.client_api_format : ''
|
||||
@@ -1224,14 +1281,14 @@ const currentAttemptSchedulerInfo = computed<{
|
||||
)
|
||||
const keepPriorityOnConversion = attempt.provider_keep_priority_on_conversion === true
|
||||
|
||||
let hint = '链路按实际调度顺序展示'
|
||||
let hint = '顺位展示 Provider / Key 优先级;不同 endpoint 独立参与候选'
|
||||
if (globalPriority !== null) {
|
||||
hint = `当前格式 ${formatApiFormat(clientApiFormat)} 先看全局 Key 优先级`
|
||||
hint = `当前格式 ${formatApiFormat(clientApiFormat)} 先看全局 Key 优先级;不同 endpoint 单独判定`
|
||||
}
|
||||
if (isCrossFormat) {
|
||||
hint = keepPriorityOnConversion
|
||||
? '跨格式候选已开启保持优先级'
|
||||
: '跨格式候选默认排在同格式候选之后'
|
||||
? '跨格式候选已开启保持优先级;同一 Key 的不同 endpoint 独立参与'
|
||||
: '跨格式候选默认排在同格式候选之后;同一 Key 的不同 endpoint 独立参与'
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1242,6 +1299,77 @@ const currentAttemptSchedulerInfo = computed<{
|
||||
}
|
||||
})
|
||||
|
||||
const currentAttemptKeyFormatsDisplay = computed(() => {
|
||||
const attempt = currentAttempt.value
|
||||
if (!attempt) return ''
|
||||
|
||||
const formats = extractStringList(attempt.key_api_formats)
|
||||
if (!formats.length) return ''
|
||||
|
||||
return formats
|
||||
.map(format => formatApiFormat(format))
|
||||
.join(' / ')
|
||||
})
|
||||
|
||||
const currentAttemptConversionInfo = computed<{
|
||||
summary: string
|
||||
hint: string
|
||||
} | null>(() => {
|
||||
const attempt = currentAttempt.value
|
||||
if (!attempt) return null
|
||||
|
||||
const clientApiFormat = resolveClientApiFormat(attempt)
|
||||
const providerApiFormat = resolveProviderApiFormat(attempt)
|
||||
if (!clientApiFormat || !providerApiFormat) return null
|
||||
|
||||
const isCrossFormat = normalizeFormatSignature(clientApiFormat) !== normalizeFormatSignature(providerApiFormat)
|
||||
if (!isCrossFormat) {
|
||||
return {
|
||||
summary: '同格式直连',
|
||||
hint: '当前候选直接命中 endpoint 原生格式',
|
||||
}
|
||||
}
|
||||
|
||||
const transportDiagnostics = resolveTransportDiagnostics(attempt)
|
||||
const providerEnabled = attempt.provider_enable_format_conversion === true
|
||||
|| transportDiagnostics?.provider_enable_format_conversion === true
|
||||
const endpointConfig = resolveEndpointFormatAcceptanceConfig(attempt)
|
||||
const endpointRuleEnabled = endpointConfig
|
||||
? endpointConfig.enabled !== false
|
||||
: false
|
||||
const acceptFormats = extractStringList(endpointConfig?.accept_formats)
|
||||
const rejectFormats = extractStringList(endpointConfig?.reject_formats)
|
||||
const normalizedClientFormat = normalizeFormatSignature(clientApiFormat)
|
||||
const endpointAcceptsClient = acceptFormats.some(
|
||||
format => normalizeFormatSignature(format) === normalizedClientFormat,
|
||||
)
|
||||
const endpointRejectsClient = rejectFormats.some(
|
||||
format => normalizeFormatSignature(format) === normalizedClientFormat,
|
||||
)
|
||||
|
||||
let summary = '未开启格式转换'
|
||||
if (providerEnabled && endpointRuleEnabled) {
|
||||
summary = 'Provider 总开关 + Endpoint 规则'
|
||||
} else if (providerEnabled) {
|
||||
summary = 'Provider 总格式转换'
|
||||
} else if (endpointRuleEnabled) {
|
||||
summary = 'Endpoint 独立格式转换'
|
||||
}
|
||||
|
||||
let hint = '同一 Key 的不同 endpoint 会分别判定格式转换'
|
||||
if (endpointRejectsClient) {
|
||||
hint = `当前 endpoint 明确拒绝 ${formatApiFormat(clientApiFormat)}`
|
||||
} else if (endpointAcceptsClient) {
|
||||
hint = `当前 endpoint 明确接受 ${formatApiFormat(clientApiFormat)}`
|
||||
} else if (providerEnabled) {
|
||||
hint = '当前跨格式由 Provider 总开关放行'
|
||||
} else if (attempt.skip_reason === 'format_conversion_disabled') {
|
||||
hint = 'Provider 总开关关闭,且当前 endpoint 未单独放行'
|
||||
}
|
||||
|
||||
return { summary, hint }
|
||||
})
|
||||
|
||||
// 计算当前尝试启用的能力标签(请求需要的能力)
|
||||
const activeCapabilities = computed(() => {
|
||||
if (!currentAttempt.value?.required_capabilities) return []
|
||||
|
||||
Reference in New Issue
Block a user