mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-28 10:44:16 +08:00
When /v1/responses streaming fails after headers are sent, we now emit a type=error chunk instead of an HTTP-style {error:{...}} payload, preventing AI SDK chunk validation errors.
120 lines
3.3 KiB
Go
120 lines
3.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type openAIResponsesStreamErrorChunk struct {
|
|
Type string `json:"type"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
SequenceNumber int `json:"sequence_number"`
|
|
}
|
|
|
|
func openAIResponsesStreamErrorCode(status int) string {
|
|
switch status {
|
|
case http.StatusUnauthorized:
|
|
return "invalid_api_key"
|
|
case http.StatusForbidden:
|
|
return "insufficient_quota"
|
|
case http.StatusTooManyRequests:
|
|
return "rate_limit_exceeded"
|
|
case http.StatusNotFound:
|
|
return "model_not_found"
|
|
case http.StatusRequestTimeout:
|
|
return "request_timeout"
|
|
default:
|
|
if status >= http.StatusInternalServerError {
|
|
return "internal_server_error"
|
|
}
|
|
if status >= http.StatusBadRequest {
|
|
return "invalid_request_error"
|
|
}
|
|
return "unknown_error"
|
|
}
|
|
}
|
|
|
|
// BuildOpenAIResponsesStreamErrorChunk builds an OpenAI Responses streaming error chunk.
|
|
//
|
|
// Important: OpenAI's HTTP error bodies are shaped like {"error":{...}}; those are valid for
|
|
// non-streaming responses, but streaming clients validate SSE `data:` payloads against a union
|
|
// of chunks that requires a top-level `type` field.
|
|
func BuildOpenAIResponsesStreamErrorChunk(status int, errText string, sequenceNumber int) []byte {
|
|
if status <= 0 {
|
|
status = http.StatusInternalServerError
|
|
}
|
|
if sequenceNumber < 0 {
|
|
sequenceNumber = 0
|
|
}
|
|
|
|
message := strings.TrimSpace(errText)
|
|
if message == "" {
|
|
message = http.StatusText(status)
|
|
}
|
|
|
|
code := openAIResponsesStreamErrorCode(status)
|
|
|
|
trimmed := strings.TrimSpace(errText)
|
|
if trimmed != "" && json.Valid([]byte(trimmed)) {
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(trimmed), &payload); err == nil {
|
|
if t, ok := payload["type"].(string); ok && strings.TrimSpace(t) == "error" {
|
|
if m, ok := payload["message"].(string); ok && strings.TrimSpace(m) != "" {
|
|
message = strings.TrimSpace(m)
|
|
}
|
|
if v, ok := payload["code"]; ok && v != nil {
|
|
if c, ok := v.(string); ok && strings.TrimSpace(c) != "" {
|
|
code = strings.TrimSpace(c)
|
|
} else {
|
|
code = strings.TrimSpace(fmt.Sprint(v))
|
|
}
|
|
}
|
|
if v, ok := payload["sequence_number"].(float64); ok && sequenceNumber == 0 {
|
|
sequenceNumber = int(v)
|
|
}
|
|
}
|
|
if e, ok := payload["error"].(map[string]any); ok {
|
|
if m, ok := e["message"].(string); ok && strings.TrimSpace(m) != "" {
|
|
message = strings.TrimSpace(m)
|
|
}
|
|
if v, ok := e["code"]; ok && v != nil {
|
|
if c, ok := v.(string); ok && strings.TrimSpace(c) != "" {
|
|
code = strings.TrimSpace(c)
|
|
} else {
|
|
code = strings.TrimSpace(fmt.Sprint(v))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.TrimSpace(code) == "" {
|
|
code = "unknown_error"
|
|
}
|
|
|
|
data, err := json.Marshal(openAIResponsesStreamErrorChunk{
|
|
Type: "error",
|
|
Code: code,
|
|
Message: message,
|
|
SequenceNumber: sequenceNumber,
|
|
})
|
|
if err == nil {
|
|
return data
|
|
}
|
|
|
|
// Extremely defensive fallback.
|
|
data, _ = json.Marshal(openAIResponsesStreamErrorChunk{
|
|
Type: "error",
|
|
Code: "internal_server_error",
|
|
Message: message,
|
|
SequenceNumber: sequenceNumber,
|
|
})
|
|
if len(data) > 0 {
|
|
return data
|
|
}
|
|
return []byte(`{"type":"error","code":"internal_server_error","message":"internal error","sequence_number":0}`)
|
|
}
|