diff --git a/frontend/src/api/endpoints/providers.ts b/frontend/src/api/endpoints/providers.ts
index eb8c590f4..4df9d509e 100644
--- a/frontend/src/api/endpoints/providers.ts
+++ b/frontend/src/api/endpoints/providers.ts
@@ -55,6 +55,7 @@ function normalizeProviderSummary(
kiro_simulated_cache_enabled: provider.kiro_simulated_cache_enabled ?? false,
max_transfer_count: provider.max_transfer_count ?? 0,
max_transfer_timeout_seconds: provider.max_transfer_timeout_seconds ?? 0,
+ responses_websocket_enabled: provider.responses_websocket_enabled ?? false,
}
}
@@ -115,6 +116,7 @@ export async function updateProvider(
website: string
provider_priority: number
keep_priority_on_conversion: boolean
+ responses_websocket_enabled: boolean
billing_type: 'monthly_quota' | 'pay_as_you_go' | 'free_tier'
monthly_quota_usd: number
quota_reset_day: number
@@ -158,6 +160,7 @@ export async function createProvider(
quota_expires_at?: string
provider_priority?: number
keep_priority_on_conversion?: boolean
+ responses_websocket_enabled?: boolean
is_active?: boolean
max_retries?: number
max_transfer_count?: number
diff --git a/frontend/src/api/endpoints/types/provider.ts b/frontend/src/api/endpoints/types/provider.ts
index 1aadc704a..e2b6980b4 100644
--- a/frontend/src/api/endpoints/types/provider.ts
+++ b/frontend/src/api/endpoints/types/provider.ts
@@ -185,9 +185,14 @@ export interface CodexProviderConfig {
[key: string]: unknown
}
+export interface ResponsesWebSocketProviderConfig {
+ enabled: boolean
+}
+
export interface ProviderConfig {
chat_pii_redaction?: ChatPiiRedactionProviderConfig
codex?: CodexProviderConfig
+ responses_websocket?: ResponsesWebSocketProviderConfig
pool_advanced?: PoolAdvancedConfig
failover_rules?: FailoverRulesConfig
claude_code_advanced?: ClaudeCodeAdvancedConfig
@@ -913,6 +918,7 @@ export interface ProviderWithEndpointsSummary {
ops_architecture_id?: string // 扩展操作使用的架构 ID(如 cubence, anyrouter)
codex_fingerprint_convergence_enabled?: boolean
kiro_simulated_cache_enabled?: boolean
+ responses_websocket_enabled?: boolean
ops_quota_alert_enabled?: boolean
created_at: string
updated_at: string
diff --git a/frontend/src/api/me.ts b/frontend/src/api/me.ts
index e91dbea7f..108fb8ffc 100644
--- a/frontend/src/api/me.ts
+++ b/frontend/src/api/me.ts
@@ -73,6 +73,7 @@ export interface UsageRecordDetail {
updated_at?: string | null
response_time_updated_at?: string | null
is_stream: boolean
+ is_websocket?: boolean
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
@@ -370,6 +371,7 @@ export const meApi = {
api_format?: string | null
endpoint_api_format?: string | null
is_stream?: boolean | null
+ is_websocket?: boolean | null
upstream_is_stream?: boolean | null
client_requested_stream?: boolean | null
client_is_stream?: boolean | null
diff --git a/frontend/src/api/usage.ts b/frontend/src/api/usage.ts
index 425a78c26..0028146e7 100644
--- a/frontend/src/api/usage.ts
+++ b/frontend/src/api/usage.ts
@@ -33,6 +33,7 @@ export interface UsageRecord {
first_byte_time_ms?: number | null
end_to_end_time_ms?: number | null
end_to_end_first_byte_time_ms?: number | null
+ is_websocket?: boolean
created_at: string
updated_at?: string | null
response_time_updated_at?: string | null
@@ -570,6 +571,7 @@ export const usageApi = {
api_format?: string | null
endpoint_api_format?: string | null
is_stream?: boolean | null
+ is_websocket?: boolean | null
upstream_is_stream?: boolean | null
client_requested_stream?: boolean | null
client_is_stream?: boolean | null
diff --git a/frontend/src/features/providers/components/ProviderFormDialog.vue b/frontend/src/features/providers/components/ProviderFormDialog.vue
index 74edf1799..387823d55 100644
--- a/frontend/src/features/providers/components/ProviderFormDialog.vue
+++ b/frontend/src/features/providers/components/ProviderFormDialog.vue
@@ -357,6 +357,19 @@
/>
+
{{ legacyT('敏感信息保护') }}
@@ -481,6 +494,8 @@ const form = ref({
codex_fingerprint_convergence_enabled: false,
// Kiro 专属配置
kiro_simulated_cache_enabled: false,
+ // Responses WebSocket 配置
+ responses_websocket_enabled: false,
})
// 重置表单
@@ -513,6 +528,8 @@ function resetForm() {
codex_fingerprint_convergence_enabled: false,
// Kiro 专属配置
kiro_simulated_cache_enabled: false,
+ // Responses WebSocket 配置
+ responses_websocket_enabled: false,
}
}
@@ -549,6 +566,8 @@ function loadProviderData() {
codex_fingerprint_convergence_enabled: props.provider.codex_fingerprint_convergence_enabled ?? false,
// Kiro 专属配置
kiro_simulated_cache_enabled: props.provider.kiro_simulated_cache_enabled ?? false,
+ // Responses WebSocket 配置
+ responses_websocket_enabled: props.provider.responses_websocket_enabled ?? false,
}
}
@@ -608,6 +627,7 @@ const handleSubmit = async () => {
quota_last_reset_at: quotaLastResetAt,
quota_expires_at: quotaExpiresAt,
keep_priority_on_conversion: form.value.keep_priority_on_conversion,
+ responses_websocket_enabled: form.value.responses_websocket_enabled,
is_active: form.value.is_active,
// 请求配置
max_retries: form.value.max_retries ?? undefined,
diff --git a/frontend/src/features/providers/components/__tests__/ProviderFormDialog.responses-websocket.spec.ts b/frontend/src/features/providers/components/__tests__/ProviderFormDialog.responses-websocket.spec.ts
new file mode 100644
index 000000000..1e38a7543
--- /dev/null
+++ b/frontend/src/features/providers/components/__tests__/ProviderFormDialog.responses-websocket.spec.ts
@@ -0,0 +1,18 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+function readSource(path: string): string {
+ return readFileSync(resolve(process.cwd(), path), 'utf8')
+}
+
+describe('ProviderFormDialog Responses WebSocket switch', () => {
+ it('displays and submits the switch for every provider type', () => {
+ const source = readSource('src/features/providers/components/ProviderFormDialog.vue')
+
+ expect(source).toContain('Responses WebSocket 模式')
+ expect(source).toContain('responses_websocket_enabled')
+ expect(source).toContain('responses_websocket_enabled: form.value.responses_websocket_enabled')
+ expect(source).not.toContain("v-if=\"form.provider_type === 'codex'\"")
+ })
+})
diff --git a/frontend/src/features/usage/components/UsageRecordsTable.vue b/frontend/src/features/usage/components/UsageRecordsTable.vue
index ea849110a..d07b283b2 100644
--- a/frontend/src/features/usage/components/UsageRecordsTable.vue
+++ b/frontend/src/features/usage/components/UsageRecordsTable.vue
@@ -277,6 +277,14 @@
>
取消
+
+ WS
+
已取消
+
+ WS
+
{
expect(root.textContent).toContain('gpt-5')
})
+ it('shows WS in the type column for completed WebSocket usage records', () => {
+ const root = mountUsageRecordsTable([buildRecord({
+ is_websocket: true,
+ status: 'completed',
+ })])
+
+ const badges = [...root.querySelectorAll('[data-usage-transport="websocket"]')]
+ expect(badges.length).toBeGreaterThan(0)
+ expect(badges.every(badge => badge.textContent?.trim() === 'WS')).toBe(true)
+ })
+
it('shows reasoning effort next to the model name', () => {
const root = mountUsageRecordsTable([buildRecord({
requested_reasoning_effort: 'xhigh',
diff --git a/frontend/src/features/usage/composables/useUsageData.ts b/frontend/src/features/usage/composables/useUsageData.ts
index 5103efd71..2d68c340f 100644
--- a/frontend/src/features/usage/composables/useUsageData.ts
+++ b/frontend/src/features/usage/composables/useUsageData.ts
@@ -647,6 +647,7 @@ export function useUsageData(options: UseUsageDataOptions) {
? (record.image_progress ?? existing.image_progress)
: existing.image_progress,
is_stream: upstreamIsStream,
+ is_websocket: mergeBooleanTrueWins(existing.is_websocket, record.is_websocket),
upstream_is_stream: upstreamIsStream,
client_requested_stream: clientRequestedStream,
client_is_stream: clientIsStream,
diff --git a/frontend/src/features/usage/types.ts b/frontend/src/features/usage/types.ts
index cb7433876..c13d0176d 100644
--- a/frontend/src/features/usage/types.ts
+++ b/frontend/src/features/usage/types.ts
@@ -120,6 +120,7 @@ export interface UsageRecord {
end_to_end_time_ms?: number | null // 客户端从请求进入网关到完成的总耗时
end_to_end_first_byte_time_ms?: number | null // 客户端从请求进入网关到首字节的耗时
is_stream: boolean
+ is_websocket?: boolean
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
diff --git a/frontend/src/features/usage/utils/__tests__/status.spec.ts b/frontend/src/features/usage/utils/__tests__/status.spec.ts
index 93cf8e886..a94629b33 100644
--- a/frontend/src/features/usage/utils/__tests__/status.spec.ts
+++ b/frontend/src/features/usage/utils/__tests__/status.spec.ts
@@ -6,6 +6,7 @@ import {
hasUsageRetry,
isUsageRecordFailed,
isUsageRecordSuccessful,
+ isUsageWebSocket,
mapRequestStatusToTimelineStatus,
normalizeRequestStatus,
resolveDisplayRequestStatus,
@@ -177,6 +178,12 @@ describe('usage status helpers', () => {
expect(hasUsageRetry(buildUsageRecord({ has_retry: undefined }))).toBe(false)
})
+ it('recognizes persisted WebSocket usage records', () => {
+ expect(isUsageWebSocket(buildUsageRecord({ is_websocket: true }))).toBe(true)
+ expect(isUsageWebSocket(buildUsageRecord({ is_websocket: false }))).toBe(false)
+ expect(isUsageWebSocket(buildUsageRecord({ is_websocket: undefined }))).toBe(false)
+ })
+
it('prefers symmetric stream aliases when present', () => {
expect(formatUsageStreamLabel(buildUsageRecord({
is_stream: true,
diff --git a/frontend/src/features/usage/utils/status.ts b/frontend/src/features/usage/utils/status.ts
index 9ed987fc2..e93f2b948 100644
--- a/frontend/src/features/usage/utils/status.ts
+++ b/frontend/src/features/usage/utils/status.ts
@@ -49,6 +49,12 @@ export function hasUsageRetry(
return record.has_retry === true
}
+export function isUsageWebSocket(
+ record: Pick
+): boolean {
+ return record.is_websocket === true
+}
+
export function resolveUsageStreamModes(
record: Pick<
UsageRecord,
diff --git a/frontend/src/views/shared/Usage.vue b/frontend/src/views/shared/Usage.vue
index 70a64ae20..92e313732 100644
--- a/frontend/src/views/shared/Usage.vue
+++ b/frontend/src/views/shared/Usage.vue
@@ -574,6 +574,9 @@ async function pollActiveRequests() {
record.is_stream = update.is_stream
record.upstream_is_stream = update.is_stream
}
+ if (typeof update.is_websocket === 'boolean') {
+ record.is_websocket = record.is_websocket === true || update.is_websocket
+ }
if (typeof update.client_is_stream === 'boolean') {
record.client_is_stream = update.client_is_stream
record.client_requested_stream = update.client_is_stream