From 956f4672e482bcc42563049741a1b83ce9ad242e Mon Sep 17 00:00:00 2001 From: xlplbo Date: Sat, 12 Sep 2026 16:13:16 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(openai-ws):=20=E5=AF=B9=E9=BD=90=20Open?= =?UTF-8?q?AI=20=E6=96=B0=E7=89=88=E4=B8=8A=E4=B8=8B=E6=96=87=E6=B1=A0?= =?UTF-8?q?=E5=AE=B9=E9=87=8F=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/config/config.go | 4 +- backend/internal/service/openai_ws_pool.go | 10 +- .../internal/service/openai_ws_pool_test.go | 98 ++++++++++++++++--- deploy/config.example.yaml | 2 + .../src/i18n/locales/en/admin/accounts.ts | 2 +- .../src/i18n/locales/zh/admin/accounts.ts | 2 +- 6 files changed, 95 insertions(+), 23 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 6c4b3748cf..a59ae1455e 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1274,7 +1274,9 @@ type GatewayOpenAIWSConfig struct { MaxConnsPerAccount int `mapstructure:"max_conns_per_account"` MinIdlePerAccount int `mapstructure:"min_idle_per_account"` MaxIdlePerAccount int `mapstructure:"max_idle_per_account"` - // DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限 + // DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限。 + // 旧版及 mode_router_v2 的 ctx_pool 共用此开关和类型系数;关闭后使用 max_conns_per_account。 + // mode_router_v2 下并发数 <= 0 的账号仍不可调度。 DynamicMaxConnsByAccountConcurrencyEnabled bool `mapstructure:"dynamic_max_conns_by_account_concurrency_enabled"` // OAuthMaxConnsFactor: OAuth 账号连接池系数(effective=ceil(concurrency*factor)) OAuthMaxConnsFactor float64 `mapstructure:"oauth_max_conns_factor"` diff --git a/backend/internal/service/openai_ws_pool.go b/backend/internal/service/openai_ws_pool.go index ed86a36284..ad0e01852c 100644 --- a/backend/internal/service/openai_ws_pool.go +++ b/backend/internal/service/openai_ws_pool.go @@ -2156,14 +2156,8 @@ func (p *openAIWSConnPool) effectiveMaxConnsByAccount(account *Account) int { if hardCap <= 0 { return 0 } - if p.modeRouterV2Enabled() { - if account == nil { - return hardCap - } - if account.Concurrency <= 0 { - return 0 - } - return min(account.Concurrency, hardCap) + if p.modeRouterV2Enabled() && account != nil && account.Concurrency <= 0 { + return 0 } if account == nil || !p.dynamicMaxConnsEnabled() { return hardCap diff --git a/backend/internal/service/openai_ws_pool_test.go b/backend/internal/service/openai_ws_pool_test.go index 1a4480dccb..25c5f0946c 100644 --- a/backend/internal/service/openai_ws_pool_test.go +++ b/backend/internal/service/openai_ws_pool_test.go @@ -1164,21 +1164,95 @@ func TestOpenAIWSConnPool_EffectiveMaxConnsDisabledFallbackHardCap(t *testing.T) require.Equal(t, 8, pool.effectiveMaxConnsByAccount(account), "关闭动态模式后应保持旧行为") } -func TestOpenAIWSConnPool_EffectiveMaxConnsByAccount_ModeRouterV2RespectsHardCap(t *testing.T) { - cfg := &config.Config{} - cfg.Gateway.OpenAIWS.ModeRouterV2Enabled = true - cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 8 - cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = true - cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor = 0.3 - cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor = 0.6 +func TestOpenAIWSConnPool_EffectiveMaxConnsByAccount_ModeRouterV2(t *testing.T) { + tests := []struct { + name string + accountType string + concurrency int + factor float64 + dynamic bool + want int + }{ + {name: "oauth expansion", accountType: AccountTypeOAuth, concurrency: 1, factor: 5, dynamic: true, want: 5}, + {name: "apikey expansion", accountType: AccountTypeAPIKey, concurrency: 1, factor: 5, dynamic: true, want: 5}, + {name: "oauth fraction", accountType: AccountTypeOAuth, concurrency: 20, factor: 0.3, dynamic: true, want: 6}, + {name: "apikey rounding", accountType: AccountTypeAPIKey, concurrency: 3, factor: 0.6, dynamic: true, want: 2}, + {name: "minimum one", accountType: AccountTypeOAuth, concurrency: 1, factor: 0.3, dynamic: true, want: 1}, + {name: "hard cap", accountType: AccountTypeOAuth, concurrency: 3, factor: 5, dynamic: true, want: 8}, + {name: "dynamic disabled", accountType: AccountTypeAPIKey, concurrency: 2, factor: 0.6, want: 8}, + {name: "zero concurrency", accountType: AccountTypeOAuth, factor: 5, dynamic: true, want: 0}, + {name: "negative concurrency", accountType: AccountTypeAPIKey, concurrency: -1, factor: 5, dynamic: true, want: 0}, + {name: "zero concurrency dynamic disabled", accountType: AccountTypeOAuth, factor: 5, want: 0}, + {name: "negative concurrency dynamic disabled", accountType: AccountTypeAPIKey, concurrency: -1, factor: 5, want: 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.Config{} + cfg.Gateway.OpenAIWS.ModeRouterV2Enabled = true + cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 8 + cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = tt.dynamic + cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor = 1 + cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor = 1 + if tt.accountType == AccountTypeOAuth { + cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor = tt.factor + } else { + cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor = tt.factor + } + pool := newOpenAIWSConnPool(cfg) + t.Cleanup(pool.Close) + account := &Account{Platform: PlatformOpenAI, Type: tt.accountType, Concurrency: tt.concurrency} + require.Equal(t, tt.want, pool.effectiveMaxConnsByAccount(account)) + require.Equal(t, 8, pool.effectiveMaxConnsByAccount(nil)) + }) + } +} - pool := newOpenAIWSConnPool(cfg) +func TestOpenAIWSConnPool_AcquireRetainedSessionsUsesScaledCapacity(t *testing.T) { + for _, mode := range []struct { + name string + v2 bool + }{ + {name: "legacy"}, + {name: "mode router v2", v2: true}, + } { + t.Run(mode.name, func(t *testing.T) { + cfg := &config.Config{} + cfg.Gateway.OpenAIWS.ModeRouterV2Enabled = mode.v2 + cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 3 + cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 3 + cfg.Gateway.OpenAIWS.PoolTargetUtilization = 1 + cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = true + cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor = 2 + pool := newOpenAIWSConnPool(cfg) + pool.setClientDialerForTest(&openAIWSFakeDialer{}) + t.Cleanup(pool.Close) + req := openAIWSAcquireRequest{ + Account: &Account{ID: 902, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 2}, + WSURL: "wss://example.com/v1/responses", + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + leases := make([]*openAIWSConnLease, 0, 3) + for range 3 { + lease, err := pool.Acquire(ctx, req) + require.NoError(t, err, "轮次之间仍持有连接的会话应可使用系数扩出的容量") + t.Cleanup(lease.Release) + leases = append(leases, lease) + } - high := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 20} - require.Equal(t, 8, pool.effectiveMaxConnsByAccount(high), "v2 路径也必须受连接池硬上限约束") + fullCtx, fullCancel := context.WithTimeout(context.Background(), 30*time.Millisecond) + defer fullCancel() + _, err := pool.Acquire(fullCtx, req) + require.ErrorIs(t, err, context.DeadlineExceeded, "扩容后仍必须受全局硬上限约束") - nonPositive := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Concurrency: 0} - require.Equal(t, 0, pool.effectiveMaxConnsByAccount(nonPositive), "并发数<=0 时应不可调度") + leases[0].Release() + reused, err := pool.Acquire(ctx, req) + require.NoError(t, err) + defer reused.Release() + require.Equal(t, leases[0].ConnID(), reused.ConnID()) + require.True(t, reused.Reused()) + }) + } } func TestOpenAIWSConnPool_AcquireRejectsWhenEffectiveMaxConnsIsZero(t *testing.T) { diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index c5adc99d93..66bd3842d5 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -357,6 +357,8 @@ gateway: max_idle_per_account: 12 # 是否按账号并发动态计算连接池上限: # effective_max_conns = min(max_conns_per_account, ceil(account.concurrency * factor)) + # 旧版及 mode_router_v2 的 ctx_pool 共用此开关和下方类型系数;关闭后使用 max_conns_per_account。 + # mode_router_v2 下并发数 <= 0 的账号仍不可调度。 dynamic_max_conns_by_account_concurrency_enabled: true # 按账号类型分别设置系数(OAuth / API Key) oauth_max_conns_factor: 1.0 diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 0c122113f0..c614caa0cc 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -613,7 +613,7 @@ export default { wsModeShared: 'Shared (shared)', wsModeDedicated: 'Dedicated (dedicated)', wsModeConcurrencyHint: - 'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.', + 'Account concurrency limits requests in flight; the context pool connection limit is controlled by the gateway connection pool settings.', wsModePassthroughHint: 'Passthrough mode does not use the WS connection pool.', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index c19ad3653b..0cbb56341b 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -698,7 +698,7 @@ export default { wsModeHttpBridge: 'HTTP 桥接(http_bridge)', wsModeShared: '共享(shared)', wsModeDedicated: '独享(dedicated)', - wsModeConcurrencyHint: '启用 WS mode 后,该账号并发数将作为该账号 WS 连接池上限。', + wsModeConcurrencyHint: '账号并发数限制同时执行的请求数;上下文池的连接上限由网关连接池配置决定。', wsModePassthroughHint: 'passthrough 模式不使用 WS 连接池。', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: From cf3aca6e3b76d7b9d9b680e8ae59a9268a67e6b7 Mon Sep 17 00:00:00 2001 From: xlplbo Date: Sat, 12 Sep 2026 16:28:04 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(ui):=20=E6=8C=89=20OpenAI=20WS=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=98=BE=E7=A4=BA=E9=93=BE=E8=B7=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/BulkEditAccountModal.vue | 18 ++++++++--------- .../components/account/CreateAccountModal.vue | 10 +++++----- .../components/account/EditAccountModal.vue | 10 +++++----- .../src/i18n/locales/en/admin/accounts.ts | 7 ++++--- .../src/i18n/locales/zh/admin/accounts.ts | 5 +++-- .../src/utils/__tests__/openaiWsMode.spec.ts | 18 ++++++++--------- frontend/src/utils/openaiWsMode.ts | 20 ++++++++++++++----- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 6e751df816..f66fb85fda 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -873,8 +873,8 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t(openAIWSModeConcurrencyHintKey) }} +

+ {{ t(openAIWSModeHintKey) }}

- resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value) +const openAIWSModeHintKey = computed(() => + resolveOpenAIWSModeHintKey(openaiOAuthResponsesWebSocketV2Mode.value) ) -const openAIAPIKeyWSModeConcurrencyHintKey = computed(() => - resolveOpenAIWSModeConcurrencyHintKey(openaiAPIKeyResponsesWebSocketV2Mode.value) +const openAIAPIKeyWSModeHintKey = computed(() => + resolveOpenAIWSModeHintKey(openaiAPIKeyResponsesWebSocketV2Mode.value) ) // Model mapping helpers diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 8e2ca7dcb7..54a6b2331f 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -3132,8 +3132,8 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t(openAIWSModeConcurrencyHintKey) }} +

+ {{ t(openAIWSModeHintKey) }}

@@ -3975,7 +3975,7 @@ import { OPENAI_WS_MODE_PASSTHROUGH, OPENAI_WS_MODE_HTTP_BRIDGE, isOpenAIWSModeEnabled, - resolveOpenAIWSModeConcurrencyHintKey, + resolveOpenAIWSModeHintKey, type OpenAIWSMode } from '@/utils/openaiWsMode' import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue' @@ -4653,8 +4653,8 @@ const openaiResponsesWebSocketV2Mode = computed({ } }) -const openAIWSModeConcurrencyHintKey = computed(() => - resolveOpenAIWSModeConcurrencyHintKey(openaiResponsesWebSocketV2Mode.value) +const openAIWSModeHintKey = computed(() => + resolveOpenAIWSModeHintKey(openaiResponsesWebSocketV2Mode.value) ) const isOpenAIModelRestrictionDisabled = computed(() => diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index dad0bfc9f4..0d3c235bea 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1871,8 +1871,8 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t(openAIWSModeConcurrencyHintKey) }} +

+ {{ t(openAIWSModeHintKey) }}

@@ -3101,7 +3101,7 @@ import { OPENAI_WS_MODE_PASSTHROUGH, OPENAI_WS_MODE_HTTP_BRIDGE, isOpenAIWSModeEnabled, - resolveOpenAIWSModeConcurrencyHintKey, + resolveOpenAIWSModeHintKey, type OpenAIWSMode, resolveOpenAIWSModeFromExtra } from '@/utils/openaiWsMode' @@ -3588,8 +3588,8 @@ const openaiResponsesWebSocketV2Mode = computed({ openaiOAuthResponsesWebSocketV2Mode.value = mode } }) -const openAIWSModeConcurrencyHintKey = computed(() => - resolveOpenAIWSModeConcurrencyHintKey(openaiResponsesWebSocketV2Mode.value) +const openAIWSModeHintKey = computed(() => + resolveOpenAIWSModeHintKey(openaiResponsesWebSocketV2Mode.value) ) const codexImageToolOptions = computed { expect(isOpenAIWSModeEnabled(OPENAI_WS_MODE_HTTP_BRIDGE)).toBe(true) }) - it('resolves concurrency hint key by mode', () => { - expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_OFF)).toBe( - 'admin.accounts.openai.wsModeConcurrencyHint' + it('hides the off hint and resolves each active mode separately', () => { + expect(resolveOpenAIWSModeHintKey(OPENAI_WS_MODE_OFF)).toBeNull() + expect(resolveOpenAIWSModeHintKey(OPENAI_WS_MODE_CTX_POOL)).toBe( + 'admin.accounts.openai.wsModeCtxPoolHint' ) - expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_CTX_POOL)).toBe( - 'admin.accounts.openai.wsModeConcurrencyHint' - ) - expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_PASSTHROUGH)).toBe( + expect(resolveOpenAIWSModeHintKey(OPENAI_WS_MODE_PASSTHROUGH)).toBe( 'admin.accounts.openai.wsModePassthroughHint' ) - expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_HTTP_BRIDGE)).toBe( - 'admin.accounts.openai.wsModePassthroughHint' + expect(resolveOpenAIWSModeHintKey(OPENAI_WS_MODE_HTTP_BRIDGE)).toBe( + 'admin.accounts.openai.wsModeHttpBridgeHint' ) }) }) diff --git a/frontend/src/utils/openaiWsMode.ts b/frontend/src/utils/openaiWsMode.ts index 77872bc656..b806746731 100644 --- a/frontend/src/utils/openaiWsMode.ts +++ b/frontend/src/utils/openaiWsMode.ts @@ -44,13 +44,23 @@ export const isOpenAIWSModeEnabled = (mode: OpenAIWSMode): boolean => { return mode !== OPENAI_WS_MODE_OFF } -export const resolveOpenAIWSModeConcurrencyHintKey = ( +export const resolveOpenAIWSModeHintKey = ( mode: OpenAIWSMode -): 'admin.accounts.openai.wsModeConcurrencyHint' | 'admin.accounts.openai.wsModePassthroughHint' => { - if (mode === OPENAI_WS_MODE_PASSTHROUGH || mode === OPENAI_WS_MODE_HTTP_BRIDGE) { - return 'admin.accounts.openai.wsModePassthroughHint' +): + | 'admin.accounts.openai.wsModeCtxPoolHint' + | 'admin.accounts.openai.wsModePassthroughHint' + | 'admin.accounts.openai.wsModeHttpBridgeHint' + | null => { + switch (mode) { + case OPENAI_WS_MODE_CTX_POOL: + return 'admin.accounts.openai.wsModeCtxPoolHint' + case OPENAI_WS_MODE_PASSTHROUGH: + return 'admin.accounts.openai.wsModePassthroughHint' + case OPENAI_WS_MODE_HTTP_BRIDGE: + return 'admin.accounts.openai.wsModeHttpBridgeHint' + default: + return null } - return 'admin.accounts.openai.wsModeConcurrencyHint' } export const resolveOpenAIWSModeFromExtra = ( From 5d5b395d0069ce752922ea65e730f7a2c52a421b Mon Sep 17 00:00:00 2001 From: xlplbo Date: Sat, 12 Sep 2026 16:32:17 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs(ui):=20=E5=AE=8C=E5=96=84=20OpenAI=20W?= =?UTF-8?q?S=20=E6=A8=A1=E5=BC=8F=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/i18n/locales/en/admin/accounts.ts | 8 +++++--- frontend/src/i18n/locales/zh/admin/accounts.ts | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 35768e544c..67d4f2ba74 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -613,9 +613,11 @@ export default { wsModeShared: 'Shared (shared)', wsModeDedicated: 'Dedicated (dedicated)', wsModeCtxPoolHint: - 'Gateway → acquire upstream WS connections from the pool; capacity is determined by configuration.', - wsModePassthroughHint: 'Gateway → upstream WS, without a connection pool.', - wsModeHttpBridgeHint: 'Gateway conversion → upstream HTTP/SSE.', + 'The gateway gets upstream WebSocket connections from a pool, reusing existing connections when possible. Gateway configuration sets the maximum number of connections in the pool.', + wsModePassthroughHint: + 'The gateway opens a separate upstream WebSocket connection for each client session and relays messages in both directions, without using a connection pool.', + wsModeHttpBridgeHint: + 'The client connects to the gateway over WebSocket. The gateway converts requests to upstream HTTP requests, then converts SSE streaming responses back into WebSocket messages for the client.', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 88bb232b45..5910bb80c0 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -698,9 +698,9 @@ export default { wsModeHttpBridge: 'HTTP 桥接(http_bridge)', wsModeShared: '共享(shared)', wsModeDedicated: '独享(dedicated)', - wsModeCtxPoolHint: '网关 → 从连接池获取上游 WS 连接,数量由配置决定。', - wsModePassthroughHint: '网关 → 上游 WS,不使用连接池。', - wsModeHttpBridgeHint: '网关转换 → 上游 HTTP/SSE。', + wsModeCtxPoolHint: '网关从连接池中获取上游 WebSocket 连接,优先复用已有连接。连接池最多能建立多少条连接,由网关配置决定。', + wsModePassthroughHint: '网关为每个客户端会话单独建立上游 WebSocket 连接,转发双方的消息,不使用连接池。', + wsModeHttpBridgeHint: '客户端通过 WebSocket 连接网关,网关将请求转换为上游 HTTP 请求,再把上游的 SSE 流式响应转换成 WebSocket 消息返回给客户端。', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: '仅对 OpenAI OAuth 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', From db15ddf07fd70afe5991dc721ce2968d07bb2b97 Mon Sep 17 00:00:00 2001 From: xlplbo Date: Sat, 12 Sep 2026 16:54:22 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs(ui):=20=E5=8C=BA=E5=88=86=20OpenAI=20W?= =?UTF-8?q?S=20=E5=BC=80=E5=85=B3=E4=B8=8E=E8=BF=9E=E6=8E=A5=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/account/BulkEditAccountModal.vue | 8 ++++++++ frontend/src/components/account/CreateAccountModal.vue | 4 ++++ frontend/src/components/account/EditAccountModal.vue | 4 ++++ frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts | 8 ++++---- frontend/src/i18n/locales/en/admin/accounts.ts | 5 ++++- frontend/src/i18n/locales/zh/admin/accounts.ts | 5 ++++- 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index f66fb85fda..fefa84abf2 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -873,7 +873,11 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

+

+ {{ t('admin.accounts.openai.wsModeRoutingDesc') }} +

+ {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

{{ t('admin.accounts.openai.wsModeDesc') }}

+

+ {{ t('admin.accounts.openai.wsModeRoutingDesc') }} +

+ {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 0d3c235bea..6fe902e51e 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1871,7 +1871,11 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

+

+ {{ t('admin.accounts.openai.wsModeRoutingDesc') }} +

+ {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

diff --git a/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts b/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts index 43075e9d80..0f3a24f0ca 100644 --- a/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts +++ b/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts @@ -5,9 +5,9 @@ import zh from '../locales/zh/admin/accounts' describe('OpenAI WS mode locale descriptions', () => { it('documents the global v2 router requirement for account WS modes', () => { - expect(zh.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled') - expect(zh.accounts.openai.wsModeDesc).toContain('http_bridge') - expect(en.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled') - expect(en.accounts.openai.wsModeDesc).toContain('http_bridge') + expect(zh.accounts.openai.wsModeRoutingDesc).toContain('mode_router_v2_enabled') + expect(zh.accounts.openai.wsModeRoutingDesc).toContain('http_bridge') + expect(en.accounts.openai.wsModeRoutingDesc).toContain('mode_router_v2_enabled') + expect(en.accounts.openai.wsModeRoutingDesc).toContain('http_bridge') }) }) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 67d4f2ba74..c1321d56d0 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -605,7 +605,10 @@ export default { 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', wsMode: 'WS mode', wsModeDesc: - 'Only applies to the current OpenAI account type; account WS modes, including http_bridge, take effect only when the global gateway.openai_ws.mode_router_v2_enabled=true.', + 'Account WS switch: select Off to disable WS for this account, or another option to enable it. Availability still depends on the other gateway WS switches.', + wsModeRoutingDesc: + 'Connection mode: when gateway.openai_ws.mode_router_v2_enabled=true, the selected mode applies, including http_bridge. When false (the default), all three modes use the legacy context pool whenever WS is allowed.', + wsModeHintPrefix: 'Connection behavior when the selected mode takes effect:', wsModeOff: 'Off (off)', wsModeCtxPool: 'Context Pool (ctx_pool)', wsModePassthrough: 'Passthrough (passthrough)', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 5910bb80c0..9753397c41 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -691,7 +691,10 @@ export default { '默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。', wsMode: 'WS mode', wsModeDesc: - '仅对当前 OpenAI 账号类型生效;包括 http_bridge 在内的账号 WS mode 仅在全局 gateway.openai_ws.mode_router_v2_enabled=true 时生效。', + '账号 WS 开关:选择“关闭”可禁用本账号的 WS;选择其他选项则启用,实际是否可用仍受网关其他 WS 开关限制。', + wsModeRoutingDesc: + '连接方式:全局 gateway.openai_ws.mode_router_v2_enabled=true 时,按所选模式(包括 http_bridge)连接;为 false(默认)时,不区分这三种模式,允许 WS 时统一使用旧版上下文池。', + wsModeHintPrefix: '所选模式生效后的连接方式:', wsModeOff: '关闭(off)', wsModeCtxPool: '上下文池(ctx_pool)', wsModePassthrough: '透传(passthrough)', From 76b3f3c7cf1d3fd302310b5ae0aaeaf38d3ed107 Mon Sep 17 00:00:00 2001 From: xlplbo Date: Sat, 12 Sep 2026 17:38:55 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs(ui):=20=E7=B2=BE=E7=AE=80=20OpenAI=20W?= =?UTF-8?q?S=20=E6=A8=A1=E5=BC=8F=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/account/BulkEditAccountModal.vue | 8 -------- .../src/components/account/CreateAccountModal.vue | 4 ---- frontend/src/components/account/EditAccountModal.vue | 4 ---- frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts | 6 ++---- frontend/src/i18n/locales/en/admin/accounts.ts | 11 ++++------- frontend/src/i18n/locales/zh/admin/accounts.ts | 11 ++++------- 6 files changed, 10 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index fefa84abf2..f66fb85fda 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -873,11 +873,7 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t('admin.accounts.openai.wsModeRoutingDesc') }} -

- {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t('admin.accounts.openai.wsModeRoutingDesc') }} -

- {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 6fe902e51e..0d3c235bea 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1871,11 +1871,7 @@

{{ t('admin.accounts.openai.wsModeDesc') }}

-

- {{ t('admin.accounts.openai.wsModeRoutingDesc') }} -

- {{ t('admin.accounts.openai.wsModeHintPrefix') }} {{ t(openAIWSModeHintKey) }}

diff --git a/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts b/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts index 0f3a24f0ca..63ae32c42e 100644 --- a/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts +++ b/frontend/src/i18n/__tests__/wsModeLocaleDesc.spec.ts @@ -5,9 +5,7 @@ import zh from '../locales/zh/admin/accounts' describe('OpenAI WS mode locale descriptions', () => { it('documents the global v2 router requirement for account WS modes', () => { - expect(zh.accounts.openai.wsModeRoutingDesc).toContain('mode_router_v2_enabled') - expect(zh.accounts.openai.wsModeRoutingDesc).toContain('http_bridge') - expect(en.accounts.openai.wsModeRoutingDesc).toContain('mode_router_v2_enabled') - expect(en.accounts.openai.wsModeRoutingDesc).toContain('http_bridge') + expect(zh.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled=true') + expect(en.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled=true') }) }) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index c1321d56d0..8c675a1c31 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -605,10 +605,7 @@ export default { 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', wsMode: 'WS mode', wsModeDesc: - 'Account WS switch: select Off to disable WS for this account, or another option to enable it. Availability still depends on the other gateway WS switches.', - wsModeRoutingDesc: - 'Connection mode: when gateway.openai_ws.mode_router_v2_enabled=true, the selected mode applies, including http_bridge. When false (the default), all three modes use the legacy context pool whenever WS is allowed.', - wsModeHintPrefix: 'Connection behavior when the selected mode takes effect:', + 'Applies only to the current OpenAI account type. Select Off to disable WS. Other modes use the selected connection method only when gateway.openai_ws.mode_router_v2_enabled=true; otherwise, they use the context pool.', wsModeOff: 'Off (off)', wsModeCtxPool: 'Context Pool (ctx_pool)', wsModePassthrough: 'Passthrough (passthrough)', @@ -616,11 +613,11 @@ export default { wsModeShared: 'Shared (shared)', wsModeDedicated: 'Dedicated (dedicated)', wsModeCtxPoolHint: - 'The gateway gets upstream WebSocket connections from a pool, reusing existing connections when possible. Gateway configuration sets the maximum number of connections in the pool.', + 'The gateway gets and reuses upstream WS connections from a pool, with the pool limit determined by gateway configuration.', wsModePassthroughHint: - 'The gateway opens a separate upstream WebSocket connection for each client session and relays messages in both directions, without using a connection pool.', + 'The gateway opens a separate upstream WS connection for each client session, without using a connection pool.', wsModeHttpBridgeHint: - 'The client connects to the gateway over WebSocket. The gateway converts requests to upstream HTTP requests, then converts SSE streaming responses back into WebSocket messages for the client.', + 'The gateway converts client WS requests to upstream HTTP requests, then converts SSE streaming responses back into WS messages.', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 9753397c41..a93e548421 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -691,19 +691,16 @@ export default { '默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。', wsMode: 'WS mode', wsModeDesc: - '账号 WS 开关:选择“关闭”可禁用本账号的 WS;选择其他选项则启用,实际是否可用仍受网关其他 WS 开关限制。', - wsModeRoutingDesc: - '连接方式:全局 gateway.openai_ws.mode_router_v2_enabled=true 时,按所选模式(包括 http_bridge)连接;为 false(默认)时,不区分这三种模式,允许 WS 时统一使用旧版上下文池。', - wsModeHintPrefix: '所选模式生效后的连接方式:', + '仅对当前 OpenAI 账号类型生效。选择“关闭”可禁用 WS;其余模式需全局 gateway.openai_ws.mode_router_v2_enabled=true 才按所选方式连接,未开启时统一使用上下文池。', wsModeOff: '关闭(off)', wsModeCtxPool: '上下文池(ctx_pool)', wsModePassthrough: '透传(passthrough)', wsModeHttpBridge: 'HTTP 桥接(http_bridge)', wsModeShared: '共享(shared)', wsModeDedicated: '独享(dedicated)', - wsModeCtxPoolHint: '网关从连接池中获取上游 WebSocket 连接,优先复用已有连接。连接池最多能建立多少条连接,由网关配置决定。', - wsModePassthroughHint: '网关为每个客户端会话单独建立上游 WebSocket 连接,转发双方的消息,不使用连接池。', - wsModeHttpBridgeHint: '客户端通过 WebSocket 连接网关,网关将请求转换为上游 HTTP 请求,再把上游的 SSE 流式响应转换成 WebSocket 消息返回给客户端。', + wsModeCtxPoolHint: '网关从连接池获取并复用上游 WS 连接,连接池上限由网关配置决定。', + wsModePassthroughHint: '网关为每个客户端会话单独建立上游 WS 连接,不使用连接池。', + wsModeHttpBridgeHint: '网关将客户端 WS 请求转换为上游 HTTP 请求,再将 SSE 流式响应转换为 WS 消息返回。', oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', oauthResponsesWebsocketsV2Desc: '仅对 OpenAI OAuth 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。',