mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(frontend): Responses WebSocket 配置与用量展示
provider 表单支持开启 Responses WebSocket;用量列表、状态与详情 区分 WebSocket 请求。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -357,6 +357,19 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">{{ legacyT('Responses WebSocket 模式') }}</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
{{ legacyT('允许此提供商处理标准 Responses API WebSocket 请求。仅在已验证兼容性后启用。') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="form.responses_websocket_enabled"
|
||||
@update:model-value="(v: boolean) => form.responses_websocket_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">{{ legacyT('敏感信息保护') }}</span>
|
||||
@@ -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,
|
||||
|
||||
+18
@@ -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'\"")
|
||||
})
|
||||
})
|
||||
@@ -277,6 +277,14 @@
|
||||
>
|
||||
取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="isUsageWebSocket(record)"
|
||||
variant="outline"
|
||||
data-usage-transport="websocket"
|
||||
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"
|
||||
>
|
||||
WS
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
:variant="streamBadgeVariant(getStreamModeSegments(record).client === '流式')"
|
||||
@@ -831,6 +839,14 @@
|
||||
>
|
||||
已取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="isUsageWebSocket(record)"
|
||||
variant="outline"
|
||||
data-usage-transport="websocket"
|
||||
class="whitespace-nowrap border-sky-500/50 text-sky-600 dark:text-sky-400"
|
||||
>
|
||||
WS
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
:variant="streamBadgeVariant(getStreamModeSegments(record).client === '流式')"
|
||||
@@ -1022,6 +1038,7 @@ import {
|
||||
formatUsageStreamLabel,
|
||||
isUsageRecordFailed,
|
||||
isUsageUpstreamStream,
|
||||
isUsageWebSocket,
|
||||
resolveDisplayRequestStatus,
|
||||
resolveUsageStreamLabelSegments
|
||||
} from '../utils/status'
|
||||
|
||||
@@ -342,6 +342,17 @@ describe('UsageRecordsTable', () => {
|
||||
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<HTMLElement>('[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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -49,6 +49,12 @@ export function hasUsageRetry(
|
||||
return record.has_retry === true
|
||||
}
|
||||
|
||||
export function isUsageWebSocket(
|
||||
record: Pick<UsageRecord, 'is_websocket'>
|
||||
): boolean {
|
||||
return record.is_websocket === true
|
||||
}
|
||||
|
||||
export function resolveUsageStreamModes(
|
||||
record: Pick<
|
||||
UsageRecord,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user