mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
fix(usage): unify OpenAI Live and WebSocket records
This commit is contained in:
@@ -258,7 +258,7 @@
|
||||
<div class="flex flex-wrap gap-2 text-muted-foreground/60">
|
||||
<span>Provider: {{ replayResult.mapping.target_provider }}</span>
|
||||
<span>Endpoint: {{ replayResult.mapping.target_endpoint_id }}</span>
|
||||
<span>Format: {{ replayResult.mapping.target_api_format || '-' }}</span>
|
||||
<span>Format: {{ formatApiFormat(replayResult.mapping.target_api_format) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="replayResult.mapping.mapping_source && replayResult.mapping.mapping_source !== 'none'"
|
||||
@@ -326,6 +326,7 @@ import { dashboardApi, type ReplayResponse, type RequestDetail } from '@/api/das
|
||||
import { getProvidersSummary } from '@/api/endpoints/providers'
|
||||
import { getProviderKeys } from '@/api/endpoints/keys'
|
||||
import type { EndpointAPIKey } from '@/api/endpoints/types'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
<Badge
|
||||
v-if="detail?.status_code === 200"
|
||||
variant="success"
|
||||
data-request-lifecycle-status
|
||||
>
|
||||
{{ detail.status_code }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="detail"
|
||||
variant="destructive"
|
||||
data-request-lifecycle-status
|
||||
>
|
||||
{{ detail.status_code }}
|
||||
</Badge>
|
||||
@@ -58,27 +60,29 @@
|
||||
:title="formatUsageWebSocketTransportTitle(detail)"
|
||||
class="border-sky-500/50 text-xs text-sky-600 dark:text-sky-400"
|
||||
>
|
||||
{{ formatUsageWebSocketTransportLabel(detail) }}
|
||||
WS
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="detail && resolveUsageStreamLabelSegments(detail).hasConversion"
|
||||
data-usage-transport="http"
|
||||
:variant="streamBadgeVariant(resolveUsageStreamLabelSegments(detail).client === '流式')"
|
||||
:class="streamBadgeVariant(resolveUsageStreamLabelSegments(detail).client === '流式') === 'secondary'
|
||||
? 'text-xs inline-flex items-center gap-1'
|
||||
: 'text-xs inline-flex items-center gap-1 border-border/60 text-muted-foreground'"
|
||||
>
|
||||
<span>{{ resolveUsageStreamLabelSegments(detail).client }}</span>
|
||||
<span>HTTP {{ resolveUsageStreamLabelSegments(detail).client }}</span>
|
||||
<span class="opacity-60">→</span>
|
||||
<span>{{ resolveUsageStreamLabelSegments(detail).upstream }}</span>
|
||||
<span>HTTP {{ resolveUsageStreamLabelSegments(detail).upstream }}</span>
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="detail"
|
||||
data-usage-transport="http"
|
||||
:variant="streamBadgeVariant(isUsageUpstreamStream(detail))"
|
||||
:class="streamBadgeVariant(isUsageUpstreamStream(detail)) === 'secondary'
|
||||
? 'text-xs'
|
||||
: 'text-xs border-border/60 text-muted-foreground'"
|
||||
>
|
||||
{{ formatUsageStreamLabel(detail) }}
|
||||
HTTP {{ formatUsageStreamLabel(detail) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
@@ -905,10 +909,7 @@ import {
|
||||
resolveDisplayRequestStatus,
|
||||
resolveUsageStreamLabelSegments,
|
||||
} from '../utils/status'
|
||||
import {
|
||||
formatUsageWebSocketTransportLabel,
|
||||
formatUsageWebSocketTransportTitle,
|
||||
} from '../utils/websocketTransport'
|
||||
import { formatUsageWebSocketTransportTitle } from '../utils/websocketTransport'
|
||||
import { isCyberPolicyError } from '../utils/cyberError'
|
||||
import {
|
||||
mergeUsageRecordErrorMessage,
|
||||
|
||||
@@ -150,13 +150,13 @@
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">
|
||||
全部状态
|
||||
全部类型
|
||||
</SelectItem>
|
||||
<SelectItem value="stream">
|
||||
流式
|
||||
HTTP 流式
|
||||
</SelectItem>
|
||||
<SelectItem value="standard">
|
||||
标准
|
||||
HTTP 标准
|
||||
</SelectItem>
|
||||
<SelectItem value="websocket">
|
||||
WebSocket (WS)
|
||||
@@ -287,7 +287,7 @@
|
||||
:title="getWebSocketTransportTitle(record)"
|
||||
class="whitespace-nowrap border-sky-500/50 text-sky-600 dark:text-sky-400 text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0"
|
||||
>
|
||||
{{ getWebSocketTransportLabel(record) }}
|
||||
WS
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
@@ -878,7 +878,7 @@
|
||||
:title="getWebSocketTransportTitle(record)"
|
||||
class="whitespace-nowrap border-sky-500/50 text-sky-600 dark:text-sky-400"
|
||||
>
|
||||
{{ getWebSocketTransportLabel(record) }}
|
||||
WS
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
@@ -1116,10 +1116,7 @@ import { API_FORMAT_ORDER, formatApiFormat } from '@/api/endpoints/types/api-for
|
||||
import { formatClientFamily } from '@/features/usage/utils/clientFamily'
|
||||
import { formatServiceTierFact } from '../utils/service-tier'
|
||||
import { isCyberPolicyError } from '../utils/cyberError'
|
||||
import {
|
||||
formatUsageWebSocketTransportLabel as getWebSocketTransportLabel,
|
||||
formatUsageWebSocketTransportTitle as getWebSocketTransportTitle,
|
||||
} from '../utils/websocketTransport'
|
||||
import { formatUsageWebSocketTransportTitle as getWebSocketTransportTitle } from '../utils/websocketTransport'
|
||||
import type { DateRangeParams, UsageRecord } from '../types'
|
||||
import { MultiSelect, TimeRangePicker } from '@/components/common'
|
||||
import type { MultiSelectOption } from '@/components/common/MultiSelect.vue'
|
||||
@@ -1364,9 +1361,9 @@ const apiFormatFilterOptions = computed<FilterOption[]>(() => [
|
||||
])
|
||||
|
||||
const statusFilterOptions: FilterOption[] = [
|
||||
{ value: '__all__', label: '全部状态' },
|
||||
{ value: 'stream', label: '流式' },
|
||||
{ value: 'standard', label: '标准' },
|
||||
{ value: '__all__', label: '全部类型' },
|
||||
{ value: 'stream', label: 'HTTP 流式' },
|
||||
{ value: 'standard', label: 'HTTP 标准' },
|
||||
{ value: 'websocket', label: 'WebSocket (WS)' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
@@ -1620,7 +1617,7 @@ function getApiFormatTooltip(record: UsageRecord): string {
|
||||
return `用户请求格式: ${displayFormat}\n端点原生格式: ${endpointDisplayFormat}\n${conversionType}`
|
||||
}
|
||||
|
||||
return record.api_format
|
||||
return displayFormat
|
||||
}
|
||||
|
||||
// 获取实际使用的模型(优先 target_model,其次列表接口下发的 model_version)
|
||||
|
||||
+10
-3
@@ -113,7 +113,7 @@ function buildFastTierDetail(): RequestDetail {
|
||||
}
|
||||
|
||||
describe('RequestDetailDrawer settlement pricing', () => {
|
||||
it('labels an unmetered Codex Live WebSocket detail without rendering zero usage as billing', async () => {
|
||||
it('labels an unmetered OpenAI Live WebSocket detail without rendering zero usage as billing', async () => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue({
|
||||
...buildEmbeddingDetail(),
|
||||
id: 'usage-live-ws-1',
|
||||
@@ -152,8 +152,11 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const transport = document.body.querySelector<HTMLElement>('[data-usage-transport="websocket"]')
|
||||
expect(transport?.textContent?.trim()).toBe('Live WS')
|
||||
expect(transport?.title).toBe('Codex Live Sideband WebSocket')
|
||||
expect(transport?.textContent?.trim()).toBe('WS')
|
||||
expect(transport?.title).toBe('OpenAI Live Sideband WebSocket')
|
||||
expect(document.body.querySelector('[data-usage-transport="http"]')).toBeNull()
|
||||
expect(document.body.querySelector('[data-request-lifecycle-status]')?.textContent?.trim())
|
||||
.toBe('200')
|
||||
expect(document.body.querySelector('[data-request-detail-usage-unavailable]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -198,6 +201,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
await nextTick()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body.querySelector('[data-usage-transport="http"]')?.textContent?.trim())
|
||||
.toBe('HTTP 流式')
|
||||
expect(document.body.textContent).toContain('10.12s / 10.63s')
|
||||
expect(document.body.textContent).toContain('95.1tps')
|
||||
expect(document.body.textContent).not.toContain('98.8tps')
|
||||
@@ -228,6 +233,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
await nextTick()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body.querySelector('[data-usage-transport="http"]')?.textContent?.trim())
|
||||
.toBe('HTTP 标准')
|
||||
expect(document.body.textContent).toContain('输入 $0.1/M')
|
||||
expect(document.body.textContent).toContain('输出 -')
|
||||
})
|
||||
|
||||
@@ -350,15 +350,30 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
})
|
||||
|
||||
it('shows WS in the type column for completed WebSocket usage records', () => {
|
||||
it('keeps the type label as WS after Responses detail enriches transport metadata', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
is_websocket: true,
|
||||
websocket_transport: 'responses',
|
||||
status: 'completed',
|
||||
})])
|
||||
|
||||
const badges = [...root.querySelectorAll<HTMLElement>('[data-usage-transport="websocket"]')]
|
||||
expect(badges.length).toBeGreaterThan(0)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.title === 'OpenAI Responses WebSocket')).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to WS when transport metadata is unavailable', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
is_websocket: true,
|
||||
websocket_transport: null,
|
||||
status: 'completed',
|
||||
})])
|
||||
|
||||
const badges = [...root.querySelectorAll<HTMLElement>('[data-usage-transport="websocket"]')]
|
||||
expect(badges.length).toBeGreaterThan(0)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.title === 'WebSocket')).toBe(true)
|
||||
})
|
||||
|
||||
it('offers a WebSocket type filter in both filter menus', () => {
|
||||
@@ -367,9 +382,11 @@ describe('UsageRecordsTable', () => {
|
||||
const labels = [...root.querySelectorAll<HTMLElement>('div')]
|
||||
.filter(element => element.textContent?.trim() === 'WebSocket (WS)')
|
||||
expect(labels.length).toBeGreaterThanOrEqual(2)
|
||||
expect(root.textContent).toContain('HTTP 流式')
|
||||
expect(root.textContent).toContain('HTTP 标准')
|
||||
})
|
||||
|
||||
it('labels Codex Live transport rows without inventing token or cost usage', () => {
|
||||
it('keeps OpenAI Live transport rows under the single WS type without inventing usage', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
is_websocket: true,
|
||||
websocket_transport: 'codex_live_direct',
|
||||
@@ -382,8 +399,8 @@ describe('UsageRecordsTable', () => {
|
||||
|
||||
const badges = [...root.querySelectorAll<HTMLElement>('[data-usage-transport="websocket"]')]
|
||||
expect(badges.length).toBeGreaterThan(0)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'Live WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.title === 'Codex Live 直连 WebSocket')).toBe(true)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.title === 'OpenAI Live 直连 WebSocket')).toBe(true)
|
||||
|
||||
const unavailableTokens = [...root.querySelectorAll<HTMLElement>('[data-usage-unavailable="tokens"]')]
|
||||
const unavailableCosts = [...root.querySelectorAll<HTMLElement>('[data-usage-unavailable="cost"]')]
|
||||
@@ -393,7 +410,7 @@ describe('UsageRecordsTable', () => {
|
||||
expect(unavailableCosts.every(element => element.textContent?.trim() === '不可用')).toBe(true)
|
||||
})
|
||||
|
||||
it('labels OpenAI Realtime WebSocket rows consistently in both layouts', () => {
|
||||
it('keeps OpenAI Realtime rows under the single WS type in both layouts', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
is_websocket: true,
|
||||
websocket_transport: 'openai_realtime',
|
||||
@@ -401,7 +418,7 @@ describe('UsageRecordsTable', () => {
|
||||
|
||||
const badges = [...root.querySelectorAll<HTMLElement>('[data-usage-transport="websocket"]')]
|
||||
expect(badges.length).toBeGreaterThan(0)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'Realtime WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.textContent?.trim() === 'WS')).toBe(true)
|
||||
expect(badges.every(badge => badge.title === 'OpenAI Realtime WebSocket')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -603,6 +620,18 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('Gemini Embedding')
|
||||
expect(root.textContent).toContain('Jina Embedding')
|
||||
expect(root.textContent).toContain('Doubao Embedding')
|
||||
const liveLabels = [...root.querySelectorAll<HTMLElement>('div')]
|
||||
.filter(element => element.textContent?.trim() === 'OpenAI Live')
|
||||
expect(liveLabels.length).toBeGreaterThanOrEqual(2)
|
||||
expect(root.querySelector('[data-filter-value="codex:live"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not expose the canonical Codex Live id in row labels or tooltips', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ api_format: 'codex:live' })])
|
||||
|
||||
expect(root.textContent).toContain('OpenAI Live')
|
||||
expect(root.textContent).not.toContain('codex:live')
|
||||
expect(root.querySelector('[title="OpenAI Live"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('emits hide unknown toggle changes', () => {
|
||||
|
||||
@@ -143,11 +143,12 @@ describe('useUsageData', () => {
|
||||
|
||||
await loadRecords(
|
||||
{ page: 2, pageSize: 20 },
|
||||
{ status: 'websocket' },
|
||||
{ api_format: 'codex:live', status: 'websocket' },
|
||||
{ preset: 'today', tz_offset_minutes: 0 },
|
||||
)
|
||||
|
||||
expect(meGetUsageMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
api_format: 'codex:live',
|
||||
status: 'websocket',
|
||||
limit: 20,
|
||||
offset: 20,
|
||||
|
||||
@@ -393,6 +393,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
if (filters?.status) {
|
||||
params.status = filters.status
|
||||
}
|
||||
if (filters?.api_format) {
|
||||
params.api_format = filters.api_format
|
||||
}
|
||||
|
||||
if (isAdminPage.value) {
|
||||
// 管理员页面:使用管理员 API
|
||||
@@ -405,9 +408,6 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
if (filters?.provider) {
|
||||
params.provider = filters.provider
|
||||
}
|
||||
if (filters?.api_format) {
|
||||
params.api_format = filters.api_format
|
||||
}
|
||||
if (filters?.client_family) {
|
||||
params.client_family = filters.client_family
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
isUserLocalOnlyRecordStatus,
|
||||
shouldUseServerUserRecordFilters,
|
||||
} from '../recordFilterPolicy'
|
||||
|
||||
describe('normal-user usage record filter policy', () => {
|
||||
it.each(['has_retry', 'has_fallback'] as const)(
|
||||
'keeps %s local even when combined with server-supported filters',
|
||||
(status) => {
|
||||
expect(isUserLocalOnlyRecordStatus(status)).toBe(true)
|
||||
expect(shouldUseServerUserRecordFilters({
|
||||
search: 'production',
|
||||
apiFormat: 'codex:live',
|
||||
status,
|
||||
})).toBe(false)
|
||||
},
|
||||
)
|
||||
|
||||
it.each([
|
||||
{ search: '', apiFormat: '__all__', status: 'websocket' as const },
|
||||
{ search: '', apiFormat: 'codex:live', status: '__all__' as const },
|
||||
{ search: 'production', apiFormat: '__all__', status: '__all__' as const },
|
||||
{ search: 'production', apiFormat: 'codex:live', status: 'websocket' as const },
|
||||
])('uses server pagination for supported filter combination %#', (filters) => {
|
||||
expect(shouldUseServerUserRecordFilters(filters)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the unfiltered normal-user list on its existing local pagination path', () => {
|
||||
expect(shouldUseServerUserRecordFilters({
|
||||
search: ' ',
|
||||
apiFormat: '__all__',
|
||||
status: '__all__',
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { UsageRecord } from '../../types'
|
||||
import { matchesUsageRecordSearch } from '../recordSearch'
|
||||
|
||||
function record(overrides: Partial<UsageRecord> = {}): UsageRecord {
|
||||
return {
|
||||
id: 'usage-1',
|
||||
model: 'gpt-5.6-sol',
|
||||
provider: 'OpenAI',
|
||||
api_key: {
|
||||
id: 'key-1',
|
||||
name: 'Production Key',
|
||||
display: 'Production Key',
|
||||
},
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
total_tokens: 0,
|
||||
cost: 0,
|
||||
is_stream: false,
|
||||
created_at: '2026-08-21T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('matchesUsageRecordSearch', () => {
|
||||
it('matches normal-user key and model search terms case-insensitively', () => {
|
||||
const usage = record()
|
||||
|
||||
expect(matchesUsageRecordSearch(usage, 'production')).toBe(true)
|
||||
expect(matchesUsageRecordSearch(usage, 'GPT-5.6')).toBe(true)
|
||||
expect(matchesUsageRecordSearch(usage, 'production gpt-5.6')).toBe(true)
|
||||
})
|
||||
|
||||
it('requires every whitespace-delimited search term to match the record', () => {
|
||||
const usage = record()
|
||||
|
||||
expect(matchesUsageRecordSearch(usage, 'production missing')).toBe(false)
|
||||
expect(matchesUsageRecordSearch(usage, ' ')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { FilterStatusValue } from '../types'
|
||||
|
||||
export function isUserLocalOnlyRecordStatus(status: FilterStatusValue): boolean {
|
||||
return status === 'has_retry' || status === 'has_fallback'
|
||||
}
|
||||
|
||||
export function shouldUseServerUserRecordFilters(input: {
|
||||
search: string
|
||||
apiFormat: string
|
||||
status: FilterStatusValue
|
||||
}): boolean {
|
||||
if (isUserLocalOnlyRecordStatus(input.status)) return false
|
||||
|
||||
return input.search.trim().length > 0
|
||||
|| input.apiFormat !== '__all__'
|
||||
|| input.status !== '__all__'
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { UsageRecord } from '../types'
|
||||
|
||||
type SearchableUsageRecord = Pick<
|
||||
UsageRecord,
|
||||
| 'model'
|
||||
| 'target_model'
|
||||
| 'provider'
|
||||
| 'api_key'
|
||||
| 'api_key_name'
|
||||
| 'provider_key_name'
|
||||
>
|
||||
|
||||
function normalizedSearchValues(record: SearchableUsageRecord): string[] {
|
||||
return [
|
||||
record.model,
|
||||
record.target_model,
|
||||
record.provider,
|
||||
record.api_key?.id,
|
||||
record.api_key?.name,
|
||||
record.api_key?.display,
|
||||
record.api_key_name,
|
||||
record.provider_key_name,
|
||||
]
|
||||
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||
.map(value => value.toLocaleLowerCase())
|
||||
}
|
||||
|
||||
export function matchesUsageRecordSearch(
|
||||
record: SearchableUsageRecord,
|
||||
search: string,
|
||||
): boolean {
|
||||
const keywords = search
|
||||
.trim()
|
||||
.toLocaleLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
if (keywords.length === 0) return true
|
||||
|
||||
const values = normalizedSearchValues(record)
|
||||
return keywords.every(keyword => values.some(value => value.includes(keyword)))
|
||||
}
|
||||
@@ -8,23 +8,6 @@ function normalizeWebSocketTransport(
|
||||
return record.websocket_transport?.trim().toLowerCase() ?? ''
|
||||
}
|
||||
|
||||
export function formatUsageWebSocketTransportLabel(
|
||||
record: UsageWebSocketTransportLike,
|
||||
): string {
|
||||
switch (normalizeWebSocketTransport(record)) {
|
||||
case 'responses':
|
||||
return 'Responses WS'
|
||||
case 'codex_live_direct':
|
||||
case 'codex_live_sideband':
|
||||
return 'Live WS'
|
||||
case 'openai_realtime':
|
||||
case 'realtime':
|
||||
return 'Realtime WS'
|
||||
default:
|
||||
return 'WS'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUsageWebSocketTransportTitle(
|
||||
record: UsageWebSocketTransportLike,
|
||||
): string {
|
||||
@@ -33,9 +16,9 @@ export function formatUsageWebSocketTransportTitle(
|
||||
case 'responses':
|
||||
return 'OpenAI Responses WebSocket'
|
||||
case 'codex_live_direct':
|
||||
return 'Codex Live 直连 WebSocket'
|
||||
return 'OpenAI Live 直连 WebSocket'
|
||||
case 'codex_live_sideband':
|
||||
return 'Codex Live Sideband WebSocket'
|
||||
return 'OpenAI Live Sideband WebSocket'
|
||||
case 'openai_realtime':
|
||||
case 'realtime':
|
||||
return 'OpenAI Realtime WebSocket'
|
||||
|
||||
Reference in New Issue
Block a user