mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-24 17:36:47 +08:00
fix(models): surface MiniMax rate-limit and API errors instead of generic messages (#17292)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user