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:
ZheFox
2026-08-21 04:27:34 +08:00
parent fe38dcd294
commit 2c89202001
105 changed files with 7553 additions and 947 deletions
@@ -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,
},
})
})
})
+6
View File
@@ -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')
+26 -1
View File
@@ -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
+11
View File
@@ -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
+27 -2
View File
@@ -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