mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
feat(gateway): add Codex Live and OpenAI Realtime
Implement preflighted Live/Realtime WebSocket transports, protocol-aware authentication, usage auditing, UI filtering, and legacy Codex permission migration.
This commit is contained in:
@@ -153,4 +153,29 @@ describe('usageApi contract alignment', () => {
|
||||
timeout: 120000,
|
||||
})
|
||||
})
|
||||
|
||||
it('passes the WebSocket usage type filter to the admin records endpoint', async () => {
|
||||
getMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
records: [],
|
||||
total: 0,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
},
|
||||
})
|
||||
|
||||
await usageApi.getAllUsageRecords({
|
||||
status: 'websocket',
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/api/admin/usage/records', {
|
||||
params: {
|
||||
status: 'websocket',
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -251,6 +251,12 @@ export interface RequestDetail {
|
||||
price_per_request?: number // 按次计费价格
|
||||
request_type: string
|
||||
is_stream: boolean
|
||||
is_websocket?: boolean
|
||||
websocket_transport?: string | null
|
||||
usage_available?: boolean
|
||||
usage_pricing_available?: boolean
|
||||
input_audio_tokens?: number | null
|
||||
output_audio_tokens?: number | null
|
||||
upstream_is_stream?: boolean
|
||||
client_requested_stream?: boolean
|
||||
client_is_stream?: boolean
|
||||
|
||||
@@ -15,6 +15,10 @@ describe('api format display helpers', () => {
|
||||
expect(normalizeApiFormatAlias('CLAUDE_MESSAGES')).toBe(API_FORMATS.CLAUDE_MESSAGES)
|
||||
expect(normalizeApiFormatAlias('OPENAI_RESPONSES')).toBe(API_FORMATS.OPENAI_RESPONSES)
|
||||
expect(normalizeApiFormatAlias('OPENAI_RESPONSES_COMPACT')).toBe(API_FORMATS.OPENAI_RESPONSES_COMPACT)
|
||||
expect(normalizeApiFormatAlias('OPENAI_REALTIME')).toBe(API_FORMATS.OPENAI_REALTIME)
|
||||
expect(normalizeApiFormatAlias('REALTIME')).toBe(API_FORMATS.OPENAI_REALTIME)
|
||||
expect(normalizeApiFormatAlias('CODEX_LIVE')).toBe(API_FORMATS.CODEX_LIVE)
|
||||
expect(normalizeApiFormatAlias('LIVE')).toBe(API_FORMATS.CODEX_LIVE)
|
||||
expect(normalizeApiFormatAlias('OPENAI_SEARCH')).toBe(API_FORMATS.OPENAI_SEARCH)
|
||||
expect(normalizeApiFormatAlias('SEARCH')).toBe(API_FORMATS.OPENAI_SEARCH)
|
||||
expect(normalizeApiFormatAlias('GEMINI_GENERATE_CONTENT')).toBe(API_FORMATS.GEMINI_GENERATE_CONTENT)
|
||||
@@ -57,6 +61,26 @@ describe('api format display helpers', () => {
|
||||
expect(apiFormatPermissionCovers('openai:responses:compact', 'openai:responses')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps public Realtime and private Codex Live formats distinct', () => {
|
||||
expect(formatApiFormat(API_FORMATS.OPENAI_REALTIME)).toBe('OpenAI Realtime')
|
||||
expect(formatApiFormatShort(API_FORMATS.OPENAI_REALTIME)).toBe('ORT')
|
||||
expect(formatApiFormat(API_FORMATS.CODEX_LIVE)).toBe('Codex Live')
|
||||
expect(formatApiFormatShort(API_FORMATS.CODEX_LIVE)).toBe('CL')
|
||||
expect(apiFormatPermissionCovers('openai:realtime', 'openai:realtime')).toBe(true)
|
||||
expect(apiFormatPermissionCovers('codex:live', 'codex:live')).toBe(true)
|
||||
expect(apiFormatPermissionCovers('openai:responses', 'openai:realtime')).toBe(false)
|
||||
expect(apiFormatPermissionCovers('openai:responses', 'codex:live')).toBe(false)
|
||||
expect(apiFormatPermissionCovers('openai:realtime', 'codex:live')).toBe(false)
|
||||
expect(apiFormatPermissionCovers('codex:live', 'openai:realtime')).toBe(false)
|
||||
expect(groupApiFormats([
|
||||
API_FORMATS.CODEX_LIVE,
|
||||
API_FORMATS.OPENAI_REALTIME,
|
||||
])).toEqual([
|
||||
{ family: 'openai', label: 'OpenAI', formats: [API_FORMATS.OPENAI_REALTIME] },
|
||||
{ family: 'codex', label: 'Codex', formats: [API_FORMATS.CODEX_LIVE] },
|
||||
])
|
||||
})
|
||||
|
||||
it('formats embedding api format ids distinctly from chat formats', () => {
|
||||
expect(formatApiFormat(API_FORMATS.GEMINI_INTERACTIONS)).toBe('Gemini Interactions')
|
||||
expect(formatApiFormatShort(API_FORMATS.GEMINI_INTERACTIONS)).toBe('GI')
|
||||
|
||||
@@ -6,11 +6,13 @@ export const API_FORMATS = {
|
||||
OPENAI: 'openai:chat',
|
||||
OPENAI_RESPONSES: 'openai:responses',
|
||||
OPENAI_RESPONSES_COMPACT: 'openai:responses:compact',
|
||||
OPENAI_REALTIME: 'openai:realtime',
|
||||
OPENAI_SEARCH: 'openai:search',
|
||||
OPENAI_IMAGE: 'openai:image',
|
||||
OPENAI_VIDEO: 'openai:video',
|
||||
OPENAI_EMBEDDING: 'openai:embedding',
|
||||
OPENAI_RERANK: 'openai:rerank',
|
||||
CODEX_LIVE: 'codex:live',
|
||||
GEMINI: 'gemini:generate_content',
|
||||
GEMINI_GENERATE_CONTENT: 'gemini:generate_content',
|
||||
GEMINI_INTERACTIONS: 'gemini:interactions',
|
||||
@@ -31,11 +33,13 @@ export const API_FORMAT_LABELS: Record<string, string> = {
|
||||
[API_FORMATS.OPENAI]: 'OpenAI Chat',
|
||||
[API_FORMATS.OPENAI_RESPONSES]: 'OpenAI Responses',
|
||||
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'OpenAI Responses Compact',
|
||||
[API_FORMATS.OPENAI_REALTIME]: 'OpenAI Realtime',
|
||||
[API_FORMATS.OPENAI_SEARCH]: 'OpenAI Search',
|
||||
[API_FORMATS.OPENAI_IMAGE]: 'OpenAI Image',
|
||||
[API_FORMATS.OPENAI_VIDEO]: 'OpenAI Video',
|
||||
[API_FORMATS.OPENAI_EMBEDDING]: 'OpenAI Embedding',
|
||||
[API_FORMATS.OPENAI_RERANK]: 'OpenAI Rerank',
|
||||
[API_FORMATS.CODEX_LIVE]: 'Codex Live',
|
||||
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'Gemini Generate Content',
|
||||
[API_FORMATS.GEMINI_INTERACTIONS]: 'Gemini Interactions',
|
||||
[API_FORMATS.GEMINI_VIDEO]: 'Gemini Video',
|
||||
@@ -50,11 +54,15 @@ export const API_FORMAT_LABELS: Record<string, string> = {
|
||||
OPENAI: 'OpenAI Chat',
|
||||
OPENAI_RESPONSES: 'OpenAI Responses',
|
||||
OPENAI_RESPONSES_COMPACT: 'OpenAI Responses Compact',
|
||||
OPENAI_REALTIME: 'OpenAI Realtime',
|
||||
REALTIME: 'OpenAI Realtime',
|
||||
OPENAI_SEARCH: 'OpenAI Search',
|
||||
OPENAI_IMAGE: 'OpenAI Image',
|
||||
OPENAI_VIDEO: 'OpenAI Video',
|
||||
OPENAI_EMBEDDING: 'OpenAI Embedding',
|
||||
OPENAI_RERANK: 'OpenAI Rerank',
|
||||
CODEX_LIVE: 'Codex Live',
|
||||
LIVE: 'Codex Live',
|
||||
GEMINI: 'Gemini Generate Content',
|
||||
GEMINI_GENERATE_CONTENT: 'Gemini Generate Content',
|
||||
GEMINI_INTERACTIONS: 'Gemini Interactions',
|
||||
@@ -72,11 +80,13 @@ export const API_FORMAT_SHORT: Record<string, string> = {
|
||||
[API_FORMATS.OPENAI]: 'O',
|
||||
[API_FORMATS.OPENAI_RESPONSES]: 'OR',
|
||||
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'ORC',
|
||||
[API_FORMATS.OPENAI_REALTIME]: 'ORT',
|
||||
[API_FORMATS.OPENAI_SEARCH]: 'OS',
|
||||
[API_FORMATS.OPENAI_IMAGE]: 'OI',
|
||||
[API_FORMATS.OPENAI_VIDEO]: 'OV',
|
||||
[API_FORMATS.OPENAI_EMBEDDING]: 'OE',
|
||||
[API_FORMATS.OPENAI_RERANK]: 'ORR',
|
||||
[API_FORMATS.CODEX_LIVE]: 'CL',
|
||||
[API_FORMATS.CLAUDE_MESSAGES]: 'CM',
|
||||
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'G',
|
||||
[API_FORMATS.GEMINI_INTERACTIONS]: 'GI',
|
||||
@@ -90,11 +100,15 @@ export const API_FORMAT_SHORT: Record<string, string> = {
|
||||
OPENAI: 'O',
|
||||
OPENAI_RESPONSES: 'OR',
|
||||
OPENAI_RESPONSES_COMPACT: 'ORC',
|
||||
OPENAI_REALTIME: 'ORT',
|
||||
REALTIME: 'ORT',
|
||||
OPENAI_SEARCH: 'OS',
|
||||
OPENAI_IMAGE: 'OI',
|
||||
OPENAI_VIDEO: 'OV',
|
||||
OPENAI_EMBEDDING: 'OE',
|
||||
OPENAI_RERANK: 'ORR',
|
||||
CODEX_LIVE: 'CL',
|
||||
LIVE: 'CL',
|
||||
CLAUDE: 'CM',
|
||||
CLAUDE_MESSAGES: 'CM',
|
||||
GEMINI: 'G',
|
||||
@@ -114,11 +128,13 @@ export const API_FORMAT_ORDER: string[] = [
|
||||
API_FORMATS.OPENAI,
|
||||
API_FORMATS.OPENAI_RESPONSES,
|
||||
API_FORMATS.OPENAI_RESPONSES_COMPACT,
|
||||
API_FORMATS.OPENAI_REALTIME,
|
||||
API_FORMATS.OPENAI_SEARCH,
|
||||
API_FORMATS.OPENAI_EMBEDDING,
|
||||
API_FORMATS.OPENAI_RERANK,
|
||||
API_FORMATS.OPENAI_IMAGE,
|
||||
API_FORMATS.OPENAI_VIDEO,
|
||||
API_FORMATS.CODEX_LIVE,
|
||||
API_FORMATS.CLAUDE_MESSAGES,
|
||||
API_FORMATS.GEMINI_GENERATE_CONTENT,
|
||||
API_FORMATS.GEMINI_INTERACTIONS,
|
||||
@@ -134,6 +150,7 @@ export const API_FORMAT_ORDER: string[] = [
|
||||
// Family 显示名称映射
|
||||
export const API_FORMAT_FAMILY_LABELS: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
codex: 'Codex',
|
||||
claude: 'Claude',
|
||||
gemini: 'Gemini',
|
||||
jina: 'Jina',
|
||||
@@ -146,6 +163,8 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
|
||||
chat: 'Chat',
|
||||
responses: 'Responses',
|
||||
'responses:compact': 'Responses Compact',
|
||||
realtime: 'Realtime',
|
||||
live: 'Live',
|
||||
search: 'Search',
|
||||
messages: 'Messages',
|
||||
generate_content: 'Generate Content',
|
||||
@@ -158,7 +177,7 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
// Family 排序顺序
|
||||
const FAMILY_ORDER = ['openai', 'claude', 'gemini', 'jina', 'doubao', 'aliyun']
|
||||
const FAMILY_ORDER = ['openai', 'codex', 'claude', 'gemini', 'jina', 'doubao', 'aliyun']
|
||||
|
||||
// 工具函数:从 API 格式中提取 family 和 kind
|
||||
export function parseApiFormat(format: string): { family: string; kind: string } {
|
||||
@@ -181,6 +200,9 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
|
||||
return API_FORMATS.OPENAI_RESPONSES
|
||||
case 'OPENAI_RESPONSES_COMPACT':
|
||||
return API_FORMATS.OPENAI_RESPONSES_COMPACT
|
||||
case 'OPENAI_REALTIME':
|
||||
case 'REALTIME':
|
||||
return API_FORMATS.OPENAI_REALTIME
|
||||
case 'OPENAI_SEARCH':
|
||||
case 'SEARCH':
|
||||
return API_FORMATS.OPENAI_SEARCH
|
||||
@@ -192,6 +214,9 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
|
||||
return API_FORMATS.OPENAI_EMBEDDING
|
||||
case 'OPENAI_RERANK':
|
||||
return API_FORMATS.OPENAI_RERANK
|
||||
case 'CODEX_LIVE':
|
||||
case 'LIVE':
|
||||
return API_FORMATS.CODEX_LIVE
|
||||
case 'GEMINI':
|
||||
case 'GEMINI_GENERATE_CONTENT':
|
||||
return API_FORMATS.GEMINI_GENERATE_CONTENT
|
||||
|
||||
@@ -74,6 +74,11 @@ export interface UsageRecordDetail {
|
||||
response_time_updated_at?: string | null
|
||||
is_stream: boolean
|
||||
is_websocket?: boolean
|
||||
websocket_transport?: string | null
|
||||
usage_available?: boolean
|
||||
usage_pricing_available?: boolean
|
||||
input_audio_tokens?: number | null
|
||||
output_audio_tokens?: number | null
|
||||
upstream_is_stream?: boolean
|
||||
client_requested_stream?: boolean
|
||||
client_is_stream?: boolean
|
||||
@@ -338,6 +343,7 @@ export const meApi = {
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
search?: string // 通用搜索:密钥名、模型名
|
||||
status?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<UsageResponse> {
|
||||
@@ -372,6 +378,11 @@ export const meApi = {
|
||||
endpoint_api_format?: string | null
|
||||
is_stream?: boolean | null
|
||||
is_websocket?: boolean | null
|
||||
websocket_transport?: string | null
|
||||
usage_available?: boolean | null
|
||||
usage_pricing_available?: boolean | null
|
||||
input_audio_tokens?: number | null
|
||||
output_audio_tokens?: number | null
|
||||
upstream_is_stream?: boolean | null
|
||||
client_requested_stream?: boolean | null
|
||||
client_is_stream?: boolean | null
|
||||
|
||||
@@ -34,6 +34,11 @@ export interface UsageRecord {
|
||||
end_to_end_time_ms?: number | null
|
||||
end_to_end_first_byte_time_ms?: number | null
|
||||
is_websocket?: boolean
|
||||
websocket_transport?: string | null
|
||||
usage_available?: boolean
|
||||
usage_pricing_available?: boolean
|
||||
input_audio_tokens?: number | null
|
||||
output_audio_tokens?: number | null
|
||||
created_at: string
|
||||
updated_at?: string | null
|
||||
response_time_updated_at?: string | null
|
||||
@@ -139,6 +144,21 @@ export interface UsageRequestOptions {
|
||||
skipCache?: boolean
|
||||
}
|
||||
|
||||
export type UsageRecordStatusFilter =
|
||||
| 'stream'
|
||||
| 'standard'
|
||||
| 'websocket'
|
||||
| 'ws'
|
||||
| 'active'
|
||||
| 'pending'
|
||||
| 'streaming'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'error'
|
||||
| 'cancelled'
|
||||
| 'has_fallback'
|
||||
| 'has_retry'
|
||||
|
||||
type UsageListResponse = {
|
||||
records?: unknown
|
||||
pagination?: {
|
||||
@@ -483,7 +503,7 @@ export const usageApi = {
|
||||
model?: string
|
||||
provider?: string
|
||||
api_format?: string // API 格式筛选(如 openai:chat, claude:messages)
|
||||
status?: string // 'stream' | 'standard' | 'error'
|
||||
status?: UsageRecordStatusFilter
|
||||
client_family?: string
|
||||
hide_unknown?: boolean
|
||||
include_total?: boolean
|
||||
@@ -516,7 +536,7 @@ export const usageApi = {
|
||||
model?: string
|
||||
provider?: string
|
||||
api_format?: string
|
||||
status?: string
|
||||
status?: UsageRecordStatusFilter
|
||||
client_family?: string
|
||||
hide_unknown?: boolean
|
||||
}): Promise<number> {
|
||||
@@ -572,6 +592,11 @@ export const usageApi = {
|
||||
endpoint_api_format?: string | null
|
||||
is_stream?: boolean | null
|
||||
is_websocket?: boolean | null
|
||||
websocket_transport?: string | null
|
||||
usage_available?: boolean | null
|
||||
usage_pricing_available?: boolean | null
|
||||
input_audio_tokens?: number | null
|
||||
output_audio_tokens?: number | null
|
||||
upstream_is_stream?: boolean | null
|
||||
client_requested_stream?: boolean | null
|
||||
client_is_stream?: boolean | null
|
||||
|
||||
Reference in New Issue
Block a user