mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Normalize canonical API formats
This commit is contained in:
@@ -1,45 +1,43 @@
|
||||
// API 格式常量
|
||||
export const API_FORMATS = {
|
||||
// 新模式:endpoint signature key(family:kind,全小写)
|
||||
CLAUDE: 'claude:chat',
|
||||
CLAUDE_CLI: 'claude:cli',
|
||||
CLAUDE: 'claude:messages',
|
||||
CLAUDE_MESSAGES: 'claude:messages',
|
||||
OPENAI: 'openai:chat',
|
||||
OPENAI_RESPONSES: 'openai:responses',
|
||||
OPENAI_RESPONSES_COMPACT: 'openai:responses:compact',
|
||||
OPENAI_IMAGE: 'openai:image',
|
||||
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',
|
||||
} as const
|
||||
|
||||
export type APIFormat = typeof API_FORMATS[keyof typeof API_FORMATS]
|
||||
|
||||
// API 格式显示名称映射(按品牌分组:Chat 在前,CLI/Video 在后)
|
||||
export const API_FORMAT_LABELS: Record<string, string> = {
|
||||
[API_FORMATS.CLAUDE]: 'Claude Chat',
|
||||
[API_FORMATS.CLAUDE_CLI]: 'Claude CLI',
|
||||
[API_FORMATS.CLAUDE_MESSAGES]: 'Claude Messages',
|
||||
[API_FORMATS.OPENAI]: 'OpenAI Chat',
|
||||
[API_FORMATS.OPENAI_RESPONSES]: 'OpenAI Responses',
|
||||
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'OpenAI Responses Compact',
|
||||
[API_FORMATS.OPENAI_IMAGE]: 'OpenAI Image',
|
||||
[API_FORMATS.OPENAI_VIDEO]: 'OpenAI Video',
|
||||
[API_FORMATS.GEMINI]: 'Gemini Chat',
|
||||
[API_FORMATS.GEMINI_CLI]: 'Gemini CLI',
|
||||
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'Gemini Generate Content',
|
||||
[API_FORMATS.GEMINI_VIDEO]: 'Gemini Video',
|
||||
// legacy 兼容(仅用于展示历史数据)
|
||||
CLAUDE: 'Claude Chat',
|
||||
CLAUDE_CLI: 'Claude CLI',
|
||||
[API_FORMATS.GEMINI_FILES]: 'Gemini Files',
|
||||
CLAUDE: 'Claude Messages',
|
||||
CLAUDE_MESSAGES: 'Claude Messages',
|
||||
OPENAI: 'OpenAI Chat',
|
||||
LEGACY_OPENAI_CLI: 'OpenAI Responses',
|
||||
LEGACY_OPENAI_COMPACT: 'OpenAI Responses Compact',
|
||||
OPENAI_RESPONSES: 'OpenAI Responses',
|
||||
OPENAI_RESPONSES_COMPACT: 'OpenAI Responses Compact',
|
||||
OPENAI_IMAGE: 'OpenAI Image',
|
||||
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',
|
||||
}
|
||||
|
||||
// API 格式缩写映射(用于空间紧凑的显示场景)
|
||||
@@ -49,24 +47,21 @@ export const API_FORMAT_SHORT: Record<string, string> = {
|
||||
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'ORC',
|
||||
[API_FORMATS.OPENAI_IMAGE]: 'OI',
|
||||
[API_FORMATS.OPENAI_VIDEO]: 'OV',
|
||||
[API_FORMATS.CLAUDE]: 'C',
|
||||
[API_FORMATS.CLAUDE_CLI]: 'CC',
|
||||
[API_FORMATS.GEMINI]: 'G',
|
||||
[API_FORMATS.GEMINI_CLI]: 'GC',
|
||||
[API_FORMATS.CLAUDE_MESSAGES]: 'CM',
|
||||
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'G',
|
||||
[API_FORMATS.GEMINI_VIDEO]: 'GV',
|
||||
// legacy 兼容(仅用于展示历史数据)
|
||||
[API_FORMATS.GEMINI_FILES]: 'GF',
|
||||
OPENAI: 'O',
|
||||
LEGACY_OPENAI_CLI: 'OR',
|
||||
LEGACY_OPENAI_COMPACT: 'ORC',
|
||||
OPENAI_RESPONSES: 'OR',
|
||||
OPENAI_RESPONSES_COMPACT: 'ORC',
|
||||
OPENAI_IMAGE: 'OI',
|
||||
OPENAI_VIDEO: 'OV',
|
||||
CLAUDE: 'C',
|
||||
CLAUDE_CLI: 'CC',
|
||||
CLAUDE: 'CM',
|
||||
CLAUDE_MESSAGES: 'CM',
|
||||
GEMINI: 'G',
|
||||
GEMINI_CLI: 'GC',
|
||||
GEMINI_GENERATE_CONTENT: 'G',
|
||||
GEMINI_VIDEO: 'GV',
|
||||
GEMINI_FILES: 'GF',
|
||||
}
|
||||
|
||||
// API 格式排序顺序(统一的显示顺序)
|
||||
@@ -76,11 +71,10 @@ export const API_FORMAT_ORDER: string[] = [
|
||||
API_FORMATS.OPENAI_RESPONSES_COMPACT,
|
||||
API_FORMATS.OPENAI_IMAGE,
|
||||
API_FORMATS.OPENAI_VIDEO,
|
||||
API_FORMATS.CLAUDE,
|
||||
API_FORMATS.CLAUDE_CLI,
|
||||
API_FORMATS.GEMINI,
|
||||
API_FORMATS.GEMINI_CLI,
|
||||
API_FORMATS.CLAUDE_MESSAGES,
|
||||
API_FORMATS.GEMINI_GENERATE_CONTENT,
|
||||
API_FORMATS.GEMINI_VIDEO,
|
||||
API_FORMATS.GEMINI_FILES,
|
||||
]
|
||||
|
||||
// Family 显示名称映射
|
||||
@@ -97,8 +91,11 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
|
||||
responses: 'Responses',
|
||||
'responses:compact': 'Responses Compact',
|
||||
compact: 'Compact',
|
||||
messages: 'Messages',
|
||||
generate_content: 'Generate Content',
|
||||
image: 'Image',
|
||||
video: 'Video',
|
||||
files: 'Files',
|
||||
}
|
||||
|
||||
// Family 排序顺序
|
||||
@@ -113,22 +110,29 @@ export function parseApiFormat(format: string): { family: string; kind: string }
|
||||
|
||||
export function normalizeApiFormatAlias(format: string | null | undefined): string {
|
||||
const raw = format?.trim() ?? ''
|
||||
switch (raw.toLowerCase()) {
|
||||
case 'openai:cli':
|
||||
return API_FORMATS.OPENAI_RESPONSES
|
||||
case 'openai:compact':
|
||||
return API_FORMATS.OPENAI_RESPONSES_COMPACT
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch (raw.toUpperCase()) {
|
||||
case 'OPENAI_CLI':
|
||||
case 'CLAUDE':
|
||||
case 'CLAUDE_MESSAGES':
|
||||
return API_FORMATS.CLAUDE_MESSAGES
|
||||
case 'OPENAI':
|
||||
return API_FORMATS.OPENAI
|
||||
case 'OPENAI_RESPONSES':
|
||||
return API_FORMATS.OPENAI_RESPONSES
|
||||
case 'OPENAI_COMPACT':
|
||||
case 'OPENAI_RESPONSES_COMPACT':
|
||||
return API_FORMATS.OPENAI_RESPONSES_COMPACT
|
||||
case 'OPENAI_IMAGE':
|
||||
return API_FORMATS.OPENAI_IMAGE
|
||||
case 'OPENAI_VIDEO':
|
||||
return API_FORMATS.OPENAI_VIDEO
|
||||
case 'GEMINI':
|
||||
case 'GEMINI_GENERATE_CONTENT':
|
||||
return API_FORMATS.GEMINI_GENERATE_CONTENT
|
||||
case 'GEMINI_VIDEO':
|
||||
return API_FORMATS.GEMINI_VIDEO
|
||||
case 'GEMINI_FILES':
|
||||
return API_FORMATS.GEMINI_FILES
|
||||
default:
|
||||
return raw
|
||||
return raw.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +175,6 @@ export function formatApiFormat(format: string | null | undefined): string {
|
||||
const upper = normalized.toUpperCase()
|
||||
return API_FORMAT_LABELS[normalized]
|
||||
|| API_FORMAT_LABELS[normalized.toLowerCase()]
|
||||
|| API_FORMAT_LABELS[legacyUppercaseApiFormatKey(upper)]
|
||||
|| API_FORMAT_LABELS[upper]
|
||||
|| normalized
|
||||
}
|
||||
@@ -183,22 +186,10 @@ export function formatApiFormatShort(format: string | null | undefined): string
|
||||
const upper = normalized.toUpperCase()
|
||||
return API_FORMAT_SHORT[normalized]
|
||||
|| API_FORMAT_SHORT[normalized.toLowerCase()]
|
||||
|| API_FORMAT_SHORT[legacyUppercaseApiFormatKey(upper)]
|
||||
|| API_FORMAT_SHORT[upper]
|
||||
|| normalized.substring(0, 2)
|
||||
}
|
||||
|
||||
function legacyUppercaseApiFormatKey(value: string): string {
|
||||
switch (value) {
|
||||
case 'OPENAI_CLI':
|
||||
return 'LEGACY_OPENAI_CLI'
|
||||
case 'OPENAI_COMPACT':
|
||||
return 'LEGACY_OPENAI_COMPACT'
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:按标准顺序排序 API 格式数组
|
||||
export function sortApiFormats(formats: string[]): string[] {
|
||||
return [...formats].sort((a, b) => {
|
||||
|
||||
@@ -267,7 +267,7 @@ export const usageApi = {
|
||||
username?: string
|
||||
model?: string
|
||||
provider?: string
|
||||
api_format?: string // API 格式筛选(如 openai:chat, claude:chat)
|
||||
api_format?: string // API 格式筛选(如 openai:chat, claude:messages)
|
||||
status?: string // 'stream' | 'standard' | 'error'
|
||||
limit?: number
|
||||
offset?: number
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -453,10 +453,10 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
active_models: 7,
|
||||
avg_health_score: 0.91,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: ['CLAUDE_CLI', 'GEMINI_CLI', 'OPENAI_RESPONSES'],
|
||||
api_formats: ['CLAUDE_MESSAGES', 'GEMINI_GENERATE_CONTENT', 'OPENAI_RESPONSES'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'CLAUDE_CLI', health_score: 0.73, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'GEMINI_CLI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'CLAUDE_MESSAGES', health_score: 0.73, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'GEMINI_GENERATE_CONTENT', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'OPENAI_RESPONSES', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
],
|
||||
created_at: '2024-12-09T14:10:36.446217+08:00',
|
||||
@@ -471,18 +471,17 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
billing_type: 'pay_as_you_go',
|
||||
monthly_used_usd: 545.18,
|
||||
is_active: true,
|
||||
total_endpoints: 2,
|
||||
active_endpoints: 2,
|
||||
total_endpoints: 1,
|
||||
active_endpoints: 1,
|
||||
total_keys: 3,
|
||||
active_keys: 3,
|
||||
total_models: 3,
|
||||
active_models: 1,
|
||||
avg_health_score: 0.825,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: ['claude:chat', 'claude:cli'],
|
||||
api_formats: ['claude:messages'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'claude:chat', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'claude:cli', health_score: 0.65, is_active: true, active_keys: 1 }
|
||||
{ api_format: 'claude:messages', health_score: 1.0, is_active: true, active_keys: 2 }
|
||||
],
|
||||
created_at: '2024-12-07T22:58:15.044538+08:00',
|
||||
updated_at: new Date().toISOString()
|
||||
@@ -504,9 +503,9 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
active_models: 5,
|
||||
avg_health_score: 1.0,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: ['claude:cli', 'openai:responses'],
|
||||
api_formats: ['claude:messages', 'openai:responses'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'claude:cli', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'claude:messages', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'openai:responses', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
],
|
||||
created_at: '2024-12-07T22:56:46.361092+08:00',
|
||||
@@ -521,19 +520,18 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
billing_type: 'pay_as_you_go',
|
||||
monthly_used_usd: 268.65,
|
||||
is_active: true,
|
||||
total_endpoints: 4,
|
||||
active_endpoints: 4,
|
||||
total_endpoints: 3,
|
||||
active_endpoints: 3,
|
||||
total_keys: 3,
|
||||
active_keys: 3,
|
||||
total_models: 7,
|
||||
active_models: 7,
|
||||
avg_health_score: 1.0,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: ['claude:cli', 'gemini:chat', 'gemini:cli', 'openai:responses'],
|
||||
api_formats: ['claude:messages', 'gemini:generate_content', 'openai:responses'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'claude:cli', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'gemini:chat', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'gemini:cli', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'claude:messages', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'gemini:generate_content', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||
{ api_format: 'openai:responses', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
],
|
||||
created_at: '2024-12-07T15:16:55.807595+08:00',
|
||||
@@ -548,20 +546,18 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
billing_type: 'pay_as_you_go',
|
||||
monthly_used_usd: 5.29,
|
||||
is_active: true,
|
||||
total_endpoints: 6,
|
||||
active_endpoints: 6,
|
||||
total_endpoints: 4,
|
||||
active_endpoints: 4,
|
||||
total_keys: 11,
|
||||
active_keys: 11,
|
||||
total_models: 8,
|
||||
active_models: 8,
|
||||
avg_health_score: 0.863,
|
||||
unhealthy_endpoints: 1,
|
||||
api_formats: ['claude:chat', 'claude:cli', 'gemini:chat', 'gemini:cli', 'openai:chat', 'openai:responses'],
|
||||
api_formats: ['claude:messages', 'gemini:generate_content', 'openai:chat', 'openai:responses'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'claude:chat', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'claude:cli', health_score: 0.48, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'gemini:chat', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'gemini:cli', health_score: 0.85, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'claude:messages', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'gemini:generate_content', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'openai:chat', health_score: 0.85, is_active: true, active_keys: 2 },
|
||||
{ api_format: 'openai:responses', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
],
|
||||
@@ -607,9 +603,9 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||
active_models: 1,
|
||||
avg_health_score: 1.0,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: ['gemini:chat'],
|
||||
api_formats: ['gemini:generate_content'],
|
||||
endpoint_health_details: [
|
||||
{ api_format: 'gemini:chat', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
{ api_format: 'gemini:generate_content', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||
],
|
||||
created_at: '2024-12-07T23:00:42.559105+08:00',
|
||||
updated_at: new Date().toISOString()
|
||||
@@ -878,15 +874,13 @@ export const MOCK_SYSTEM_CONFIGS = [
|
||||
|
||||
export const MOCK_API_FORMATS = {
|
||||
formats: [
|
||||
{ value: 'claude:chat', label: 'Claude Chat', default_path: '/v1/messages', aliases: [] },
|
||||
{ value: 'claude:cli', label: 'Claude CLI', default_path: '/v1/messages', aliases: [] },
|
||||
{ value: 'claude:messages', label: 'Claude Messages', default_path: '/v1/messages', aliases: [] },
|
||||
{ value: 'openai:chat', label: 'OpenAI Chat', default_path: '/v1/chat/completions', aliases: [] },
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses', default_path: '/v1/responses', aliases: ['openai:cli'] },
|
||||
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact', default_path: '/v1/responses/compact', aliases: ['openai:compact'] },
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses', default_path: '/v1/responses', aliases: [] },
|
||||
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact', default_path: '/v1/responses/compact', aliases: [] },
|
||||
{ value: 'openai:image', label: 'OpenAI Image', default_path: '/v1/images/generations', aliases: [] },
|
||||
{ value: 'openai:video', label: 'OpenAI Video', default_path: '/v1/videos', aliases: [] },
|
||||
{ value: 'gemini:chat', label: 'Gemini Chat', default_path: '/v1beta/models/{model}:{action}', aliases: [] },
|
||||
{ value: 'gemini:cli', label: 'Gemini CLI', default_path: '/v1beta/models/{model}:{action}', aliases: [] },
|
||||
{ value: 'gemini:generate_content', label: 'Gemini Generate Content', default_path: '/v1beta/models/{model}:{action}', aliases: [] },
|
||||
{ value: 'gemini:video', label: 'Gemini Video', default_path: '/v1beta/models/{model}:predictLongRunning', aliases: [] }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ const MOCK_ENDPOINT_STATUS = {
|
||||
generated_at: new Date().toISOString(),
|
||||
formats: [
|
||||
{
|
||||
api_format: 'claude:chat',
|
||||
api_format: 'claude:messages',
|
||||
api_path: '/v1/messages',
|
||||
total_attempts: 2580,
|
||||
success_count: 2540,
|
||||
@@ -160,7 +160,7 @@ const MOCK_ENDPOINT_STATUS = {
|
||||
events: generateHealthEvents(80, 0.984, 0.012, 0.004, 900, 500)
|
||||
},
|
||||
{
|
||||
api_format: 'claude:cli',
|
||||
api_format: 'claude:messages',
|
||||
api_path: '/v1/messages',
|
||||
total_attempts: 1890,
|
||||
success_count: 1780,
|
||||
@@ -174,7 +174,7 @@ const MOCK_ENDPOINT_STATUS = {
|
||||
events: generateHealthEvents(120, 0.942, 0.045, 0.013, 1200, 800)
|
||||
},
|
||||
{
|
||||
api_format: 'gemini:chat',
|
||||
api_format: 'gemini:generate_content',
|
||||
api_path: '/v1beta/models',
|
||||
total_attempts: 890,
|
||||
success_count: 890,
|
||||
@@ -188,7 +188,7 @@ const MOCK_ENDPOINT_STATUS = {
|
||||
events: generateHealthEvents(45, 1.0, 0, 0, 400, 200)
|
||||
},
|
||||
{
|
||||
api_format: 'gemini:cli',
|
||||
api_format: 'gemini:generate_content',
|
||||
api_path: '/v1beta/models',
|
||||
total_attempts: 456,
|
||||
success_count: 450,
|
||||
@@ -324,7 +324,7 @@ function generateMockUsageRecords(count: number = 100) {
|
||||
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: 'bob@demo.aether.ai' }
|
||||
]
|
||||
|
||||
const apiFormats = ['claude:chat', 'claude:cli', 'openai:chat', 'openai:responses', 'gemini:chat', 'gemini:cli']
|
||||
const apiFormats = ['claude:messages', 'openai:chat', 'openai:responses', 'gemini:generate_content']
|
||||
const statusOptions: Array<'completed' | 'failed' | 'streaming'> = ['completed', 'completed', 'completed', 'completed', 'failed', 'streaming']
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -335,11 +335,11 @@ function generateMockUsageRecords(count: number = 100) {
|
||||
// 根据模型类型选择 API 格式
|
||||
let apiFormat = apiFormats[0]
|
||||
if (model.provider === 'anthropic') {
|
||||
apiFormat = Math.random() > 0.3 ? 'claude:cli' : 'claude:chat'
|
||||
apiFormat = 'claude:messages'
|
||||
} else if (model.provider === 'openai') {
|
||||
apiFormat = Math.random() > 0.3 ? 'openai:responses' : 'openai:chat'
|
||||
} else {
|
||||
apiFormat = Math.random() > 0.3 ? 'gemini:cli' : 'gemini:chat'
|
||||
apiFormat = 'gemini:generate_content'
|
||||
}
|
||||
|
||||
const inputTokens = 500 + Math.floor(Math.random() * 10000)
|
||||
@@ -424,7 +424,7 @@ function getMockEndpointExtras(apiFormat: string) {
|
||||
const normalizedFormat = normalizeApiFormat(apiFormat)
|
||||
const extras: Record<string, unknown> = {}
|
||||
|
||||
if (normalizedFormat === 'claude:chat') {
|
||||
if (normalizedFormat === 'claude:messages') {
|
||||
extras.header_rules = [
|
||||
{ action: 'set', key: 'x-app-id', value: 'demo-app' },
|
||||
{ action: 'rename', from: 'x-client-id', to: 'x-client' },
|
||||
@@ -442,12 +442,12 @@ function getMockEndpointExtras(apiFormat: string) {
|
||||
]
|
||||
extras.format_acceptance_config = {
|
||||
enabled: true,
|
||||
accept_formats: ['openai:chat', 'claude:chat']
|
||||
accept_formats: ['openai:chat', 'claude:messages']
|
||||
}
|
||||
extras.config = { upstream_stream_policy: 'force_stream' }
|
||||
} else if (normalizedFormat === 'openai:responses' || normalizedFormat === 'openai:cli') {
|
||||
} else if (normalizedFormat === 'openai:responses') {
|
||||
extras.config = { upstream_stream_policy: 'force_non_stream' }
|
||||
} else if (normalizedFormat === 'gemini:chat') {
|
||||
} else if (normalizedFormat === 'gemini:generate_content') {
|
||||
extras.custom_path = '/v1beta/models/gemini-3-pro-preview:generateContent'
|
||||
extras.body_rules = [
|
||||
{ action: 'drop', path: 'metadata.debug' }
|
||||
@@ -460,16 +460,16 @@ function getMockEndpointExtras(apiFormat: string) {
|
||||
|
||||
// Mock Endpoint Keys
|
||||
const MOCK_ENDPOINT_KEYS = [
|
||||
{ id: 'ekey-001', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...abc1', auth_type: 'api_key', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 0.99, avg_response_time_ms: 1200, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-002', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...def2', auth_type: 'api_key', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 0.95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 0.975, avg_response_time_ms: 1350, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-001', provider_id: 'provider-001', api_formats: ['claude:messages'], api_key_masked: 'sk-ant...abc1', auth_type: 'api_key', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 0.99, avg_response_time_ms: 1200, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-002', provider_id: 'provider-001', api_formats: ['claude:messages'], api_key_masked: 'sk-ant...def2', auth_type: 'api_key', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 0.95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 0.975, avg_response_time_ms: 1350, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-003', provider_id: 'provider-002', api_formats: ['openai:chat'], api_key_masked: 'sk-oai...ghi3', auth_type: 'oauth', name: 'OpenAI OAuth', oauth_email: 'oauth-demo@aether.dev', oauth_expires_at: Math.floor(Date.now() / 1000) + 6 * 3600, oauth_plan_type: 'pro', oauth_account_id: 'acct-demo-002', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.97, consecutive_failures: 0, request_count: 3500, success_count: 3450, error_count: 50, success_rate: 0.986, avg_response_time_ms: 900, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||||
]
|
||||
|
||||
// Mock Endpoints
|
||||
const MOCK_ENDPOINTS = [
|
||||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'claude:chat', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('claude:chat') },
|
||||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'claude:messages', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('claude:messages') },
|
||||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'openai:chat', base_url: 'https://api.openai.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('openai:chat') },
|
||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini:chat', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('gemini:chat') }
|
||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini:generate_content', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('gemini:generate_content') }
|
||||
]
|
||||
|
||||
// Mock 能力定义
|
||||
@@ -3148,14 +3148,14 @@ mockHandlers['GET /api/admin/monitoring/resilience-status'] = async () => {
|
||||
{
|
||||
error_id: 'usage-request-2',
|
||||
error_type: 'server_error',
|
||||
operation: 'Anthropic:claude:chat',
|
||||
operation: 'Anthropic:claude:messages',
|
||||
timestamp: new Date(Date.now() - 11 * 60 * 1000).toISOString(),
|
||||
context: {
|
||||
request_id: 'req-live-002',
|
||||
provider_id: 'provider-anthropic',
|
||||
provider_name: 'Anthropic',
|
||||
model: 'claude-sonnet-4-5',
|
||||
api_format: 'claude:chat',
|
||||
api_format: 'claude:messages',
|
||||
status_code: 502,
|
||||
error_message: '上游返回 502 Bad Gateway'
|
||||
}
|
||||
@@ -3192,7 +3192,7 @@ mockHandlers['GET /api/admin/monitoring/resilience/circuit-history'] = async ()
|
||||
provider_id: 'provider-gemini',
|
||||
provider_name: 'Gemini',
|
||||
key_name: 'gemini-burst',
|
||||
api_format: 'gemini:chat',
|
||||
api_format: 'gemini:generate_content',
|
||||
reason: '正在探测恢复',
|
||||
recovery_seconds: 120,
|
||||
timestamp: new Date(Date.now() - 22 * 60 * 1000).toISOString()
|
||||
|
||||
Reference in New Issue
Block a user