feat: Enhance DeepSeek API compatibility by updating SSE parsing, standardizing error responses, and improving API key management in the tester UI.

This commit is contained in:
CJACK
2026-02-16 15:17:42 +08:00
parent 57f2041edb
commit c7ffcd76e6
10 changed files with 195 additions and 26 deletions

View File

@@ -42,7 +42,7 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
if err == auth.ErrNoAccount {
status = http.StatusTooManyRequests
}
writeJSON(w, status, map[string]any{"error": detail})
writeOpenAIError(w, status, detail)
return
}
defer h.Auth.Release(a)
@@ -50,18 +50,18 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
var req map[string]any
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
writeOpenAIError(w, http.StatusBadRequest, "invalid json")
return
}
model, _ := req["model"].(string)
messagesRaw, _ := req["messages"].([]any)
if model == "" || len(messagesRaw) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "Request must include 'model' and 'messages'."})
writeOpenAIError(w, http.StatusBadRequest, "Request must include 'model' and 'messages'.")
return
}
thinkingEnabled, searchEnabled, ok := config.GetModelConfig(model)
if !ok {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": fmt.Sprintf("Model '%s' is not available.", model)})
writeOpenAIError(w, http.StatusServiceUnavailable, fmt.Sprintf("Model '%s' is not available.", model))
return
}
@@ -74,12 +74,16 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
sessionID, err := h.DS.CreateSession(r.Context(), a, 3)
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "invalid token."})
if a.UseConfigToken {
writeOpenAIError(w, http.StatusUnauthorized, "Account token is invalid. Please re-login the account in admin.")
} else {
writeOpenAIError(w, http.StatusUnauthorized, "Invalid token. If this should be a DS2API key, add it to config.keys first.")
}
return
}
pow, err := h.DS.GetPow(r.Context(), a, 3)
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "Failed to get PoW (invalid token or unknown error)."})
writeOpenAIError(w, http.StatusUnauthorized, "Failed to get PoW (invalid token or unknown error).")
return
}
payload := map[string]any{
@@ -92,7 +96,7 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
}
resp, err := h.DS.CallCompletion(r.Context(), a, payload, pow, 3)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "Failed to get completion."})
writeOpenAIError(w, http.StatusInternalServerError, "Failed to get completion.")
return
}
if toBool(req["stream"]) {
@@ -106,7 +110,7 @@ func (h *Handler) handleNonStream(w http.ResponseWriter, ctx context.Context, re
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
writeJSON(w, resp.StatusCode, map[string]any{"error": string(body)})
writeOpenAIError(w, resp.StatusCode, string(body))
return
}
thinking := strings.Builder{}
@@ -183,7 +187,7 @@ func (h *Handler) handleStream(w http.ResponseWriter, r *http.Request, resp *htt
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
writeJSON(w, resp.StatusCode, map[string]any{"error": string(body)})
writeOpenAIError(w, resp.StatusCode, string(body))
return
}
w.Header().Set("Content-Type", "text/event-stream")
@@ -191,7 +195,7 @@ func (h *Handler) handleStream(w http.ResponseWriter, r *http.Request, resp *htt
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "streaming unsupported"})
writeOpenAIError(w, http.StatusInternalServerError, "streaming unsupported")
return
}
@@ -436,3 +440,32 @@ func writeJSON(w http.ResponseWriter, status int, payload any) {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func writeOpenAIError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]any{
"error": map[string]any{
"message": message,
"type": openAIErrorType(status),
},
})
}
func openAIErrorType(status int) string {
switch status {
case http.StatusBadRequest:
return "invalid_request_error"
case http.StatusUnauthorized:
return "authentication_error"
case http.StatusForbidden:
return "permission_error"
case http.StatusTooManyRequests:
return "rate_limit_error"
case http.StatusServiceUnavailable:
return "service_unavailable_error"
default:
if status >= 500 {
return "api_error"
}
return "invalid_request_error"
}
}

View File

