收紧 Grok 容量重试与兼容性分类

This commit is contained in:
IanShaw
2026-08-20 08:07:04 -07:00
parent d78e366db5
commit 39aaf2fea3
6 changed files with 36 additions and 13 deletions
+2 -1
View File
@@ -1288,7 +1288,7 @@ func (s *OpenAIGatewayService) handleGrokMediaErrorResponse(
Detail: upstreamDetail,
})
if kind == "failover" {
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, body)
retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, body)
return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode,
ResponseBody: body,
@@ -1297,6 +1297,7 @@ func (s *OpenAIGatewayService) handleGrokMediaErrorResponse(
RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
SameAccountRetryDelay: retryDelay,
SameAccountRetryDeadline: retryDeadline,
SameAccountRetryMax: retryMax,
}
}
@@ -119,7 +119,7 @@ func classifyGrokUpstreamFailure(statusCode int, responseBody []byte, requestedM
// deployment while the same request is valid on another account. Treat
// these precise decoder/content-shape failures as account compatibility
// errors, rather than durable account health failures.
if isGrokCompatibilityError(low, code) {
if isGrokCompatibilityError(statusCode, low, code) {
return GrokUpstreamFailureDecision{
Class: GrokFailureCompatibility,
Model: model,
@@ -406,15 +406,19 @@ func grokRetryableOnSameAccount(account *Account, statusCode int, responseBody [
return account.IsPoolMode() && account.IsPoolModeRetryableStatus(statusCode)
}
func grokSameAccountRetryMetadata(account *Account, statusCode int, responseBody []byte) (bool, time.Duration, time.Time) {
func grokSameAccountRetryMetadata(account *Account, statusCode int, responseBody []byte) (bool, time.Duration, time.Time, int) {
if !grokRetryableOnSameAccount(account, statusCode, responseBody) {
return false, 0, time.Time{}
return false, 0, time.Time{}, 0
}
decision := classifyGrokUpstreamFailure(statusCode, responseBody, "")
if decision.Class != GrokFailureModelCapacity {
return true, 0, time.Time{}
return true, 0, time.Time{}, 0
}
return true, 500 * time.Millisecond, time.Now().Add(30 * time.Second)
// The error is reconstructed after every upstream attempt, so a deadline
// stored on the error cannot provide a request-wide window. Cap capacity
// retries explicitly to one replay; this remains effective even when the
// first attempt itself takes longer than the nominal 30-second window.
return true, 500 * time.Millisecond, time.Now().Add(30 * time.Second), 1
}
// shouldMarkGrokTeamModelRateLimit controls the process-local sibling-account
@@ -430,7 +434,10 @@ func shouldMarkGrokTeamModelRateLimit(statusCode int, responseBody []byte) bool
return statusCode == http.StatusTooManyRequests || decision.Class == GrokFailureFreeUsage
}
func isGrokCompatibilityError(low, code string) bool {
func isGrokCompatibilityError(statusCode int, low, code string) bool {
if statusCode != http.StatusBadRequest && statusCode != http.StatusUnprocessableEntity {
return false
}
combined := strings.ToLower(strings.TrimSpace(low + " " + code))
// Compaction blobs are account/session-bound and frequently fail with 400
// or 422 after a reconnect. Also cover xAI's JSON decoder shape errors.
@@ -110,17 +110,19 @@ func TestShouldMarkGrokTeamModelRateLimit_ExcludesCapacity(t *testing.T) {
func TestGrokSameAccountRetryMetadata_CapacityDeadline(t *testing.T) {
account := &Account{ID: 9107, Platform: PlatformGrok, Type: AccountTypeOAuth}
retryable, delay, deadline := grokSameAccountRetryMetadata(account, http.StatusTooManyRequests,
retryable, delay, deadline, retryMax := grokSameAccountRetryMetadata(account, http.StatusTooManyRequests,
[]byte(`{"error":{"message":"model capacity exceeded"}}`))
require.True(t, retryable)
require.Equal(t, 500*time.Millisecond, delay)
require.WithinDuration(t, time.Now().Add(30*time.Second), deadline, 2*time.Second)
require.Equal(t, 1, retryMax)
retryable, delay, deadline = grokSameAccountRetryMetadata(account, http.StatusTooManyRequests,
retryable, delay, deadline, retryMax = grokSameAccountRetryMetadata(account, http.StatusTooManyRequests,
[]byte(`{"error":{"message":"rate limit exceeded"}}`))
require.False(t, retryable)
require.Zero(t, delay)
require.True(t, deadline.IsZero())
require.Zero(t, retryMax)
}
func TestClassifyGrokUpstreamFailure_ValidationNoCool(t *testing.T) {
@@ -151,6 +153,15 @@ func TestClassifyGrokUpstreamFailure_CompatibilityDoesNotCooldown(t *testing.T)
}
}
func TestClassifyGrokUpstreamFailure_CompatibilityRequiresClientError(t *testing.T) {
body := []byte(`{"error":{"message":"upstream failed while handling the compaction blob"}}`)
for _, status := range []int{http.StatusBadGateway, http.StatusInternalServerError} {
d := classifyGrokUpstreamFailure(status, body, "grok-4.6")
require.NotEqual(t, GrokFailureCompatibility, d.Class)
require.True(t, d.ShouldCooldown)
}
}
func TestClassifyGrokUpstreamFailure_GenericShapeErrorDoesNotFailover(t *testing.T) {
d := classifyGrokUpstreamFailure(http.StatusBadRequest,
[]byte(`{"error":{"message":"data did not match any variant of the untagged enum content"}}`), "grok-4.6")
@@ -200,7 +200,7 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
})
s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.StatusCode, resp.Header, respBody)
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode,
ResponseBody: respBody,
@@ -209,6 +209,7 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
SameAccountRetryDelay: retryDelay,
SameAccountRetryDeadline: retryDeadline,
SameAccountRetryMax: retryMax,
}
}
return s.handleChatCompletionsErrorResponse(resp, c, account, billingModel)
@@ -173,7 +173,7 @@ func (s *OpenAIGatewayService) forwardGrokResponses(
markGrokTeamModelRateLimit(account, upstreamModel, resolveGrokTeamRateLimitUntil(time.Now().Add(grokTeamRateLimitDefaultTTL), time.Now()))
}
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode,
ResponseBody: respBody,
@@ -182,6 +182,7 @@ func (s *OpenAIGatewayService) forwardGrokResponses(
RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
SameAccountRetryDelay: retryDelay,
SameAccountRetryDeadline: retryDeadline,
SameAccountRetryMax: retryMax,
}
}
return s.handleErrorResponse(ctx, resp, c, account, patchedBody, upstreamModel)
@@ -1186,7 +1187,7 @@ func (s *OpenAIGatewayService) describeGrokComposerImage(
})
s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, grokComposerImageBridgeVisionModel), account, resp.StatusCode, resp.Header, respBody)
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
return "", OpenAIUsage{}, &UpstreamFailoverError{
StatusCode: resp.StatusCode,
ResponseBody: respBody,
@@ -1195,6 +1196,7 @@ func (s *OpenAIGatewayService) describeGrokComposerImage(
RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
SameAccountRetryDelay: retryDelay,
SameAccountRetryDeadline: retryDeadline,
SameAccountRetryMax: retryMax,
}
}
return "", OpenAIUsage{}, fmt.Errorf("grok composer image bridge upstream error: %s", upstreamMsg)
@@ -633,7 +633,7 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
})
s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.StatusCode, resp.Header, respBody)
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
retryable, retryDelay, retryDeadline, retryMax := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
return nil, &UpstreamFailoverError{
StatusCode: resp.StatusCode,
ResponseBody: respBody,
@@ -642,6 +642,7 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
RequestScopedTransient: retryable && resp.StatusCode == http.StatusTooManyRequests,
SameAccountRetryDelay: retryDelay,
SameAccountRetryDeadline: retryDeadline,
SameAccountRetryMax: retryMax,
}
}
return s.handleChatCompletionsErrorResponse(resp, c, account, billingModel)