flipped-aurora/gin-vue-admin · error

上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s

Error message

上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s

What it means

This is the readable-body branch of the same non-2xx check in proxyLLMStream. When the upstream LLM service responds with a status outside 200-299 and its body can be read, the error includes status, Content-Type, and a preview of the body (previewResponseBody). It surfaces the upstream's own error payload (usually JSON with a provider error message) to the caller of LLMAuto.

Source

Thrown at server/api/v1/system/sys_auto_code.go:176

	if stream, ok := llm["stream"].(bool); ok && stream {
		return true
	}
	return strings.Contains(strings.ToLower(c.GetHeader("Accept")), "text/event-stream")
}

func (autoApi *AutoCodeApi) proxyLLMStream(c *gin.Context, llm common.JSONMap) error {
	res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode < 200 || res.StatusCode >= 300 {
		body, readErr := io.ReadAll(res.Body)
		if readErr != nil {
			return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr)
		}
		return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body))
	}

	flusher, ok := c.Writer.(http.Flusher)
	if !ok {
		return errors.New("当前响应不支持流式输出")
	}

	copyLLMStreamHeaders(c.Writer.Header(), res.Header)
	if c.Writer.Header().Get("Content-Type") == "" {
		c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
	}
	if c.Writer.Header().Get("Cache-Control") == "" {
		c.Writer.Header().Set("Cache-Control", "no-cache")
	}
	c.Writer.Header().Set("Connection", "keep-alive")
	c.Writer.Header().Set("X-Accel-Buffering", "no")
	c.Status(res.StatusCode)
	flusher.Flush()

View on GitHub (pinned to 3136500ef3)

Solutions

  1. Read the body preview in the error message — it normally contains the provider's JSON error (e.g. "invalid_api_key") telling you exactly what to fix.
  2. Verify the API key/env config for the LLM service and rotate if expired.
  3. Confirm the model name and API path still exist on the provider (version changes often turn 200 into 404).
  4. Back off and retry on 429/5xx; add rate limiting on your side to stay under quota.

Example fix

// before
// error: 上游大模型流式服务返回非 2xx: status=401 ... body={"error":"invalid api key"}
LLM_API_KEY=old-expired-key

// after
LLM_API_KEY=sk-current-valid-key  # then restart the server
Defensive patterns

Strategy: try-catch

Validate before calling

resp, err := client.Do(req)
if err != nil { return err }
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
    return fmt.Errorf("LLM upstream %d: %s", resp.StatusCode, string(b))
}

Try / catch

if err := api.proxyLLMStream(c, payload); err != nil {
    var apiErr struct{ Error struct{ Message string `json:"message"` } `json:"error"` }
    if json.Unmarshal([]byte(extractBody(err.Error())), &apiErr) == nil && apiErr.Error.Message != "" {
        c.JSON(502, gin.H{"msg": apiErr.Error.Message})
        return
    }
    c.JSON(502, gin.H{"msg": err.Error()})
}

Prevention

When it happens

Trigger: LLMAuto -> proxyLLMStream receives 400 (malformed prompt/model name), 401/403 (invalid or expired API key), 404 (wrong model or path), 429 (rate limit), or 5xx (provider outage) with a readable error body from the LLM endpoint.

Common situations: LLM_API_KEY missing/expired, wrong model name after a provider API version change, base URL misconfigured (pointing to a proxy root), free-tier quota exhausted, or provider regional outage producing 5xx JSON error bodies.

Related errors


AI-assisted analysis of flipped-aurora/gin-vue-admin@3136500ef3 (2026-08-31). Data as JSON: /api/errors/e39f4a9e810746e4. Report an issue: GitHub.