@@ -3,6 +3,7 @@ package transport
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
@@ -44,16 +45,36 @@ func safariTLSDialer() func(ctx context.Context, network, addr string) (net.Conn
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
uCfg := &utls.Config{
ServerName: host,
NextProtos: []string{"http/1.1"},
}
uCfg := &utls.Config{ServerName: host}
uConn := utls.UClient(plainConn, uCfg, utls.HelloSafari_Auto)
if err := forceHTTP11ALPN(uConn); err != nil {
_ = plainConn.Close()
return nil, err
}
err = uConn.HandshakeContext(ctx)
if err != nil {
_ = plainConn.Close()
return nil, err
}
if negotiated := uConn.ConnectionState().NegotiatedProtocol; negotiated != "" && negotiated != "http/1.1" {
_ = uConn.Close()
return nil, fmt.Errorf("unexpected ALPN protocol negotiated: %s", negotiated)
}
return uConn, nil
}
}
func forceHTTP11ALPN(uConn *utls.UConn) error {
if err := uConn.BuildHandshakeState(); err != nil {
return err
}
for _, ext := range uConn.Extensions {
alpnExt, ok := ext.(*utls.ALPNExtension)
if !ok {
continue
}
alpnExt.AlpnProtocols = []string{"http/1.1"}
return nil
}
return nil
}

View File

@@ -59,6 +59,42 @@ func ParseSSEChunkForContent(chunk map[string]any, thinkingEnabled bool, current
}
}
newType := currentFragmentType
parts := make([]ContentPart, 0, 8)
// Newer DeepSeek responses may emit fragment APPEND directly on
// path "response/fragments" instead of wrapping it in path "response".
if path == "response/fragments" {
if op, _ := chunk["o"].(string); strings.EqualFold(op, "APPEND") {
if frags, ok := v.([]any); ok {
for _, frag := range frags {
fm, ok := frag.(map[string]any)
if !ok {
continue
}
t, _ := fm["type"].(string)
content, _ := fm["content"].(string)
t = strings.ToUpper(t)
switch t {
case "THINK", "THINKING":
newType = "thinking"
if content != "" {
parts = append(parts, ContentPart{Text: content, Type: "thinking"})
}
case "RESPONSE":
newType = "text"
if content != "" {
parts = append(parts, ContentPart{Text: content, Type: "text"})
}
default:
if content != "" {
parts = append(parts, ContentPart{Text: content, Type: "text"})
}
}
}
}
}
}
if path == "response" {
if arr, ok := v.([]any); ok {
for _, it := range arr {
@@ -99,7 +135,6 @@ func ParseSSEChunkForContent(chunk map[string]any, thinkingEnabled bool, current
partType = newType
}
}
parts := make([]ContentPart, 0, 8)
switch val := v.(type) {
case string:
if val == "FINISHED" && (path == "" || path == "status") {

View File

@@ -47,3 +47,43 @@ func TestIsCitation(t *testing.T) {
t.Fatal("expected citation false")
}
}
func TestParseSSEChunkForContentFragmentsAppendSwitchToResponse(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "RESPONSE",
"content": "你好",
},
},
}
parts, finished, nextType := ParseSSEChunkForContent(chunk, true, "thinking")
if finished {
t.Fatal("expected unfinished")
}
if nextType != "text" {
t.Fatalf("expected next type text, got %q", nextType)
}
if len(parts) != 1 || parts[0].Type != "text" || parts[0].Text != "你好" {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestParseSSEChunkForContentAfterAppendUsesUpdatedType(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments/-1/content",
"v": "",
}
parts, finished, nextType := ParseSSEChunkForContent(chunk, true, "text")
if finished {
t.Fatal("expected unfinished")
}
if nextType != "text" {
t.Fatalf("expected next type text, got %q", nextType)
}
if len(parts) != 1 || parts[0].Type != "text" || parts[0].Text != "" {
t.Fatalf("unexpected parts: %#v", parts)
}
}