feat: 实现跨 API 格式自动转换功能

- 新增端点级 format_acceptance_config 配置,控制是否接受跨格式请求
- 重构 EndpointFormDialog 为卡片式布局,支持内联编辑和格式转换开关
- StreamProcessor 实现流式响应的跨格式转换,支持 OpenAI/Claude/Gemini 互转
- CacheAwareScheduler 按端点格式筛选候选,同格式优先于跨格式
- 健康度/熔断按 Provider 端点格式分桶,而非客户端请求格式
- 新增 format_conversion_total 和 format_conversion_duration_seconds 指标
- 新增全局配置 format_conversion_enabled 控制总开关
- Input 组件新增 size="sm" 尺寸选项
This commit is contained in:
fawney19
2026-01-22 01:48:56 +08:00
parent 99388bfa33
commit cc5db20c58
25 changed files with 1818 additions and 547 deletions

View File

@@ -1,5 +1,5 @@
import client from '../client'
import type { ProviderEndpoint, ProxyConfig, HeaderRule } from './types'
import type { ProviderEndpoint, ProxyConfig, HeaderRule, FormatAcceptanceConfig } from './types'
/**
* 获取指定 Provider 的所有 Endpoints
@@ -32,6 +32,7 @@ export async function createEndpoint(
is_active?: boolean
config?: Record<string, any>
proxy?: ProxyConfig | null
format_acceptance_config?: FormatAcceptanceConfig | null
}
): Promise<ProviderEndpoint> {
const response = await client.post(`/api/admin/endpoints/providers/${providerId}/endpoints`, data)
@@ -51,6 +52,7 @@ export async function updateEndpoint(
is_active: boolean
config: Record<string, any>
proxy: ProxyConfig | null
format_acceptance_config: FormatAcceptanceConfig | null
}>
): Promise<ProviderEndpoint> {
const response = await client.put(`/api/admin/endpoints/${endpointId}`, data)

View File

@@ -87,6 +87,16 @@ export interface HeaderRuleRename {
export type HeaderRule = HeaderRuleSet | HeaderRuleDrop | HeaderRuleRename
/**
* 格式接受策略配置
* 用于控制端点是否接受来自不同 API 格式的请求,并自动进行格式转换
*/
export interface FormatAcceptanceConfig {
enabled: boolean // 是否启用格式转换
accept_formats?: string[] // 白名单:接受哪些格式的请求
reject_formats?: string[] // 黑名单:拒绝哪些格式(优先级高于白名单)
}
export interface ProviderEndpoint {
id: string
provider_id: string
@@ -100,6 +110,8 @@ export interface ProviderEndpoint {
is_active: boolean
config?: Record<string, any>
proxy?: ProxyConfig | null
// 格式转换配置
format_acceptance_config?: FormatAcceptanceConfig | null
total_keys: number
active_keys: number
created_at: string

View File

@@ -8,7 +8,7 @@ const props = defineProps<CollapsibleContentProps & { class?: string }>()
<template>
<CollapsibleContent
v-bind="props"
:class="cn('overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class)"
:class="cn('data-[state=closed]:overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class)"
>
<slot />
</CollapsibleContent>

View File

@@ -75,6 +75,12 @@ interface Props {
modelValue?: string | number
class?: string
autocomplete?: string
/**
* 输入框尺寸
* - 'default': 默认尺寸 (h-11, py-2)
* - 'sm': 小尺寸 (h-8, py-1)
*/
size?: 'default' | 'sm'
/**
* 遮蔽显示内容(用于 API Key 等敏感信息)
* 使用 CSS -webkit-text-security 实现,不会触发浏览器密码管理器
@@ -150,9 +156,16 @@ const autocompleteAttr = computed(() => {
return props.autocomplete ?? 'off'
})
// 尺寸相关的样式
const sizeClasses = {
default: 'h-11 py-2 px-4',
sm: 'h-8 py-1 px-3'
}
const inputClass = computed(() =>
cn(
'flex h-11 w-full rounded-xl border border-border/60 bg-muted/50 px-4 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary/60 text-foreground transition-all',
'flex w-full rounded-xl border border-border/60 bg-muted/50 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary/60 text-foreground transition-all',
sizeClasses[props.size || 'default'],
props.masked && 'pr-10',
props.class
)