mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Normalize canonical API formats
This commit is contained in:
@@ -688,18 +688,12 @@ interface ApiFormatGroup {
|
||||
const STANDARD_ROUTING_API_FORMATS = [
|
||||
'openai:chat',
|
||||
'openai:responses',
|
||||
'claude:chat',
|
||||
'claude:cli',
|
||||
'gemini:chat',
|
||||
'gemini:cli'
|
||||
'claude:messages',
|
||||
'gemini:generate_content'
|
||||
]
|
||||
|
||||
function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
|
||||
switch (apiFormat.trim().toLowerCase()) {
|
||||
case 'openai:cli':
|
||||
return 'openai:responses'
|
||||
case 'openai:compact':
|
||||
return 'openai:responses:compact'
|
||||
default:
|
||||
return apiFormat.trim().toLowerCase()
|
||||
}
|
||||
@@ -707,11 +701,9 @@ function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
|
||||
|
||||
function apiDataFormatId(apiFormat: string): string | null {
|
||||
switch (normalizeLegacyOpenAIFormatAlias(apiFormat)) {
|
||||
case 'claude:chat':
|
||||
case 'claude:cli':
|
||||
case 'claude:messages':
|
||||
return 'claude'
|
||||
case 'gemini:chat':
|
||||
case 'gemini:cli':
|
||||
case 'gemini:generate_content':
|
||||
return 'gemini'
|
||||
case 'openai:chat':
|
||||
return 'openai_chat'
|
||||
@@ -736,11 +728,9 @@ function requestConversionKind(clientApiFormat: string, providerApiFormat: strin
|
||||
return 'to_openai_chat'
|
||||
case 'openai:responses':
|
||||
return 'to_openai_responses'
|
||||
case 'claude:chat':
|
||||
case 'claude:cli':
|
||||
case 'claude:messages':
|
||||
return 'to_claude'
|
||||
case 'gemini:chat':
|
||||
case 'gemini:cli':
|
||||
case 'gemini:generate_content':
|
||||
return 'to_gemini'
|
||||
default:
|
||||
return null
|
||||
|
||||
@@ -264,7 +264,7 @@ async function handleTestModel(modelName: string) {
|
||||
provider_id: props.providerId,
|
||||
model_name: modelName,
|
||||
api_key_id: props.keyId,
|
||||
api_format: 'gemini:chat',
|
||||
api_format: 'gemini:generate_content',
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -1338,10 +1338,6 @@ function hasDefaultBodyRules(apiFormat: string): boolean {
|
||||
|
||||
function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
|
||||
switch (apiFormat.trim().toLowerCase()) {
|
||||
case 'openai:cli':
|
||||
return 'openai:responses'
|
||||
case 'openai:compact':
|
||||
return 'openai:responses:compact'
|
||||
default:
|
||||
return apiFormat.trim().toLowerCase()
|
||||
}
|
||||
@@ -1385,10 +1381,10 @@ function getDefaultPath(apiFormat: string, baseUrl?: string): string {
|
||||
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
||||
const normalizedApiFormat = normalizeLegacyOpenAIFormatAlias(apiFormat)
|
||||
if (providerType === 'vertex_ai') {
|
||||
if (normalizedApiFormat === 'gemini:chat') {
|
||||
if (normalizedApiFormat === 'gemini:generate_content') {
|
||||
return '/v1/publishers/google/models/{model}:{action}'
|
||||
}
|
||||
if (normalizedApiFormat === 'claude:chat') {
|
||||
if (normalizedApiFormat === 'claude:messages') {
|
||||
return '/v1/projects/{project_id}/locations/{region}/publishers/anthropic/models/{model}:{action}'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ import {
|
||||
type CapabilityDefinition,
|
||||
type ProviderType
|
||||
} from '@/api/endpoints'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { formatApiFormat, normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -370,13 +370,13 @@ const { success, error: showError } = useToast()
|
||||
|
||||
function getVertexAllowedFormatsByAuth(authType: 'api_key' | 'service_account'): Set<string> {
|
||||
if (authType === 'api_key') {
|
||||
return new Set(['gemini:chat'])
|
||||
return new Set(['gemini:generate_content'])
|
||||
}
|
||||
return new Set(['gemini:chat', 'claude:chat'])
|
||||
return new Set(['gemini:generate_content', 'claude:messages'])
|
||||
}
|
||||
|
||||
function normalizeApiFormat(format: string): string {
|
||||
return String(format || '').trim().toLowerCase()
|
||||
return normalizeApiFormatAlias(format).trim().toLowerCase()
|
||||
}
|
||||
|
||||
function getAvailableApiFormatSet(): Set<string> {
|
||||
|
||||
@@ -529,7 +529,7 @@ function handleDialogUpdate(value: boolean) {
|
||||
|
||||
// 主 Tab 状态
|
||||
const activeMainTab = ref<'provider' | 'key'>('provider')
|
||||
const activeFormatTab = ref<string>('claude:chat')
|
||||
const activeFormatTab = ref<string>('claude:messages')
|
||||
|
||||
// 提供商排序状态
|
||||
const sortedProviders = ref<ProviderWithEndpointsSummary[]>([])
|
||||
@@ -614,17 +614,16 @@ async function loadBalances() {
|
||||
}
|
||||
|
||||
const LEGACY_API_FORMAT_MAP: Record<string, string> = {
|
||||
CLAUDE: 'claude:chat',
|
||||
CLAUDE_CLI: 'claude:cli',
|
||||
CLAUDE: 'claude:messages',
|
||||
CLAUDE_MESSAGES: 'claude:messages',
|
||||
OPENAI: 'openai:chat',
|
||||
OPENAI_CLI: 'openai:responses',
|
||||
OPENAI_COMPACT: 'openai:responses:compact',
|
||||
OPENAI_RESPONSES: 'openai:responses',
|
||||
OPENAI_RESPONSES_COMPACT: 'openai:responses:compact',
|
||||
OPENAI_VIDEO: 'openai:video',
|
||||
GEMINI: 'gemini:chat',
|
||||
GEMINI_CLI: 'gemini:cli',
|
||||
GEMINI: 'gemini:generate_content',
|
||||
GEMINI_GENERATE_CONTENT: 'gemini:generate_content',
|
||||
GEMINI_VIDEO: 'gemini:video',
|
||||
GEMINI_FILES: 'gemini:files',
|
||||
}
|
||||
|
||||
function normalizeApiFormatKey(value: string | null | undefined): string {
|
||||
@@ -635,6 +634,12 @@ function normalizeApiFormatKey(value: string | null | undefined): string {
|
||||
const [family, kind] = raw.split(':', 2)
|
||||
const familyNorm = family?.trim().toLowerCase()
|
||||
const kindNorm = kind?.trim().toLowerCase()
|
||||
if (familyNorm === 'claude' && ['chat', 'cli', 'messages'].includes(kindNorm)) {
|
||||
return 'claude:messages'
|
||||
}
|
||||
if (familyNorm === 'gemini' && ['chat', 'cli', 'generate_content'].includes(kindNorm)) {
|
||||
return 'gemini:generate_content'
|
||||
}
|
||||
if (familyNorm && kindNorm) return `${familyNorm}:${kindNorm}`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,14 @@ import type { EndpointHealthDetail } from '@/api/endpoints'
|
||||
export type EndpointStatus = 'disabled' | 'no_keys' | 'keys_disabled' | 'available'
|
||||
|
||||
const ENDPOINT_SORT_ORDER = [
|
||||
'claude:chat',
|
||||
'claude:cli',
|
||||
'claude:messages',
|
||||
'openai:chat',
|
||||
'openai:responses',
|
||||
'openai:responses:compact',
|
||||
'gemini:chat',
|
||||
'gemini:cli',
|
||||
'gemini:generate_content',
|
||||
'openai:video',
|
||||
'gemini:video',
|
||||
'gemini:files',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,13 +23,11 @@ export function useProviderFilters(
|
||||
|
||||
const apiFormatFilters: FilterOption[] = [
|
||||
{ value: 'all', label: '全部格式' },
|
||||
{ value: 'claude:chat', label: 'Claude Chat' },
|
||||
{ value: 'claude:cli', label: 'Claude CLI' },
|
||||
{ value: 'claude:messages', label: 'Claude Messages' },
|
||||
{ value: 'openai:chat', label: 'OpenAI Chat' },
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses' },
|
||||
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact' },
|
||||
{ value: 'gemini:chat', label: 'Gemini Chat' },
|
||||
{ value: 'gemini:cli', label: 'Gemini CLI' },
|
||||
{ value: 'gemini:generate_content', label: 'Gemini Generate Content' },
|
||||
]
|
||||
|
||||
const modelFilters = computed<FilterOption[]>(() => {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('usage token normalization', () => {
|
||||
cache_creation_input_tokens: 1530,
|
||||
cache_read_input_tokens: 104026,
|
||||
output_tokens: 591,
|
||||
api_format: 'claude:chat',
|
||||
api_format: 'claude:messages',
|
||||
}
|
||||
|
||||
expect(getEffectiveInputTokens(usage)).toBe(1)
|
||||
@@ -47,7 +47,7 @@ describe('usage token normalization', () => {
|
||||
cache_creation_input_tokens: 687,
|
||||
cache_read_input_tokens: 52873,
|
||||
output_tokens: 973,
|
||||
api_format: 'claude:chat',
|
||||
api_format: 'claude:messages',
|
||||
})).toBe(4941)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -757,11 +757,10 @@ const AVAILABLE_API_FORMATS = [
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses' },
|
||||
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact' },
|
||||
{ value: 'openai:video', label: 'OpenAI Video' },
|
||||
{ value: 'claude:chat', label: 'Claude Chat' },
|
||||
{ value: 'claude:cli', label: 'Claude CLI' },
|
||||
{ value: 'gemini:chat', label: 'Gemini Chat' },
|
||||
{ value: 'gemini:cli', label: 'Gemini CLI' },
|
||||
{ value: 'claude:messages', label: 'Claude Messages' },
|
||||
{ value: 'gemini:generate_content', label: 'Gemini Generate Content' },
|
||||
{ value: 'gemini:video', label: 'Gemini Video' },
|
||||
{ value: 'gemini:files', label: 'Gemini Files' },
|
||||
] as const
|
||||
|
||||
// 使用模块级常量
|
||||
@@ -844,7 +843,7 @@ function formatOptionalTokens(value: number | null | undefined): string {
|
||||
// useDebounceFn 自动处理清理,无需 onUnmounted
|
||||
|
||||
// 判断是否应该显示格式转换信息
|
||||
// 包括:1. 跨格式转换(has_format_conversion=true)2. 同族格式差异(如 CLAUDE_CLI → CLAUDE)
|
||||
// 包括:1. 跨格式转换(has_format_conversion=true)2. 同族格式差异
|
||||
function shouldShowFormatConversion(record: UsageRecord): boolean {
|
||||
if (!record.api_format || !record.endpoint_api_format) {
|
||||
return false
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Gemini conversation parser', () => {
|
||||
}
|
||||
|
||||
it('parses normalized output payload when hint is gemini', () => {
|
||||
const parsed = parseResponse(normalizedResponse, requestBody, 'gemini:chat')
|
||||
const parsed = parseResponse(normalizedResponse, requestBody, 'gemini:generate_content')
|
||||
expect(parsed.apiFormat).toBe('gemini')
|
||||
expect(parsed.messages).toHaveLength(1)
|
||||
expect(parsed.messages[0]?.role).toBe('assistant')
|
||||
@@ -31,7 +31,7 @@ describe('Gemini conversation parser', () => {
|
||||
})
|
||||
|
||||
it('renders normalized output payload when hint is gemini', () => {
|
||||
const rendered = renderResponse(normalizedResponse, requestBody, 'gemini:chat')
|
||||
const rendered = renderResponse(normalizedResponse, requestBody, 'gemini:generate_content')
|
||||
expect(rendered.error).toBeUndefined()
|
||||
expect(rendered.blocks).toHaveLength(1)
|
||||
expect(rendered.blocks[0]).toMatchObject({
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('usage status helpers', () => {
|
||||
|
||||
it('defaults OpenAI and Claude requests to non-stream when client flags are absent', () => {
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
api_format: 'openai:cli',
|
||||
api_format: 'openai:responses',
|
||||
is_stream: true,
|
||||
upstream_is_stream: true,
|
||||
client_requested_stream: undefined,
|
||||
@@ -143,7 +143,7 @@ describe('usage status helpers', () => {
|
||||
}))).toBe('标准->流式')
|
||||
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
api_format: 'claude:chat',
|
||||
api_format: 'claude:messages',
|
||||
is_stream: false,
|
||||
upstream_is_stream: false,
|
||||
client_requested_stream: undefined,
|
||||
@@ -153,7 +153,7 @@ describe('usage status helpers', () => {
|
||||
|
||||
it('keeps upstream fallback for formats without a default non-stream convention', () => {
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
api_format: 'gemini:cli',
|
||||
api_format: 'gemini:generate_content',
|
||||
is_stream: true,
|
||||
upstream_is_stream: true,
|
||||
client_requested_stream: undefined,
|
||||
@@ -163,7 +163,7 @@ describe('usage status helpers', () => {
|
||||
|
||||
it('prefers client_requested_stream over stale client_is_stream when they disagree', () => {
|
||||
expect(formatUsageStreamLabel(buildUsageRecord({
|
||||
api_format: 'openai:cli',
|
||||
api_format: 'openai:responses',
|
||||
is_stream: true,
|
||||
upstream_is_stream: true,
|
||||
client_requested_stream: false,
|
||||
|
||||
@@ -124,14 +124,10 @@ function normalizeUsageStreamApiFormat(value: string | null | undefined): string
|
||||
switch (normalized) {
|
||||
case 'openai':
|
||||
return 'openai:chat'
|
||||
case 'openai:cli':
|
||||
return 'openai:responses'
|
||||
case 'openai:compact':
|
||||
return 'openai:responses:compact'
|
||||
case 'claude':
|
||||
return 'claude:chat'
|
||||
return 'claude:messages'
|
||||
case 'gemini':
|
||||
return 'gemini:chat'
|
||||
return 'gemini:generate_content'
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
@@ -143,8 +139,7 @@ function usageApiFormatDefaultsToNonStream(apiFormat: string): boolean {
|
||||
case 'openai:responses':
|
||||
case 'openai:responses:compact':
|
||||
case 'openai:image':
|
||||
case 'claude:chat':
|
||||
case 'claude:cli':
|
||||
case 'claude:messages':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user