openai_ws_protocol_resolver_test.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. package service
  2. import (
  3. "testing"
  4. "github.com/Wei-Shaw/sub2api/internal/config"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func TestOpenAIWSProtocolResolver_Resolve(t *testing.T) {
  8. baseCfg := &config.Config{}
  9. baseCfg.Gateway.OpenAIWS.Enabled = true
  10. baseCfg.Gateway.OpenAIWS.OAuthEnabled = true
  11. baseCfg.Gateway.OpenAIWS.APIKeyEnabled = true
  12. baseCfg.Gateway.OpenAIWS.ResponsesWebsockets = false
  13. baseCfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 = true
  14. openAIOAuthEnabled := &Account{
  15. Platform: PlatformOpenAI,
  16. Type: AccountTypeOAuth,
  17. Extra: map[string]any{
  18. "openai_oauth_responses_websockets_v2_enabled": true,
  19. },
  20. }
  21. t.Run("v2优先", func(t *testing.T) {
  22. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(openAIOAuthEnabled)
  23. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  24. require.Equal(t, "ws_v2_enabled", decision.Reason)
  25. })
  26. t.Run("OpenAI setup token复用OAuth开关", func(t *testing.T) {
  27. account := &Account{
  28. Platform: PlatformOpenAI,
  29. Type: AccountTypeSetupToken,
  30. Extra: map[string]any{
  31. "openai_oauth_responses_websockets_v2_enabled": true,
  32. },
  33. }
  34. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(account)
  35. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  36. require.Equal(t, "ws_v2_enabled", decision.Reason)
  37. })
  38. t.Run("非OpenAI setup token不进入OAuth WS", func(t *testing.T) {
  39. account := &Account{
  40. Platform: PlatformAnthropic,
  41. Type: AccountTypeSetupToken,
  42. Extra: map[string]any{
  43. "openai_oauth_responses_websockets_v2_enabled": true,
  44. },
  45. }
  46. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(account)
  47. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  48. require.Equal(t, "platform_not_openai", decision.Reason)
  49. })
  50. t.Run("v2关闭时回退v1", func(t *testing.T) {
  51. cfg := *baseCfg
  52. cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 = false
  53. cfg.Gateway.OpenAIWS.ResponsesWebsockets = true
  54. decision := NewOpenAIWSProtocolResolver(&cfg).Resolve(openAIOAuthEnabled)
  55. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocket, decision.Transport)
  56. require.Equal(t, "ws_v1_enabled", decision.Reason)
  57. })
  58. t.Run("透传开关不影响WS协议判定", func(t *testing.T) {
  59. account := *openAIOAuthEnabled
  60. account.Extra = map[string]any{
  61. "openai_oauth_responses_websockets_v2_enabled": true,
  62. "openai_passthrough": true,
  63. }
  64. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(&account)
  65. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  66. require.Equal(t, "ws_v2_enabled", decision.Reason)
  67. })
  68. t.Run("账号级强制HTTP", func(t *testing.T) {
  69. account := *openAIOAuthEnabled
  70. account.Extra = map[string]any{
  71. "openai_oauth_responses_websockets_v2_enabled": true,
  72. "openai_ws_force_http": true,
  73. }
  74. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(&account)
  75. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  76. require.Equal(t, "account_force_http", decision.Reason)
  77. })
  78. t.Run("全局强制HTTP无需启用mode router", func(t *testing.T) {
  79. cfg := *baseCfg
  80. cfg.Gateway.OpenAIWS.ForceHTTP = true
  81. decision := NewOpenAIWSProtocolResolver(&cfg).Resolve(openAIOAuthEnabled)
  82. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  83. require.Equal(t, "global_force_http", decision.Reason)
  84. })
  85. t.Run("全局关闭保持HTTP", func(t *testing.T) {
  86. cfg := *baseCfg
  87. cfg.Gateway.OpenAIWS.Enabled = false
  88. decision := NewOpenAIWSProtocolResolver(&cfg).Resolve(openAIOAuthEnabled)
  89. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  90. require.Equal(t, "global_disabled", decision.Reason)
  91. })
  92. t.Run("账号开关关闭保持HTTP", func(t *testing.T) {
  93. account := *openAIOAuthEnabled
  94. account.Extra = map[string]any{
  95. "openai_oauth_responses_websockets_v2_enabled": false,
  96. }
  97. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(&account)
  98. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  99. require.Equal(t, "account_disabled", decision.Reason)
  100. })
  101. t.Run("OAuth账号不会读取API Key专用开关", func(t *testing.T) {
  102. account := *openAIOAuthEnabled
  103. account.Extra = map[string]any{
  104. "openai_apikey_responses_websockets_v2_enabled": true,
  105. }
  106. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(&account)
  107. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  108. require.Equal(t, "account_disabled", decision.Reason)
  109. })
  110. t.Run("兼容旧键openai_ws_enabled", func(t *testing.T) {
  111. account := *openAIOAuthEnabled
  112. account.Extra = map[string]any{
  113. "openai_ws_enabled": true,
  114. }
  115. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(&account)
  116. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  117. require.Equal(t, "ws_v2_enabled", decision.Reason)
  118. })
  119. t.Run("按账号类型开关控制", func(t *testing.T) {
  120. cfg := *baseCfg
  121. cfg.Gateway.OpenAIWS.OAuthEnabled = false
  122. decision := NewOpenAIWSProtocolResolver(&cfg).Resolve(openAIOAuthEnabled)
  123. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  124. require.Equal(t, "oauth_disabled", decision.Reason)
  125. })
  126. t.Run("API Key 账号关闭开关时回退HTTP", func(t *testing.T) {
  127. cfg := *baseCfg
  128. cfg.Gateway.OpenAIWS.APIKeyEnabled = false
  129. account := &Account{
  130. Platform: PlatformOpenAI,
  131. Type: AccountTypeAPIKey,
  132. Extra: map[string]any{
  133. "openai_apikey_responses_websockets_v2_enabled": true,
  134. },
  135. }
  136. decision := NewOpenAIWSProtocolResolver(&cfg).Resolve(account)
  137. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  138. require.Equal(t, "apikey_disabled", decision.Reason)
  139. })
  140. t.Run("未知认证类型回退HTTP", func(t *testing.T) {
  141. account := &Account{
  142. Platform: PlatformOpenAI,
  143. Type: "unknown_type",
  144. Extra: map[string]any{
  145. "responses_websockets_v2_enabled": true,
  146. },
  147. }
  148. decision := NewOpenAIWSProtocolResolver(baseCfg).Resolve(account)
  149. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  150. require.Equal(t, "unknown_auth_type", decision.Reason)
  151. })
  152. }
  153. func TestOpenAIWSProtocolResolver_Resolve_ModeRouterV2(t *testing.T) {
  154. cfg := &config.Config{}
  155. cfg.Gateway.OpenAIWS.Enabled = true
  156. cfg.Gateway.OpenAIWS.OAuthEnabled = true
  157. cfg.Gateway.OpenAIWS.APIKeyEnabled = true
  158. cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 = true
  159. cfg.Gateway.OpenAIWS.ModeRouterV2Enabled = true
  160. cfg.Gateway.OpenAIWS.IngressModeDefault = OpenAIWSIngressModeCtxPool
  161. account := &Account{
  162. Platform: PlatformOpenAI,
  163. Type: AccountTypeOAuth,
  164. Concurrency: 1,
  165. Extra: map[string]any{
  166. "openai_oauth_responses_websockets_v2_mode": OpenAIWSIngressModeCtxPool,
  167. },
  168. }
  169. t.Run("ctx_pool mode routes to ws v2", func(t *testing.T) {
  170. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(account)
  171. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  172. require.Equal(t, "ws_v2_mode_ctx_pool", decision.Reason)
  173. })
  174. t.Run("off mode routes to http", func(t *testing.T) {
  175. offAccount := &Account{
  176. Platform: PlatformOpenAI,
  177. Type: AccountTypeOAuth,
  178. Concurrency: 1,
  179. Extra: map[string]any{
  180. "openai_oauth_responses_websockets_v2_mode": OpenAIWSIngressModeOff,
  181. },
  182. }
  183. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(offAccount)
  184. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  185. require.Equal(t, "account_mode_off", decision.Reason)
  186. })
  187. t.Run("legacy boolean maps to ctx_pool in v2 router", func(t *testing.T) {
  188. legacyAccount := &Account{
  189. Platform: PlatformOpenAI,
  190. Type: AccountTypeAPIKey,
  191. Concurrency: 1,
  192. Extra: map[string]any{
  193. "openai_apikey_responses_websockets_v2_enabled": true,
  194. },
  195. }
  196. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(legacyAccount)
  197. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  198. require.Equal(t, "ws_v2_mode_ctx_pool", decision.Reason)
  199. })
  200. t.Run("passthrough mode routes to ws v2", func(t *testing.T) {
  201. passthroughAccount := &Account{
  202. Platform: PlatformOpenAI,
  203. Type: AccountTypeOAuth,
  204. Concurrency: 1,
  205. Extra: map[string]any{
  206. "openai_oauth_responses_websockets_v2_mode": OpenAIWSIngressModePassthrough,
  207. },
  208. }
  209. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(passthroughAccount)
  210. require.Equal(t, OpenAIUpstreamTransportResponsesWebsocketV2, decision.Transport)
  211. require.Equal(t, "ws_v2_mode_passthrough", decision.Reason)
  212. })
  213. t.Run("http_bridge mode routes to http_sse", func(t *testing.T) {
  214. httpBridgeAccount := &Account{
  215. Platform: PlatformOpenAI,
  216. Type: AccountTypeOAuth,
  217. Concurrency: 1,
  218. Extra: map[string]any{
  219. "openai_oauth_responses_websockets_v2_mode": OpenAIWSIngressModeHTTPBridge,
  220. },
  221. }
  222. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(httpBridgeAccount)
  223. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  224. require.Equal(t, "ws_v2_mode_http_bridge", decision.Reason)
  225. })
  226. t.Run("non-positive concurrency is rejected in v2 router", func(t *testing.T) {
  227. invalidConcurrency := &Account{
  228. Platform: PlatformOpenAI,
  229. Type: AccountTypeOAuth,
  230. Extra: map[string]any{
  231. "openai_oauth_responses_websockets_v2_mode": OpenAIWSIngressModeCtxPool,
  232. },
  233. }
  234. decision := NewOpenAIWSProtocolResolver(cfg).Resolve(invalidConcurrency)
  235. require.Equal(t, OpenAIUpstreamTransportHTTPSSE, decision.Transport)
  236. require.Equal(t, "account_concurrency_invalid", decision.Reason)
  237. })
  238. }