fix(models): surface MiniMax rate-limit and API errors instead of generic messages (#17292)

This commit is contained in:
euvre
2026-07-24 09:54:08 +08:00
committed by GitHub
parent 99bea630b0
commit 40388c4a71
2 changed files with 149 additions and 3 deletions

View File

@@ -59,6 +59,50 @@ func validateMinimaxModelName(modelName string) (string, error) {
return strings.TrimSpace(modelName), nil
}
// extractMinimaxAPIError checks a parsed MiniMax response for
// provider-specific or OpenAI-compatible error payloads.
//
// MiniMax can embed errors in the response body even when the HTTP
// status is 200 — the classic case is rate limiting, where the body
// carries a `base_resp` block with a non-zero `status_code` instead
// of the expected `choices` array. Returning the original message
// (which typically contains "rate limit" / "frequency limit" / "429")
// lets the upstream retry predicates match and retry appropriately.
//
// Returns the error description, or "" when no error is present.
func extractMinimaxAPIError(result map[string]interface{}) string {
if br, ok := result["base_resp"].(map[string]interface{}); ok {
if sc, _ := br["status_code"].(float64); sc != 0 {
msg, _ := br["status_msg"].(string)
if msg == "" {
return fmt.Sprintf("status_code %v", sc)
}
return msg
}
}
if e, ok := result["error"].(map[string]interface{}); ok {
if msg, _ := e["message"].(string); msg != "" {
return msg
}
}
return ""
}
// extractMinimaxErrorBody parses a raw HTTP error response body and
// returns the human-readable error message when the body is a
// MiniMax or OpenAI-compatible error JSON. Falls back to the raw
// body string when parsing fails so no information is lost.
func extractMinimaxErrorBody(body []byte) string {
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return string(body)
}
if msg := extractMinimaxAPIError(result); msg != "" {
return msg
}
return string(body)
}
// ChatWithMessages sends multiple messages with roles and returns response
func (m *MinimaxModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) {
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
@@ -126,7 +170,7 @@ func (m *MinimaxModel) ChatWithMessages(ctx context.Context, modelName string, m
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to send request: %d %s", resp.StatusCode, string(body))
return nil, fmt.Errorf("minimax API error: status %d: %s", resp.StatusCode, extractMinimaxErrorBody(body))
}
// Parse response
@@ -135,6 +179,11 @@ func (m *MinimaxModel) ChatWithMessages(ctx context.Context, modelName string, m
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// MiniMax can embed an error (rate limit, etc.) inside a 200 body.
if errMsg := extractMinimaxAPIError(result); errMsg != "" {
return nil, fmt.Errorf("minimax API error: %s", errMsg)
}
choices, ok := result["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil, fmt.Errorf("no choices in response")
@@ -252,7 +301,7 @@ func (m *MinimaxModel) ChatStreamlyWithSender(ctx context.Context, modelName str
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
return fmt.Errorf("minimax API error: status %d: %s", resp.StatusCode, extractMinimaxErrorBody(body))
}
// SSE parsing: read line by line
@@ -261,6 +310,13 @@ func (m *MinimaxModel) ChatStreamlyWithSender(ctx context.Context, modelName str
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
// MiniMax can send an error event (rate limit, etc.)
// without a choices array. Surface it so the retry
// predicates can match and the caller sees the real
// reason instead of a generic "stream ended" error.
if errMsg := extractMinimaxAPIError(event); errMsg != "" {
return fmt.Errorf("minimax API error: %s", errMsg)
}
return nil
}
@@ -351,7 +407,7 @@ func (m *MinimaxModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
return nil, fmt.Errorf("minimax API error: status %d: %s", resp.StatusCode, extractMinimaxErrorBody(body))
}
// Parse response

View File

@@ -153,6 +153,96 @@ func TestMinimaxChatRejectsEmptyChoices(t *testing.T) {
}
}
func TestMinimaxChatSurfacesBaseRespError(t *testing.T) {
ctx := t.Context()
srv := newMinimaxServer(t, func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) {
// MiniMax rate-limit: HTTP 200 with base_resp, no choices.
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"base_resp": map[string]interface{}{
"status_code": 1027,
"status_msg": "frequency limit reached",
},
})
})
defer srv.Close()
apiKey := "test-key"
_, err := newMinimaxForTest(srv.URL).ChatWithMessages(
ctx,
"MiniMax-M3",
[]Message{{Role: "user", Content: "ping"}},
&APIConfig{ApiKey: &apiKey},
nil,
nil,
)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "frequency limit reached") {
t.Fatalf("expected rate-limit message, got %v", err)
}
}
func TestMinimaxChatSurfacesOpenAIError(t *testing.T) {
ctx := t.Context()
srv := newMinimaxServer(t, func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) {
w.WriteHeader(http.StatusTooManyRequests)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"message": "rate limit exceeded",
"type": "rate_limit_error",
},
})
})
defer srv.Close()
apiKey := "test-key"
_, err := newMinimaxForTest(srv.URL).ChatWithMessages(
ctx,
"MiniMax-M3",
[]Message{{Role: "user", Content: "ping"}},
&APIConfig{ApiKey: &apiKey},
nil,
nil,
)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "rate limit exceeded") {
t.Fatalf("expected rate-limit message, got %v", err)
}
if !strings.Contains(err.Error(), "429") {
t.Fatalf("expected status code 429 in error, got %v", err)
}
}
func TestMinimaxStreamSurfacesBaseRespError(t *testing.T) {
ctx := t.Context()
srv := newMinimaxServer(t, func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/event-stream")
// Error event before any choices — rate limit.
_, _ = io.WriteString(w, `data: {"base_resp":{"status_code":1027,"status_msg":"frequency limit reached"}}`+"\n")
})
defer srv.Close()
apiKey := "test-key"
err := newMinimaxForTest(srv.URL).ChatStreamlyWithSender(
ctx,
"MiniMax-M3",
[]Message{{Role: "user", Content: "ping"}},
&APIConfig{ApiKey: &apiKey},
nil,
nil,
func(*string, *string) error { return nil },
)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "frequency limit reached") {
t.Fatalf("expected rate-limit message, got %v", err)
}
}
func TestMinimaxStreamForcesStreaming(t *testing.T) {
ctx := t.Context()
srv := newMinimaxServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) {