openai_access_state_failover_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. package service
  2. import (
  3. "context"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. )
  14. type openAIStream403AccountRepo struct {
  15. AccountRepository
  16. setErrorCalls int
  17. }
  18. func (r *openAIStream403AccountRepo) SetError(context.Context, int64, string) error {
  19. r.setErrorCalls++
  20. return nil
  21. }
  22. type openAIAuthPolicyAccountRepo struct {
  23. AccountRepository
  24. tempCalls int
  25. setErrorCalls int
  26. }
  27. func (r *openAIAuthPolicyAccountRepo) SetTempUnschedulable(context.Context, int64, time.Time, string) error {
  28. r.tempCalls++
  29. return nil
  30. }
  31. func (r *openAIAuthPolicyAccountRepo) SetError(context.Context, int64, string) error {
  32. r.setErrorCalls++
  33. return nil
  34. }
  35. type openAIAuthPolicy403Counter struct {
  36. counts []int64
  37. }
  38. func (s *openAIAuthPolicy403Counter) IncrementOpenAI403Count(context.Context, int64, int) (int64, error) {
  39. if len(s.counts) == 0 {
  40. return 1, nil
  41. }
  42. count := s.counts[0]
  43. s.counts = s.counts[1:]
  44. return count, nil
  45. }
  46. func (*openAIAuthPolicy403Counter) ResetOpenAI403Count(context.Context, int64) error {
  47. return nil
  48. }
  49. func TestOpenAIUpstreamAccessStateClassification(t *testing.T) {
  50. tests := []struct {
  51. name string
  52. body string
  53. want bool
  54. }{
  55. {"workspace_code", `{"detail":{"code":"deactivated_workspace"}}`, true},
  56. {"disabled_account_message", `{"error":{"message":"Your account is disabled"}}`, false},
  57. {"suspended_workspace_message", `{"response":{"error":{"message":"This workspace has been suspended"}}}`, false},
  58. {"deactivated_organization_message", `{"detail":{"message":"The organization is deactivated"}}`, false},
  59. {"scalar_detail", `{"detail":"This workspace has been disabled"}`, false},
  60. {"suspended_org_code", `{"error":{"code":"org_suspended"}}`, true},
  61. }
  62. for _, tt := range tests {
  63. t.Run(tt.name, func(t *testing.T) {
  64. body := []byte(tt.body)
  65. require.Equal(t, tt.want, isOpenAIUpstreamAccessStateError("", body))
  66. if !tt.want {
  67. return
  68. }
  69. require.True(t, (&OpenAIGatewayService{}).shouldFailoverOpenAIUpstreamResponse(newOpenAIUpstreamErrorTestAccount(), http.StatusForbidden, "", body))
  70. require.True(t, shouldFailoverOpenAIPassthroughResponse(&Account{Type: AccountTypeOAuth}, http.StatusForbidden, body))
  71. err := newOpenAIUpstreamFailoverError(http.StatusForbidden, nil, body, "", true)
  72. require.True(t, err.IsCredentialFailure())
  73. require.Equal(t, GatewayFailureScopeAccount, err.Scope)
  74. require.Equal(t, OpenAIUpstreamAccessStateReason, err.Reason)
  75. require.Equal(t, NextAccountRetry, err.NextAccountAction)
  76. require.False(t, err.RetryableOnSameAccount)
  77. require.False(t, err.RequestScopedTransient)
  78. require.Equal(t, http.StatusBadGateway, err.ClientStatusCode)
  79. require.Equal(t, openAIUpstreamAccessUnavailableClientMessage, err.ClientMessage)
  80. })
  81. }
  82. }
  83. func TestOpenAIUpstreamAccessStateDoesNotScanEchoedJSON(t *testing.T) {
  84. body := []byte(`{"error":{"code":"invalid_request_error","message":"Invalid input"},"echo":{"prompt":"my account is disabled"}}`)
  85. require.False(t, isOpenAIUpstreamAccessStateError("", body))
  86. require.False(t, shouldFailoverOpenAIPassthroughResponse(&Account{Type: AccountTypeOAuth}, http.StatusBadRequest, body))
  87. }
  88. func TestOpenAIHTTPAccessStateDoesNotTrustBadRequestMessage(t *testing.T) {
  89. body := []byte(`{"error":{"type":"invalid_request_error","code":"unknown_parameter","message":"Unknown parameter: account disabled"}}`)
  90. svc := &OpenAIGatewayService{}
  91. require.False(t, isOpenAIUpstreamAccessStateError("", body), "free-form stream messages are not durable account evidence")
  92. require.False(t, isOpenAIHTTPUpstreamAccessStateError(http.StatusBadRequest, "", body))
  93. require.False(t, svc.shouldFailoverOpenAIUpstreamResponse(newOpenAIUpstreamErrorTestAccount(), http.StatusBadRequest, "", body))
  94. require.False(t, shouldFailoverOpenAIPassthroughResponse(&Account{Type: AccountTypeOAuth}, http.StatusBadRequest, body))
  95. err := newOpenAIUpstreamFailoverError(http.StatusBadRequest, nil, body, "", false)
  96. require.False(t, err.IsCredentialFailure())
  97. }
  98. func TestOpenAIHTTPAccessStateBadRequestDoesNotDisableAccount(t *testing.T) {
  99. repo := &openAIStream403AccountRepo{}
  100. svc := &OpenAIGatewayService{rateLimitService: &RateLimitService{accountRepo: repo}}
  101. account := &Account{ID: 925, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
  102. body := []byte(`{"error":{"code":"unknown_parameter","message":"Unknown parameter: account disabled"}}`)
  103. disabled := svc.handleOpenAIAccountUpstreamError(context.Background(), account, http.StatusBadRequest, nil, body)
  104. require.False(t, disabled)
  105. require.Zero(t, repo.setErrorCalls)
  106. require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
  107. }
  108. func TestOpenAIStreamEchoedAccessStateMessageDoesNotDisableOrFailover(t *testing.T) {
  109. repo := &openAIStream403AccountRepo{}
  110. svc := &OpenAIGatewayService{rateLimitService: &RateLimitService{accountRepo: repo}}
  111. account := &Account{ID: 926, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
  112. payload := []byte(`{"type":"response.failed","response":{"error":{"type":"invalid_request_error","code":"unknown_parameter","message":"Unknown parameter: account disabled"}}}`)
  113. message := extractOpenAISSEErrorMessage(payload)
  114. require.False(t, isOpenAIUpstreamAccessStateError(message, payload))
  115. require.False(t, openAIStreamFailedEventShouldFailover(payload, message))
  116. status, disabled := svc.handleOpenAIStreamTerminalAccountSideEffects(nil, account, payload, message, nil)
  117. require.Equal(t, http.StatusBadGateway, status)
  118. require.False(t, disabled)
  119. require.Zero(t, repo.setErrorCalls)
  120. require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
  121. }
  122. func TestOpenAIHTTPAccessStateTrustsStructuredCode(t *testing.T) {
  123. repo := &openAIStream403AccountRepo{}
  124. svc := &OpenAIGatewayService{rateLimitService: &RateLimitService{accountRepo: repo}}
  125. account := &Account{ID: 930, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
  126. body := []byte(`{"error":{"code":"organization_deactivated","message":"request rejected"}}`)
  127. require.True(t, isOpenAIHTTPUpstreamAccessStateError(http.StatusBadRequest, "", body))
  128. require.True(t, svc.shouldFailoverOpenAIUpstreamResponse(newOpenAIUpstreamErrorTestAccount(), http.StatusBadRequest, "", body))
  129. require.True(t, svc.handleOpenAIAccountUpstreamError(context.Background(), account, http.StatusBadRequest, nil, body))
  130. require.Equal(t, 1, repo.setErrorCalls)
  131. require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
  132. }
  133. func TestOpenAIHTTPAuthMessagesUseExistingStatusPolicies(t *testing.T) {
  134. t.Run("oauth 401 remains recoverable", func(t *testing.T) {
  135. repo := &openAIAuthPolicyAccountRepo{}
  136. rateLimits := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
  137. svc := &OpenAIGatewayService{rateLimitService: rateLimits}
  138. account := &Account{ID: 931, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true,
  139. Credentials: map[string]any{"refresh_token": "refreshable"}}
  140. body := []byte(`{"error":{"message":"account is disabled"}}`)
  141. require.False(t, isOpenAIHTTPUpstreamAccessStateError(http.StatusUnauthorized, "", body))
  142. require.True(t, svc.handleOpenAIAccountUpstreamError(context.Background(), account, http.StatusUnauthorized, nil, body))
  143. require.Zero(t, repo.setErrorCalls)
  144. require.Equal(t, 1, repo.tempCalls)
  145. })
  146. t.Run("403 uses counter cooldown", func(t *testing.T) {
  147. repo := &openAIAuthPolicyAccountRepo{}
  148. counter := &openAIAuthPolicy403Counter{counts: []int64{1}}
  149. rateLimits := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
  150. rateLimits.openAI403CounterCache = counter
  151. svc := &OpenAIGatewayService{rateLimitService: rateLimits}
  152. rateLimits.SetAccountRuntimeBlocker(svc)
  153. account := &Account{ID: 932, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true}
  154. body := []byte(`{"error":{"message":"workspace has been suspended"}}`)
  155. require.False(t, isOpenAIHTTPUpstreamAccessStateError(http.StatusForbidden, "", body))
  156. require.True(t, svc.handleOpenAIAccountUpstreamError(context.Background(), account, http.StatusForbidden, nil, body))
  157. require.Zero(t, repo.setErrorCalls)
  158. require.Equal(t, 1, repo.tempCalls)
  159. })
  160. }
  161. func TestOpenAICyberPolicyWrapped5xxNeverFailsOver(t *testing.T) {
  162. body := []byte(`{"error":{"code":"cyber_policy","message":"blocked"}}`)
  163. svc := &OpenAIGatewayService{}
  164. require.False(t, svc.shouldFailoverOpenAIUpstreamResponse(newOpenAIUpstreamErrorTestAccount(), http.StatusBadGateway, "wrapped upstream failure", body))
  165. require.False(t, shouldFailoverOpenAIPassthroughResponse(&Account{Type: AccountTypeOAuth}, http.StatusBadGateway, body))
  166. }
  167. func TestOpenAICapacityFailoverCarriesSafeTerminalResponse(t *testing.T) {
  168. message := "Our servers are currently overloaded. Please try again later."
  169. body := []byte(`{"error":{"code":"server_is_overloaded","message":"` + message + `"}}`)
  170. err := newOpenAIUpstreamFailoverError(http.StatusBadRequest, nil, body, message, false)
  171. require.True(t, err.IsOpenAICapacityShed())
  172. require.Equal(t, http.StatusServiceUnavailable, err.ClientStatusCode)
  173. require.Equal(t, message, err.ClientMessage)
  174. require.NotContains(t, err.ClientMessage, "server_is_overloaded")
  175. }
  176. func TestOpenAIStreamSemanticStatusesPreservedAcrossTerminalShapes(t *testing.T) {
  177. tests := []struct {
  178. name string
  179. body string
  180. status int
  181. wantFailover bool
  182. }{
  183. {"unauthorized", `{"type":"error","error":{"type":"authentication_error","code":"invalid_api_key","message":"unauthorized"}}`, http.StatusUnauthorized, true},
  184. {"forbidden", `{"type":"response.failed","response":{"error":{"type":"permission_error","message":"forbidden"}}}`, http.StatusForbidden, false},
  185. {"rate_limit", `{"error":{"type":"rate_limit_error","code":"rate_limit_exceeded","message":"slow down"}}`, http.StatusTooManyRequests, true},
  186. {"overload_529", `{"type":"error","error":{"status_code":529,"code":"overloaded","message":"overloaded"}}`, 529, true},
  187. }
  188. for _, tt := range tests {
  189. t.Run(tt.name, func(t *testing.T) {
  190. payload := []byte(tt.body)
  191. message := extractOpenAISSEErrorMessage(payload)
  192. require.Equal(t, tt.status, openAIStreamFailureStatus(payload, message))
  193. require.Equal(t, tt.wantFailover, openAIStreamErrorEventShouldFailover(payload, message))
  194. })
  195. }
  196. }
  197. func TestOpenAIStreamBareErrorUsesSemanticFailover(t *testing.T) {
  198. payload := []byte(`{"error":{"type":"rate_limit_error","code":"rate_limit_exceeded","message":"slow down"}}`)
  199. require.True(t, openAIStreamErrorEventShouldFailover(payload, "slow down"))
  200. }
  201. func TestOpenAIStream403FailoverRequiresStructuredAccountCredentialSignal(t *testing.T) {
  202. tests := []struct {
  203. name string
  204. payload string
  205. want bool
  206. }{
  207. {
  208. name: "ordinary permission error",
  209. payload: `{"type":"response.failed","response":{"error":{"type":"permission_error","code":"forbidden","message":"access denied for this request"}}}`,
  210. },
  211. {
  212. name: "explicit 403 request status",
  213. payload: `{"type":"error","error":{"type":"permission_error","code":"forbidden","status_code":403,"message":"forbidden content"}}`,
  214. },
  215. {
  216. name: "structured access state",
  217. payload: `{"type":"response.failed","response":{"error":{"code":"workspace_suspended","message":"workspace is suspended"}}}`,
  218. want: true,
  219. },
  220. {
  221. name: "explicit credential auth code",
  222. payload: `{"type":"error","error":{"type":"permission_error","code":"invalid_api_key","status_code":403,"message":"credential rejected"}}`,
  223. want: true,
  224. },
  225. {
  226. name: "explicit authentication type",
  227. payload: `{"type":"error","error":{"type":"authentication_error","status_code":403,"message":"credential rejected"}}`,
  228. want: true,
  229. },
  230. }
  231. for _, tt := range tests {
  232. t.Run(tt.name, func(t *testing.T) {
  233. payload := []byte(tt.payload)
  234. message := extractOpenAISSEErrorMessage(payload)
  235. require.Equal(t, http.StatusForbidden, openAIStreamFailureStatus(payload, message))
  236. require.Equal(t, tt.want, openAIStreamFailedEventShouldFailover(payload, message))
  237. require.Equal(t, tt.want, openAIStreamErrorEventShouldFailover(payload, message))
  238. })
  239. }
  240. }
  241. func TestOpenAIStream403PostOutputAccountSideEffectsIgnoreRequestPermissionErrors(t *testing.T) {
  242. repo := &openAIStream403AccountRepo{}
  243. rateLimits := &RateLimitService{accountRepo: repo}
  244. svc := &OpenAIGatewayService{rateLimitService: rateLimits}
  245. account := &Account{ID: 918, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  246. payload := []byte(`{"type":"error","error":{"type":"permission_error","code":"forbidden","status_code":403,"message":"access denied for this request"}}`)
  247. status, disabled := svc.handleOpenAIStreamTerminalAccountSideEffects(nil, account, payload, "access denied for this request", nil)
  248. require.Equal(t, http.StatusForbidden, status)
  249. require.False(t, disabled)
  250. require.Zero(t, repo.setErrorCalls)
  251. require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
  252. }
  253. func TestOpenAIStream403ExplicitCredentialAuthAppliesAccountSideEffects(t *testing.T) {
  254. repo := &openAIStream403AccountRepo{}
  255. rateLimits := &RateLimitService{accountRepo: repo}
  256. svc := &OpenAIGatewayService{rateLimitService: rateLimits}
  257. account := &Account{ID: 917, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  258. payload := []byte(`{"type":"error","error":{"type":"permission_error","code":"invalid_api_key","status_code":403,"message":"credential rejected"}}`)
  259. status, disabled := svc.handleOpenAIStreamTerminalAccountSideEffects(nil, account, payload, "credential rejected", nil)
  260. require.Equal(t, http.StatusForbidden, status)
  261. require.True(t, disabled)
  262. require.Equal(t, 1, repo.setErrorCalls)
  263. require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
  264. }
  265. func TestOpenAIWSStandaloneFailedStructured403AppliesAccountSideEffectsOnce(t *testing.T) {
  266. repo := &openAIStream403AccountRepo{}
  267. svc := &OpenAIGatewayService{rateLimitService: &RateLimitService{accountRepo: repo}}
  268. account := &Account{ID: 923, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  269. failed := []byte(`{"type":"response.failed","response":{"error":{"type":"permission_error","code":"invalid_api_key","status_code":403,"message":"credential rejected"}}}`)
  270. require.True(t, svc.handleOpenAIWSFailureAccountSideEffects(context.Background(), account, "gpt-5", nil, failed))
  271. require.Equal(t, 1, repo.setErrorCalls)
  272. require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
  273. }
  274. func TestOpenAIWSPairedStructured403SideEffectsCanBeDeduplicated(t *testing.T) {
  275. repo := &openAIStream403AccountRepo{}
  276. svc := &OpenAIGatewayService{rateLimitService: &RateLimitService{accountRepo: repo}}
  277. account := &Account{ID: 924, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  278. errorEvent := []byte(`{"type":"error","error":{"code":"workspace_suspended","status_code":403,"message":"workspace is suspended"}}`)
  279. failedEvent := []byte(`{"type":"response.failed","response":{"error":{"code":"workspace_suspended","status_code":403,"message":"workspace is suspended"}}}`)
  280. applied := svc.handleOpenAIWSFailureAccountSideEffects(context.Background(), account, "gpt-5", nil, errorEvent)
  281. if !applied {
  282. applied = svc.handleOpenAIWSFailureAccountSideEffects(context.Background(), account, "gpt-5", nil, failedEvent)
  283. }
  284. require.True(t, applied)
  285. require.Equal(t, 1, repo.setErrorCalls)
  286. }
  287. func TestOpenAIStreamAccessStateAppliesAccountHealthBeforeFailover(t *testing.T) {
  288. svc := &OpenAIGatewayService{}
  289. account := &Account{ID: 919, Platform: PlatformOpenAI, Type: AccountTypeSetupToken}
  290. payload := []byte(`{"type":"response.failed","response":{"error":{"code":"workspace_suspended","message":"workspace is suspended"}}}`)
  291. status, disabled := svc.handleOpenAIStreamTerminalAccountSideEffects(nil, account, payload, "workspace is suspended", nil)
  292. require.Equal(t, http.StatusForbidden, status)
  293. require.True(t, disabled)
  294. require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
  295. }
  296. func TestOpenAIStreamPairedFailureAppliesAccountSideEffectsOnce(t *testing.T) {
  297. const upstream = "data: {\"type\":\"response.output_text.delta\",\"delta\":\"partial\"}\n\n" +
  298. "data: {\"type\":\"error\",\"error\":{\"status_code\":403,\"code\":\"workspace_suspended\",\"message\":\"workspace is suspended\"}}\n\n" +
  299. "data: {\"type\":\"response.failed\",\"response\":{\"id\":\"resp_failed\",\"error\":{\"status_code\":403,\"code\":\"workspace_suspended\",\"message\":\"workspace is suspended\"}}}\n\n"
  300. t.Run("native", func(t *testing.T) {
  301. gin.SetMode(gin.TestMode)
  302. repo := &openAIStream403AccountRepo{}
  303. svc := &OpenAIGatewayService{
  304. cfg: &config.Config{},
  305. toolCorrector: NewCodexToolCorrector(),
  306. rateLimitService: &RateLimitService{accountRepo: repo},
  307. }
  308. account := &Account{ID: 921, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  309. recorder := newOpenAIResponseFlushRecorder()
  310. c, _ := gin.CreateTestContext(recorder)
  311. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  312. resp := &http.Response{
  313. StatusCode: http.StatusOK,
  314. Header: http.Header{"Content-Type": []string{"text/event-stream"}},
  315. Body: io.NopCloser(strings.NewReader(upstream)),
  316. }
  317. result, err := svc.handleStreamingResponse(context.Background(), resp, c, account, time.Now(), "gpt-5", "gpt-5")
  318. require.Error(t, err)
  319. require.NotNil(t, result)
  320. require.Equal(t, 1, repo.setErrorCalls)
  321. })
  322. t.Run("passthrough", func(t *testing.T) {
  323. gin.SetMode(gin.TestMode)
  324. repo := &openAIStream403AccountRepo{}
  325. svc := &OpenAIGatewayService{
  326. cfg: &config.Config{},
  327. rateLimitService: &RateLimitService{accountRepo: repo},
  328. }
  329. account := &Account{ID: 922, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  330. recorder := httptest.NewRecorder()
  331. c, _ := gin.CreateTestContext(recorder)
  332. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  333. writer := &passthroughFlushTestWriter{
  334. ResponseWriter: c.Writer,
  335. recorder: recorder,
  336. failAfterWrites: -1,
  337. }
  338. c.Writer = writer
  339. resp := &http.Response{
  340. StatusCode: http.StatusOK,
  341. Header: http.Header{"Content-Type": []string{"text/event-stream"}},
  342. Body: io.NopCloser(strings.NewReader(upstream)),
  343. }
  344. result, err := svc.handleStreamingResponsePassthrough(
  345. context.Background(), resp, c, account, time.Now(), "gpt-5", "gpt-5",
  346. )
  347. require.Error(t, err)
  348. require.NotNil(t, result)
  349. require.Equal(t, 1, repo.setErrorCalls)
  350. })
  351. }
  352. func TestOpenAIStreamOAuthLike429GetsDeadlineWithoutImmediateRuntimeBlock(t *testing.T) {
  353. for _, accountType := range []string{AccountTypeOAuth, AccountTypeSetupToken} {
  354. t.Run(accountType, func(t *testing.T) {
  355. svc := &OpenAIGatewayService{}
  356. account := &Account{ID: 920, Platform: PlatformOpenAI, Type: accountType}
  357. payload := []byte(`{"type":"error","error":{"type":"rate_limit_error","code":"rate_limit_exceeded","message":"slow down"}}`)
  358. status, disabled := svc.handleOpenAIStreamTerminalAccountSideEffects(nil, account, payload, "slow down", nil)
  359. err := svc.newOpenAIAccountFailoverError(account, status, nil, payload, "slow down", disabled, false)
  360. require.Equal(t, http.StatusTooManyRequests, status)
  361. require.False(t, disabled)
  362. require.True(t, err.RetryableOnSameAccount)
  363. require.False(t, err.SameAccountRetryDeadline.IsZero())
  364. require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
  365. })
  366. }
  367. }