mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 统一测试请求构建逻辑,支持多格式测试
- 重构 adapter 基类的 build_request_body 方法,使用 converter_registry 自动处理格式转换 - 后端 test_model 接口增加 endpoint_id 和 api_format 参数支持 - 前端模型映射测试支持根据 Key 和端点配置动态显示可用格式下拉菜单
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import client from '../client'
|
import client from '../client'
|
||||||
import type { EndpointAPIKey, AllowedModels } from './types'
|
import type { EndpointAPIKey, AllowedModels } from './types'
|
||||||
|
|
||||||
|
// Re-export types for convenience
|
||||||
|
export type { EndpointAPIKey, AllowedModels }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 能力定义类型
|
* 能力定义类型
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export interface TestModelRequest {
|
|||||||
provider_id: string
|
provider_id: string
|
||||||
model_name: string
|
model_name: string
|
||||||
api_key_id?: string
|
api_key_id?: string
|
||||||
|
endpoint_id?: string
|
||||||
message?: string
|
message?: string
|
||||||
api_format?: string
|
api_format?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,13 +150,47 @@
|
|||||||
<span class="font-mono text-sm truncate">
|
<span class="font-mono text-sm truncate">
|
||||||
{{ mapping.name }}
|
{{ mapping.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<!-- 测试按钮(支持多格式选择) -->
|
||||||
|
<DropdownMenu
|
||||||
|
v-if="getItemAvailableFormats(item).length > 1"
|
||||||
|
v-model:open="formatMenuOpen[`${item.key}-${mapping.name}`]"
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7 shrink-0"
|
||||||
|
title="测试映射"
|
||||||
|
:disabled="testingMapping === `${item.key}-${mapping.name}`"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="testingMapping === `${item.key}-${mapping.name}`"
|
||||||
|
class="w-3 h-3 animate-spin"
|
||||||
|
/>
|
||||||
|
<Play
|
||||||
|
v-else
|
||||||
|
class="w-3 h-3"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="fmt in getItemAvailableFormats(item)"
|
||||||
|
:key="fmt"
|
||||||
|
@select="testMapping(item, mapping, fmt)"
|
||||||
|
>
|
||||||
|
{{ fmt }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
|
v-else
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7 shrink-0"
|
class="h-7 w-7 shrink-0"
|
||||||
title="测试映射"
|
title="测试映射"
|
||||||
:disabled="testingMapping === `${item.key}-${mapping.name}`"
|
:disabled="testingMapping === `${item.key}-${mapping.name}` || getItemAvailableFormats(item).length === 0"
|
||||||
@click="testMapping(item, mapping)"
|
@click="testMapping(item, mapping, getItemAvailableFormats(item)[0])"
|
||||||
>
|
>
|
||||||
<Loader2
|
<Loader2
|
||||||
v-if="testingMapping === `${item.key}-${mapping.name}`"
|
v-if="testingMapping === `${item.key}-${mapping.name}`"
|
||||||
@@ -281,7 +315,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
||||||
import { Card, Button, Badge } from '@/components/ui'
|
import {
|
||||||
|
Card, Button, Badge,
|
||||||
|
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem
|
||||||
|
} from '@/components/ui'
|
||||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||||
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
@@ -293,6 +330,9 @@ import {
|
|||||||
type ProviderModelAlias,
|
type ProviderModelAlias,
|
||||||
type ProviderMappingPreviewResponse
|
type ProviderMappingPreviewResponse
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
|
import { getProviderEndpoints } from '@/api/endpoints/endpoints'
|
||||||
|
import { getProviderKeys, type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||||
|
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
||||||
import { updateModel } from '@/api/endpoints/models'
|
import { updateModel } from '@/api/endpoints/models'
|
||||||
import { parseTestModelError } from '@/utils/errorParser'
|
import { parseTestModelError } from '@/utils/errorParser'
|
||||||
|
|
||||||
@@ -341,6 +381,15 @@ const deletingGroup = ref<AliasGroup | null>(null)
|
|||||||
const testingMapping = ref<string | null>(null)
|
const testingMapping = ref<string | null>(null)
|
||||||
const preselectedModelId = ref<string | null>(null)
|
const preselectedModelId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 端点数据(用于测试格式选择)
|
||||||
|
const providerEndpoints = ref<ProviderEndpoint[]>([])
|
||||||
|
|
||||||
|
// Key 数据(用于判断支持的格式)
|
||||||
|
const providerKeys = ref<EndpointAPIKey[]>([])
|
||||||
|
|
||||||
|
// 测试下拉菜单状态
|
||||||
|
const formatMenuOpen = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
// 展开状态
|
// 展开状态
|
||||||
const expandedItems = ref<Set<number>>(new Set())
|
const expandedItems = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
@@ -470,12 +519,16 @@ const combinedMappings = computed<CombinedMapping[]>(() => {
|
|||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const [modelsData, previewData] = await Promise.all([
|
const [modelsData, previewData, endpointsData, keysData] = await Promise.all([
|
||||||
getProviderModels(props.provider.id),
|
getProviderModels(props.provider.id),
|
||||||
getProviderMappingPreview(props.provider.id).catch(() => null)
|
getProviderMappingPreview(props.provider.id).catch(() => null),
|
||||||
|
getProviderEndpoints(props.provider.id).catch(() => []),
|
||||||
|
getProviderKeys(props.provider.id).catch(() => [])
|
||||||
])
|
])
|
||||||
models.value = modelsData
|
models.value = modelsData
|
||||||
aliasMappingPreview.value = previewData
|
aliasMappingPreview.value = previewData
|
||||||
|
providerEndpoints.value = endpointsData
|
||||||
|
providerKeys.value = keysData
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -566,16 +619,69 @@ async function onDialogSaved() {
|
|||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试模型映射(精确映射)
|
// 获取可用的 API 格式(有活跃端点,去重)
|
||||||
async function testMapping(item: CombinedMapping, mapping: MappingItem) {
|
const availableApiFormats = computed(() => {
|
||||||
|
const formats = new Set(
|
||||||
|
providerEndpoints.value
|
||||||
|
.filter(ep => ep.is_active)
|
||||||
|
.map(ep => ep.api_format)
|
||||||
|
)
|
||||||
|
return [...formats]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取映射项支持的 API 格式
|
||||||
|
// 逻辑:找到支持该映射格式的所有活跃 Key,获取这些 Key 支持的所有格式,与活跃端点格式取交集
|
||||||
|
function getItemAvailableFormats(item: CombinedMapping): string[] {
|
||||||
|
// 精确映射:基于 group.apiFormats 筛选
|
||||||
|
if (item.type === 'exact' && item.group?.apiFormats && item.group.apiFormats.length > 0) {
|
||||||
|
const mappingFormats = item.group.apiFormats
|
||||||
|
|
||||||
|
// 找到所有支持该映射格式的活跃 Key
|
||||||
|
const supportingKeys = providerKeys.value.filter(key => {
|
||||||
|
if (!key.is_active) return false
|
||||||
|
// Key 的 api_formats 与映射的 apiFormats 有交集
|
||||||
|
return key.api_formats?.some(fmt => mappingFormats.includes(fmt))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (supportingKeys.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集这些 Key 支持的所有格式
|
||||||
|
const keyFormats = new Set<string>()
|
||||||
|
for (const key of supportingKeys) {
|
||||||
|
for (const fmt of key.api_formats || []) {
|
||||||
|
keyFormats.add(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与活跃端点格式取交集
|
||||||
|
return availableApiFormats.value.filter(fmt => keyFormats.has(fmt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正则映射或无限制:返回所有有活跃 Key 支持的端点格式
|
||||||
|
const allKeyFormats = new Set<string>()
|
||||||
|
for (const key of providerKeys.value) {
|
||||||
|
if (!key.is_active) continue
|
||||||
|
for (const fmt of key.api_formats || []) {
|
||||||
|
allKeyFormats.add(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return availableApiFormats.value.filter(fmt => allKeyFormats.has(fmt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试精确映射(直接发请求或显示下拉菜单选择格式)
|
||||||
|
async function testMapping(item: CombinedMapping, mapping: MappingItem, apiFormat?: string) {
|
||||||
const testingKey = `${item.key}-${mapping.name}`
|
const testingKey = `${item.key}-${mapping.name}`
|
||||||
testingMapping.value = testingKey
|
testingMapping.value = testingKey
|
||||||
|
formatMenuOpen.value[testingKey] = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await testModel({
|
const result = await testModel({
|
||||||
provider_id: props.provider.id,
|
provider_id: props.provider.id,
|
||||||
model_name: mapping.name,
|
model_name: mapping.name,
|
||||||
message: "hello"
|
message: "hello",
|
||||||
|
api_format: apiFormat
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -591,7 +697,7 @@ async function testMapping(item: CombinedMapping, mapping: MappingItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试正则映射(指定 Key)
|
// 测试正则映射(指定 Key,直接发请求,因为已经有 Key 信息)
|
||||||
async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo, match: MappingItem) {
|
async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo, match: MappingItem) {
|
||||||
const testingKey = `${item.key}-${keyItem.keyId}-${match.name}`
|
const testingKey = `${item.key}-${keyItem.keyId}-${match.name}`
|
||||||
testingMapping.value = testingKey
|
testingMapping.value = testingKey
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class TestModelRequest(BaseModel):
|
|||||||
provider_id: str
|
provider_id: str
|
||||||
model_name: str
|
model_name: str
|
||||||
api_key_id: Optional[str] = None
|
api_key_id: Optional[str] = None
|
||||||
|
endpoint_id: Optional[str] = None # 指定使用的端点ID
|
||||||
stream: bool = False
|
stream: bool = False
|
||||||
message: Optional[str] = "你好"
|
message: Optional[str] = "你好"
|
||||||
api_format: Optional[str] = None # 指定使用的API格式,如果不指定则使用端点的默认格式
|
api_format: Optional[str] = None # 指定使用的API格式,如果不指定则使用端点的默认格式
|
||||||
@@ -200,17 +201,73 @@ async def test_model(
|
|||||||
if not provider:
|
if not provider:
|
||||||
raise HTTPException(status_code=404, detail="Provider not found")
|
raise HTTPException(status_code=404, detail="Provider not found")
|
||||||
|
|
||||||
# 构建 api_format -> endpoint 映射
|
# 构建 api_format -> endpoint 映射 和 id -> endpoint 映射
|
||||||
format_to_endpoint: dict[str, ProviderEndpoint] = {}
|
format_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||||
|
id_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||||
for ep in provider.endpoints:
|
for ep in provider.endpoints:
|
||||||
if ep.is_active:
|
if ep.is_active:
|
||||||
format_to_endpoint[ep.api_format] = ep
|
format_to_endpoint[ep.api_format] = ep
|
||||||
|
id_to_endpoint[ep.id] = ep
|
||||||
|
|
||||||
# 找到合适的端点和 API Key
|
# 找到合适的端点和 API Key
|
||||||
endpoint = None
|
endpoint = None
|
||||||
api_key = None
|
api_key = None
|
||||||
|
|
||||||
if request.api_key_id:
|
# 优先级: api_format > endpoint_id > api_key_id > 自动选择
|
||||||
|
# 如果指定了 api_format,优先使用该格式对应的 endpoint
|
||||||
|
if request.api_format:
|
||||||
|
endpoint = format_to_endpoint.get(request.api_format)
|
||||||
|
if not endpoint:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"No active endpoint found for API format: {request.api_format}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.api_key_id:
|
||||||
|
# 使用指定的 Key,但需要校验是否支持该格式
|
||||||
|
api_key = next(
|
||||||
|
(key for key in provider.api_keys if key.id == request.api_key_id and key.is_active),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if api_key and request.api_format not in (api_key.api_formats or []):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"API Key does not support format: {request.api_format}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 找支持该格式的第一个可用 Key
|
||||||
|
for key in provider.api_keys:
|
||||||
|
if not key.is_active:
|
||||||
|
continue
|
||||||
|
if request.api_format in (key.api_formats or []):
|
||||||
|
api_key = key
|
||||||
|
break
|
||||||
|
elif request.endpoint_id:
|
||||||
|
# 使用指定的端点
|
||||||
|
endpoint = id_to_endpoint.get(request.endpoint_id)
|
||||||
|
if not endpoint:
|
||||||
|
raise HTTPException(status_code=404, detail="Endpoint not found or not active")
|
||||||
|
|
||||||
|
if request.api_key_id:
|
||||||
|
# 同时指定了 Key,需要校验是否支持该端点格式
|
||||||
|
api_key = next(
|
||||||
|
(key for key in provider.api_keys if key.id == request.api_key_id and key.is_active),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if api_key and endpoint.api_format not in (api_key.api_formats or []):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"API Key does not support endpoint format: {endpoint.api_format}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 找支持该端点格式的第一个可用 Key
|
||||||
|
for key in provider.api_keys:
|
||||||
|
if not key.is_active:
|
||||||
|
continue
|
||||||
|
if endpoint.api_format in (key.api_formats or []):
|
||||||
|
api_key = key
|
||||||
|
break
|
||||||
|
elif request.api_key_id:
|
||||||
# 使用指定的 API Key
|
# 使用指定的 API Key
|
||||||
api_key = next(
|
api_key = next(
|
||||||
(key for key in provider.api_keys if key.id == request.api_key_id and key.is_active),
|
(key for key in provider.api_keys if key.id == request.api_key_id and key.is_active),
|
||||||
@@ -274,24 +331,6 @@ async def test_model(
|
|||||||
logger.debug(f"[test-model] 使用 Adapter: {adapter_class.__name__}")
|
logger.debug(f"[test-model] 使用 Adapter: {adapter_class.__name__}")
|
||||||
logger.debug(f"[test-model] 端点 API Format: {endpoint.api_format}")
|
logger.debug(f"[test-model] 端点 API Format: {endpoint.api_format}")
|
||||||
|
|
||||||
# 如果请求指定了 api_format,优先使用它
|
|
||||||
target_api_format = request.api_format or endpoint.api_format
|
|
||||||
if request.api_format and request.api_format != endpoint.api_format:
|
|
||||||
logger.debug(f"[test-model] 请求指定 API Format: {request.api_format}")
|
|
||||||
# 重新获取适配器
|
|
||||||
adapter_class = _get_adapter_for_format(request.api_format)
|
|
||||||
if not adapter_class:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": f"Unknown API format: {request.api_format}",
|
|
||||||
"provider": {
|
|
||||||
"id": provider.id,
|
|
||||||
"name": provider.name,
|
|
||||||
},
|
|
||||||
"model": request.model_name,
|
|
||||||
}
|
|
||||||
logger.debug(f"[test-model] 重新选择 Adapter: {adapter_class.__name__}")
|
|
||||||
|
|
||||||
# 准备测试请求数据
|
# 准备测试请求数据
|
||||||
check_request = {
|
check_request = {
|
||||||
"model": request.model_name,
|
"model": request.model_name,
|
||||||
|
|||||||
@@ -107,10 +107,18 @@ class ChatAdapterBase(ApiAdapter):
|
|||||||
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
|
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
def build_request_body(cls, request_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
"""构建请求体,子类可以覆盖以自定义请求格式转换"""
|
"""构建测试请求体,使用转换器注册表自动处理格式转换
|
||||||
# 默认实现:直接使用请求数据
|
|
||||||
return request_data.copy()
|
Args:
|
||||||
|
request_data: 可选的请求数据,会与默认测试请求合并
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
转换为目标 API 格式的请求体
|
||||||
|
"""
|
||||||
|
from src.api.handlers.base.request_builder import build_test_request_body
|
||||||
|
|
||||||
|
return build_test_request_body(cls.FORMAT_ID, request_data)
|
||||||
|
|
||||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||||
"""从请求中提取 API 密钥,使用统一的 headers.py 实现"""
|
"""从请求中提取 API 密钥,使用统一的 headers.py 实现"""
|
||||||
|
|||||||
@@ -684,17 +684,18 @@ class CliAdapterBase(ApiAdapter):
|
|||||||
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement build_endpoint_url")
|
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement build_endpoint_url")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
def build_request_body(cls, request_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""构建测试请求体,使用转换器注册表自动处理格式转换
|
||||||
构建CLI API请求体 - 子类应覆盖
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request_data: 请求数据
|
request_data: 可选的请求数据,会与默认测试请求合并
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
请求体字典
|
转换为目标 API 格式的请求体
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement build_request_body")
|
from src.api.handlers.base.request_builder import build_test_request_body
|
||||||
|
|
||||||
|
return build_test_request_body(cls.FORMAT_ID, request_data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cli_user_agent(cls) -> Optional[str]:
|
def get_cli_user_agent(cls) -> Optional[str]:
|
||||||
|
|||||||
@@ -27,6 +27,67 @@ from src.core.api_format import HeaderBuilder, UPSTREAM_DROP_HEADERS
|
|||||||
SENSITIVE_HEADERS: FrozenSet[str] = UPSTREAM_DROP_HEADERS
|
SENSITIVE_HEADERS: FrozenSet[str] = UPSTREAM_DROP_HEADERS
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 测试请求常量与辅助函数
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# 标准测试请求体(OpenAI 格式)
|
||||||
|
# 用于 check_endpoint 等测试场景,使用简单安全的消息内容避免触发安全过滤
|
||||||
|
DEFAULT_TEST_REQUEST: Dict[str, Any] = {
|
||||||
|
"messages": [{"role": "user", "content": "Hi"}],
|
||||||
|
"max_tokens": 5,
|
||||||
|
"temperature": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_test_request_data(request_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""获取测试请求数据
|
||||||
|
|
||||||
|
如果传入 request_data,则合并到默认测试请求中;
|
||||||
|
否则使用默认测试请求。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request_data: 用户提供的请求数据(会覆盖默认值)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
合并后的测试请求数据(OpenAI 格式)
|
||||||
|
"""
|
||||||
|
if request_data:
|
||||||
|
merged = DEFAULT_TEST_REQUEST.copy()
|
||||||
|
merged.update(request_data)
|
||||||
|
return merged
|
||||||
|
return DEFAULT_TEST_REQUEST.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def build_test_request_body(
|
||||||
|
format_id: str,
|
||||||
|
request_data: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""构建测试请求体,自动处理格式转换
|
||||||
|
|
||||||
|
使用 converter_registry 将 OpenAI 格式的测试请求转换为目标格式。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
format_id: 目标 API 格式 ID(如 "CLAUDE", "GEMINI", "OPENAI_CLI")
|
||||||
|
request_data: 可选的请求数据,会与默认测试请求合并
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
转换为目标 API 格式的请求体
|
||||||
|
"""
|
||||||
|
from src.core.api_format.conversion import converter_registry
|
||||||
|
from src.core.api_format.utils import get_base_format
|
||||||
|
|
||||||
|
# 获取测试请求数据(OpenAI 格式)
|
||||||
|
source_data = get_test_request_data(request_data)
|
||||||
|
|
||||||
|
# CLI 格式使用基础格式进行转换(CLAUDE_CLI -> CLAUDE)
|
||||||
|
# 因为 converter_registry 只注册了基础格式之间的转换器
|
||||||
|
target_format = get_base_format(format_id) or format_id
|
||||||
|
|
||||||
|
# 使用注册表进行格式转换 (OPENAI -> 目标基础格式)
|
||||||
|
return converter_registry.convert_request(source_data, "OPENAI", target_format)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 请求构建器
|
# 请求构建器
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -198,14 +198,7 @@ class ClaudeChatAdapter(ChatAdapterBase):
|
|||||||
else:
|
else:
|
||||||
return f"{base_url}/v1/messages"
|
return f"{base_url}/v1/messages"
|
||||||
|
|
||||||
@classmethod
|
# build_request_body 使用基类实现,通过 converter_registry 自动转换 OPENAI -> CLAUDE
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""构建Claude API请求体"""
|
|
||||||
return {
|
|
||||||
"model": request_data.get("model"),
|
|
||||||
"max_tokens": request_data.get("max_tokens", 100),
|
|
||||||
"messages": request_data.get("messages", []),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_claude_adapter(x_app_header: Optional[str]):
|
def build_claude_adapter(x_app_header: Optional[str]):
|
||||||
|
|||||||
@@ -128,14 +128,7 @@ class ClaudeCliAdapter(CliAdapterBase):
|
|||||||
else:
|
else:
|
||||||
return f"{base_url}/v1/messages"
|
return f"{base_url}/v1/messages"
|
||||||
|
|
||||||
@classmethod
|
# build_request_body 使用基类实现,通过 converter_registry 自动转换 OPENAI -> CLAUDE_CLI
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""构建Claude CLI API请求体"""
|
|
||||||
return {
|
|
||||||
"model": request_data.get("model"),
|
|
||||||
"max_tokens": request_data.get("max_tokens", 100),
|
|
||||||
"messages": request_data.get("messages", []),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cli_user_agent(cls) -> Optional[str]:
|
def get_cli_user_agent(cls) -> Optional[str]:
|
||||||
|
|||||||
@@ -223,19 +223,7 @@ class GeminiChatAdapter(ChatAdapterBase):
|
|||||||
else:
|
else:
|
||||||
return f"{base_url}/v1beta"
|
return f"{base_url}/v1beta"
|
||||||
|
|
||||||
@classmethod
|
# build_request_body 使用基类实现,通过 converter_registry 自动转换 OPENAI -> GEMINI
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""构建Gemini API请求体"""
|
|
||||||
return {
|
|
||||||
"contents": request_data.get("messages", []),
|
|
||||||
"generationConfig": {
|
|
||||||
"maxOutputTokens": request_data.get("max_tokens", 100),
|
|
||||||
"temperature": request_data.get("temperature", 0.7),
|
|
||||||
},
|
|
||||||
"safetySettings": [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def check_endpoint(
|
async def check_endpoint(
|
||||||
|
|||||||
@@ -149,19 +149,7 @@ class GeminiCliAdapter(CliAdapterBase):
|
|||||||
prefix = f"{base_url}/v1beta"
|
prefix = f"{base_url}/v1beta"
|
||||||
return f"{prefix}/models/{effective_model_name}:generateContent"
|
return f"{prefix}/models/{effective_model_name}:generateContent"
|
||||||
|
|
||||||
@classmethod
|
# build_request_body 使用基类实现,通过 converter_registry 自动转换 OPENAI -> GEMINI_CLI
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""构建Gemini CLI API请求体"""
|
|
||||||
return {
|
|
||||||
"contents": request_data.get("messages", []),
|
|
||||||
"generationConfig": {
|
|
||||||
"maxOutputTokens": request_data.get("max_tokens", 100),
|
|
||||||
"temperature": request_data.get("temperature", 0.7),
|
|
||||||
},
|
|
||||||
"safetySettings": [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cli_user_agent(cls) -> Optional[str]:
|
def get_cli_user_agent(cls) -> Optional[str]:
|
||||||
|
|||||||
@@ -70,10 +70,8 @@ class OpenAICliAdapter(CliAdapterBase):
|
|||||||
else:
|
else:
|
||||||
return f"{base_url}/v1/chat/completions"
|
return f"{base_url}/v1/chat/completions"
|
||||||
|
|
||||||
@classmethod
|
# build_request_body 使用基类实现
|
||||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
# OPENAI -> OPENAI_CLI 无转换器,会直接透传原始请求
|
||||||
"""构建OpenAI CLI API请求体"""
|
|
||||||
return request_data.copy()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cli_user_agent(cls) -> Optional[str]:
|
def get_cli_user_agent(cls) -> Optional[str]:
|
||||||
|
|||||||
Reference in New Issue
Block a user