From 4946a8b449a963cbe04c1719f88f07d95f692f29 Mon Sep 17 00:00:00 2001
From: qs3c <2749950753@qq.com>
Date: Wed, 4 Mar 2026 17:50:46 +0800
Subject: [PATCH 1/6] fix(openai_compat): clarify HTML response errors
---
pkg/providers/openai_compat/provider.go | 48 +++++++++++++++++++-
pkg/providers/openai_compat/provider_test.go | 21 +++++++++
2 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index ff9109e96..621e34a89 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -192,7 +192,53 @@ func (p *Provider) Chat(
return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, string(body))
}
- return parseResponse(body)
+ out, err := parseResponse(body)
+ if err != nil {
+ return nil, wrapResponseParseError(err, body, resp.Header.Get("Content-Type"), p.apiBase)
+ }
+
+ return out, nil
+}
+
+func wrapResponseParseError(err error, body []byte, contentType, apiBase string) error {
+ trimmedContentType := strings.TrimSpace(contentType)
+ if looksLikeHTML(body, trimmedContentType) {
+ contentTypeHint := ""
+ if trimmedContentType != "" {
+ contentTypeHint = fmt.Sprintf(" (content-type: %s)", trimmedContentType)
+ }
+ return fmt.Errorf(
+ "expected JSON response from %s/chat/completions, but received HTML%s; check api_base or proxy configuration. Response preview: %s",
+ apiBase,
+ contentTypeHint,
+ responsePreview(body, 160),
+ )
+ }
+ return err
+}
+
+func looksLikeHTML(body []byte, contentType string) bool {
+ contentType = strings.ToLower(strings.TrimSpace(contentType))
+ if strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") {
+ return true
+ }
+
+ trimmed := strings.ToLower(strings.TrimSpace(string(body)))
+ return strings.HasPrefix(trimmed, ""
+ }
+ if len(preview) <= max {
+ return preview
+ }
+ return preview[:max] + "..."
}
func parseResponse(body []byte) (*LLMResponse, error) {
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 174bcf00d..244a20672 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -212,6 +212,27 @@ func TestProviderChat_HTTPError(t *testing.T) {
}
}
+func TestProviderChat_HTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("
gateway login"))
+ }))
+ defer server.Close()
+
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "received HTML") {
+ t.Fatalf("expected helpful HTML error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
+ t.Fatalf("expected configuration hint, got %v", err)
+ }
+}
+
func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testing.T) {
var requestBody map[string]any
From a305c0a4790d39590592f529a883386b9db83316 Mon Sep 17 00:00:00 2001
From: amagi <2749950753@qq.com>
Date: Wed, 4 Mar 2026 23:57:26 +0800
Subject: [PATCH 2/6] fix(openai_compat): avoid predeclared identifier in
preview
---
pkg/providers/openai_compat/provider.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 621e34a89..e6ccbdefc 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -230,15 +230,15 @@ func looksLikeHTML(body []byte, contentType string) bool {
strings.HasPrefix(trimmed, ""
}
- if len(preview) <= max {
+ if len(preview) <= maxLen {
return preview
}
- return preview[:max] + "..."
+ return preview[:maxLen] + "..."
}
func parseResponse(body []byte) (*LLMResponse, error) {
From 9216cd14b5a7bc39b92eb94011cddb4386834f83 Mon Sep 17 00:00:00 2001
From: qs3c <2749950753@qq.com>
Date: Thu, 5 Mar 2026 19:42:58 +0800
Subject: [PATCH 3/6] fix(openai_compat): handle html error bodies and reduce
allocations
---
pkg/providers/openai_compat/provider.go | 72 ++++++++++++++------
pkg/providers/openai_compat/provider_test.go | 32 +++++++++
2 files changed, 85 insertions(+), 19 deletions(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index e6ccbdefc..0422f0eb4 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"log"
@@ -189,7 +190,7 @@ func (p *Provider) Chat(
}
if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, string(body))
+ return nil, wrapHTTPResponseError(resp.StatusCode, body, resp.Header.Get("Content-Type"), p.apiBase)
}
out, err := parseResponse(body)
@@ -201,44 +202,77 @@ func (p *Provider) Chat(
}
func wrapResponseParseError(err error, body []byte, contentType, apiBase string) error {
- trimmedContentType := strings.TrimSpace(contentType)
- if looksLikeHTML(body, trimmedContentType) {
- contentTypeHint := ""
- if trimmedContentType != "" {
- contentTypeHint = fmt.Sprintf(" (content-type: %s)", trimmedContentType)
- }
- return fmt.Errorf(
- "expected JSON response from %s/chat/completions, but received HTML%s; check api_base or proxy configuration. Response preview: %s",
- apiBase,
- contentTypeHint,
- responsePreview(body, 160),
- )
+ if message, ok := htmlResponseMessage(body, contentType, apiBase); ok {
+ return errors.New(message)
}
return err
}
+func wrapHTTPResponseError(statusCode int, body []byte, contentType, apiBase string) error {
+ if message, ok := htmlResponseMessage(body, contentType, apiBase); ok {
+ return fmt.Errorf("API request failed:\n Status: %d\n Detail: %s", statusCode, message)
+ }
+ return fmt.Errorf("API request failed:\n Status: %d\n Body: %s", statusCode, string(body))
+}
+
+func htmlResponseMessage(body []byte, contentType, apiBase string) (string, bool) {
+ trimmedContentType := strings.TrimSpace(contentType)
+ if !looksLikeHTML(body, trimmedContentType) {
+ return "", false
+ }
+
+ contentTypeHint := ""
+ if trimmedContentType != "" {
+ contentTypeHint = fmt.Sprintf(" (content-type: %s)", trimmedContentType)
+ }
+
+ return fmt.Sprintf(
+ "expected JSON response from %s/chat/completions, but received HTML%s; check api_base or proxy configuration. Response preview: %s",
+ apiBase,
+ contentTypeHint,
+ responsePreview(body, 160),
+ ), true
+}
+
func looksLikeHTML(body []byte, contentType string) bool {
contentType = strings.ToLower(strings.TrimSpace(contentType))
if strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") {
return true
}
- trimmed := strings.ToLower(strings.TrimSpace(string(body)))
+ trimmed := strings.ToLower(string(leadingTrimmedPrefix(body, 128)))
return strings.HasPrefix(trimmed, " len(body) {
+ end = len(body)
+ }
+ return body[i:end]
+ }
+ }
+ return nil
+}
+
func responsePreview(body []byte, maxLen int) string {
- preview := strings.TrimSpace(string(body))
- if preview == "" {
+ trimmed := bytes.TrimSpace(body)
+ if len(trimmed) == 0 {
return ""
}
- if len(preview) <= maxLen {
- return preview
+ if len(trimmed) <= maxLen {
+ return string(trimmed)
}
- return preview[:maxLen] + "..."
+ return string(trimmed[:maxLen]) + "..."
}
func parseResponse(body []byte) (*LLMResponse, error) {
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 244a20672..899d10c8d 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -1,6 +1,7 @@
package openai_compat
import (
+ "bytes"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -233,6 +234,37 @@ func TestProviderChat_HTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
}
}
+func TestProviderChat_HTMLErrorResponseReturnsHelpfulError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusBadGateway)
+ _, _ = w.Write([]byte("bad gateway"))
+ }))
+ defer server.Close()
+
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "Status: 502") {
+ t.Fatalf("expected status code in error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "received HTML") {
+ t.Fatalf("expected helpful HTML error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
+ t.Fatalf("expected configuration hint, got %v", err)
+ }
+}
+
+func TestLooksLikeHTML_SniffsPrefixWithLargeBody(t *testing.T) {
+ body := append([]byte(" \r\n\tx"), bytes.Repeat([]byte("A"), 1024*1024)...)
+ if !looksLikeHTML(body, "") {
+ t.Fatal("expected looksLikeHTML to detect html prefix")
+ }
+}
+
func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testing.T) {
var requestBody map[string]any
From c1a3876f7de9251412ddc71a42530482d80334b1 Mon Sep 17 00:00:00 2001
From: amagi <2749950753@qq.com>
Date: Fri, 6 Mar 2026 01:51:24 +0800
Subject: [PATCH 4/6] fix: improve error handling for non-JSON responses by
checking content type and using a streaming JSON parser.
---
pkg/providers/openai_compat/provider.go | 89 ++++----------------
pkg/providers/openai_compat/provider_test.go | 46 +++++++++-
2 files changed, 58 insertions(+), 77 deletions(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 0422f0eb4..22d4da56c 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"io"
"log"
@@ -184,84 +183,28 @@ func (p *Provider) Chat(
}
defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response: %w", err)
+ contentType := resp.Header.Get("Content-Type")
+
+ // check if there is an HTTP error (caused by proxy or gateway) or if the response is HTML
+ if resp.StatusCode != http.StatusOK || strings.Contains(strings.ToLower(contentType), "text/html") {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
+ return nil, wrapHTTPResponseError(resp.StatusCode, body, contentType, p.apiBase)
}
- if resp.StatusCode != http.StatusOK {
- return nil, wrapHTTPResponseError(resp.StatusCode, body, resp.Header.Get("Content-Type"), p.apiBase)
- }
-
- out, err := parseResponse(body)
+ // directly pass the stream (resp.Body) to the JSON parser without loading everything into memory
+ out, err := parseResponse(resp.Body)
if err != nil {
- return nil, wrapResponseParseError(err, body, resp.Header.Get("Content-Type"), p.apiBase)
+ // Note: if it fails here, we do not have the full body in memory for HTML inspection,
+ // but having already checked the Content-Type above, the error is genuinely related to JSON parsing.
+ return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
return out, nil
}
-func wrapResponseParseError(err error, body []byte, contentType, apiBase string) error {
- if message, ok := htmlResponseMessage(body, contentType, apiBase); ok {
- return errors.New(message)
- }
- return err
-}
-
func wrapHTTPResponseError(statusCode int, body []byte, contentType, apiBase string) error {
- if message, ok := htmlResponseMessage(body, contentType, apiBase); ok {
- return fmt.Errorf("API request failed:\n Status: %d\n Detail: %s", statusCode, message)
- }
- return fmt.Errorf("API request failed:\n Status: %d\n Body: %s", statusCode, string(body))
-}
-
-func htmlResponseMessage(body []byte, contentType, apiBase string) (string, bool) {
- trimmedContentType := strings.TrimSpace(contentType)
- if !looksLikeHTML(body, trimmedContentType) {
- return "", false
- }
-
- contentTypeHint := ""
- if trimmedContentType != "" {
- contentTypeHint = fmt.Sprintf(" (content-type: %s)", trimmedContentType)
- }
-
- return fmt.Sprintf(
- "expected JSON response from %s/chat/completions, but received HTML%s; check api_base or proxy configuration. Response preview: %s",
- apiBase,
- contentTypeHint,
- responsePreview(body, 160),
- ), true
-}
-
-func looksLikeHTML(body []byte, contentType string) bool {
- contentType = strings.ToLower(strings.TrimSpace(contentType))
- if strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") {
- return true
- }
-
- trimmed := strings.ToLower(string(leadingTrimmedPrefix(body, 128)))
- return strings.HasPrefix(trimmed, " len(body) {
- end = len(body)
- }
- return body[i:end]
- }
- }
- return nil
+ respPreview := responsePreview(body, 128)
+ return fmt.Errorf("API request failed: %s returned HTML instead of JSON (content-type: %s); check api_base or proxy configuration.\n Status: %d\n Body: %s", apiBase, contentType, statusCode, respPreview)
}
func responsePreview(body []byte, maxLen int) string {
@@ -275,7 +218,7 @@ func responsePreview(body []byte, maxLen int) string {
return string(trimmed[:maxLen]) + "..."
}
-func parseResponse(body []byte) (*LLMResponse, error) {
+func parseResponse(body io.Reader) (*LLMResponse, error) {
var apiResponse struct {
Choices []struct {
Message struct {
@@ -302,8 +245,8 @@ func parseResponse(body []byte) (*LLMResponse, error) {
Usage *UsageInfo `json:"usage"`
}
- if err := json.Unmarshal(body, &apiResponse); err != nil {
- return nil, fmt.Errorf("failed to unmarshal response: %w", err)
+ if err := json.NewDecoder(body).Decode(&apiResponse); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(apiResponse.Choices) == 0 {
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 899d10c8d..84e6bbe3e 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -258,10 +258,48 @@ func TestProviderChat_HTMLErrorResponseReturnsHelpfulError(t *testing.T) {
}
}
-func TestLooksLikeHTML_SniffsPrefixWithLargeBody(t *testing.T) {
- body := append([]byte(" \r\n\tx"), bytes.Repeat([]byte("A"), 1024*1024)...)
- if !looksLikeHTML(body, "") {
- t.Fatal("expected looksLikeHTML to detect html prefix")
+func TestProviderChat_MislabeledHTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(" \r\n\tgateway login"))
+ }))
+ defer server.Close()
+
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "received HTML") {
+ t.Fatalf("expected helpful HTML error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
+ t.Fatalf("expected configuration hint, got %v", err)
+ }
+}
+
+func TestProviderChat_LargeHTMLResponsePreviewIsTruncated(t *testing.T) {
+ body := append([]byte(""), bytes.Repeat([]byte("A"), 2048)...)
+ body = append(body, []byte("")...)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusBadGateway)
+ _, _ = w.Write(body)
+ }))
+ defer server.Close()
+
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "Response preview: ") {
+ t.Fatalf("expected html preview in error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "...") {
+ t.Fatalf("expected truncated preview, got %v", err)
}
}
From 6eaa49f7ab1f488ba8b3df39539cb191339b0799 Mon Sep 17 00:00:00 2001
From: amagi <2749950753@qq.com>
Date: Sat, 7 Mar 2026 15:50:08 +0800
Subject: [PATCH 5/6] fix: improve openai compat HTML response handling
---
pkg/providers/openai_compat/provider.go | 61 +++++++++++--
pkg/providers/openai_compat/provider_test.go | 91 +++++++++++++++++++-
2 files changed, 139 insertions(+), 13 deletions(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 22d4da56c..6b8f0181d 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -1,6 +1,7 @@
package openai_compat
import (
+ "bufio"
"bytes"
"context"
"encoding/json"
@@ -185,28 +186,70 @@ func (p *Provider) Chat(
contentType := resp.Header.Get("Content-Type")
- // check if there is an HTTP error (caused by proxy or gateway) or if the response is HTML
- if resp.StatusCode != http.StatusOK || strings.Contains(strings.ToLower(contentType), "text/html") {
- body, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
- return nil, wrapHTTPResponseError(resp.StatusCode, body, contentType, p.apiBase)
+ // Non-200: read a prefix to tell HTML error page apart from JSON error body.
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+ if looksLikeHTML(body, contentType) {
+ return nil, wrapHTMLResponseError(resp.StatusCode, body, contentType, p.apiBase)
+ }
+ return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, responsePreview(body, 128))
}
- // directly pass the stream (resp.Body) to the JSON parser without loading everything into memory
- out, err := parseResponse(resp.Body)
+ // Peek without consuming so the full stream reaches the JSON decoder.
+ reader := bufio.NewReader(resp.Body)
+ prefix, err := reader.Peek(256) // io.EOF/ErrBufferFull are normal; only real errors abort
+ if err != nil && err != io.EOF && err != bufio.ErrBufferFull {
+ return nil, fmt.Errorf("failed to inspect response: %w", err)
+ }
+ if looksLikeHTML(prefix, contentType) {
+ return nil, wrapHTMLResponseError(resp.StatusCode, prefix, contentType, p.apiBase)
+ }
+
+ out, err := parseResponse(reader)
if err != nil {
- // Note: if it fails here, we do not have the full body in memory for HTML inspection,
- // but having already checked the Content-Type above, the error is genuinely related to JSON parsing.
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
return out, nil
}
-func wrapHTTPResponseError(statusCode int, body []byte, contentType, apiBase string) error {
+func wrapHTMLResponseError(statusCode int, body []byte, contentType, apiBase string) error {
respPreview := responsePreview(body, 128)
return fmt.Errorf("API request failed: %s returned HTML instead of JSON (content-type: %s); check api_base or proxy configuration.\n Status: %d\n Body: %s", apiBase, contentType, statusCode, respPreview)
}
+func looksLikeHTML(body []byte, contentType string) bool {
+ contentType = strings.ToLower(strings.TrimSpace(contentType))
+ if strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") {
+ return true
+ }
+ prefix := bytes.ToLower(leadingTrimmedPrefix(body, 128))
+ return bytes.HasPrefix(prefix, []byte(" len(body) {
+ end = len(body)
+ }
+ return body[i:end]
+ }
+ }
+ return nil
+}
+
func responsePreview(body []byte, maxLen int) string {
trimmed := bytes.TrimSpace(body)
if len(trimmed) == 0 {
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 84e6bbe3e..c729289d4 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -3,6 +3,7 @@ package openai_compat
import (
"bytes"
"encoding/json"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
@@ -213,6 +214,27 @@ func TestProviderChat_HTTPError(t *testing.T) {
}
}
+func TestProviderChat_JSONHTTPErrorDoesNotReportHTML(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"bad request"}`))
+ }))
+ defer server.Close()
+
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "Status: 400") {
+ t.Fatalf("expected status code in error, got %v", err)
+ }
+ if strings.Contains(err.Error(), "returned HTML instead of JSON") {
+ t.Fatalf("expected non-HTML http error, got %v", err)
+ }
+}
+
func TestProviderChat_HTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -226,7 +248,7 @@ func TestProviderChat_HTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
if err == nil {
t.Fatal("expected error, got nil")
}
- if !strings.Contains(err.Error(), "received HTML") {
+ if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
t.Fatalf("expected helpful HTML error, got %v", err)
}
if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
@@ -250,7 +272,7 @@ func TestProviderChat_HTMLErrorResponseReturnsHelpfulError(t *testing.T) {
if !strings.Contains(err.Error(), "Status: 502") {
t.Fatalf("expected status code in error, got %v", err)
}
- if !strings.Contains(err.Error(), "received HTML") {
+ if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
t.Fatalf("expected helpful HTML error, got %v", err)
}
if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
@@ -271,7 +293,7 @@ func TestProviderChat_MislabeledHTMLSuccessResponseReturnsHelpfulError(t *testin
if err == nil {
t.Fatal("expected error, got nil")
}
- if !strings.Contains(err.Error(), "received HTML") {
+ if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
t.Fatalf("expected helpful HTML error, got %v", err)
}
if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
@@ -279,6 +301,33 @@ func TestProviderChat_MislabeledHTMLSuccessResponseReturnsHelpfulError(t *testin
}
}
+func TestProviderChat_SuccessResponseUsesStreamingDecoder(t *testing.T) {
+ content := strings.Repeat("a", 1024)
+ body := `{"choices":[{"message":{"content":"` + content + `"},"finish_reason":"stop"}]}`
+
+ p := NewProvider("key", "https://example.com/v1", "")
+ p.httpClient = &http.Client{
+ Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Header: http.Header{"Content-Type": []string{"application/json"}},
+ Body: &errAfterDataReadCloser{
+ data: []byte(body),
+ chunkSize: 64,
+ },
+ }, nil
+ }),
+ }
+
+ out, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err != nil {
+ t.Fatalf("Chat() error = %v", err)
+ }
+ if out.Content != content {
+ t.Fatalf("Content = %q, want %q", out.Content, content)
+ }
+}
+
func TestProviderChat_LargeHTMLResponsePreviewIsTruncated(t *testing.T) {
body := append([]byte(""), bytes.Repeat([]byte("A"), 2048)...)
body = append(body, []byte("")...)
@@ -295,7 +344,7 @@ func TestProviderChat_LargeHTMLResponsePreviewIsTruncated(t *testing.T) {
if err == nil {
t.Fatal("expected error, got nil")
}
- if !strings.Contains(err.Error(), "Response preview: ") {
+ if !strings.Contains(err.Error(), "Body: ") {
t.Fatalf("expected html preview in error, got %v", err)
}
if !strings.Contains(err.Error(), "...") {
@@ -490,6 +539,40 @@ func TestProvider_RequestTimeoutOverride(t *testing.T) {
}
}
+type roundTripperFunc func(*http.Request) (*http.Response, error)
+
+func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
+ return f(r)
+}
+
+type errAfterDataReadCloser struct {
+ data []byte
+ chunkSize int
+ offset int
+}
+
+func (r *errAfterDataReadCloser) Read(p []byte) (int, error) {
+ if r.offset >= len(r.data) {
+ return 0, io.ErrUnexpectedEOF
+ }
+
+ n := r.chunkSize
+ if n <= 0 || n > len(p) {
+ n = len(p)
+ }
+ remaining := len(r.data) - r.offset
+ if n > remaining {
+ n = remaining
+ }
+ copy(p, r.data[r.offset:r.offset+n])
+ r.offset += n
+ return n, nil
+}
+
+func (r *errAfterDataReadCloser) Close() error {
+ return nil
+}
+
func TestProvider_FunctionalOptionMaxTokensField(t *testing.T) {
p := NewProvider("key", "https://example.com/v1", "", WithMaxTokensField("max_completion_tokens"))
if p.maxTokensField != "max_completion_tokens" {
From 53cba73283e53e1bf6933cf01a42de3b696bc298 Mon Sep 17 00:00:00 2001
From: amagi <2749950753@qq.com>
Date: Sat, 7 Mar 2026 16:12:23 +0800
Subject: [PATCH 6/6] fix: resolve openai compat lint issues
---
pkg/providers/openai_compat/provider.go | 20 +++-
pkg/providers/openai_compat/provider_test.go | 111 +++++++++----------
2 files changed, 65 insertions(+), 66 deletions(-)
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 6b8f0181d..83966180a 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -188,14 +188,18 @@ func (p *Provider) Chat(
// Non-200: read a prefix to tell HTML error page apart from JSON error body.
if resp.StatusCode != http.StatusOK {
- body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
- if err != nil {
- return nil, fmt.Errorf("failed to read response: %w", err)
+ body, readErr := io.ReadAll(io.LimitReader(resp.Body, 256))
+ if readErr != nil {
+ return nil, fmt.Errorf("failed to read response: %w", readErr)
}
if looksLikeHTML(body, contentType) {
return nil, wrapHTMLResponseError(resp.StatusCode, body, contentType, p.apiBase)
}
- return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, responsePreview(body, 128))
+ return nil, fmt.Errorf(
+ "API request failed:\n Status: %d\n Body: %s",
+ resp.StatusCode,
+ responsePreview(body, 128),
+ )
}
// Peek without consuming so the full stream reaches the JSON decoder.
@@ -218,7 +222,13 @@ func (p *Provider) Chat(
func wrapHTMLResponseError(statusCode int, body []byte, contentType, apiBase string) error {
respPreview := responsePreview(body, 128)
- return fmt.Errorf("API request failed: %s returned HTML instead of JSON (content-type: %s); check api_base or proxy configuration.\n Status: %d\n Body: %s", apiBase, contentType, statusCode, respPreview)
+ return fmt.Errorf(
+ "API request failed: %s returned HTML instead of JSON (content-type: %s); check api_base or proxy configuration.\n Status: %d\n Body: %s",
+ apiBase,
+ contentType,
+ statusCode,
+ respPreview,
+ )
}
func looksLikeHTML(body []byte, contentType string) bool {
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index c729289d4..5c4dcd1b0 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -3,6 +3,7 @@ package openai_compat
import (
"bytes"
"encoding/json"
+ "fmt"
"io"
"net/http"
"net/http/httptest"
@@ -235,69 +236,57 @@ func TestProviderChat_JSONHTTPErrorDoesNotReportHTML(t *testing.T) {
}
}
-func TestProviderChat_HTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte("gateway login"))
- }))
- defer server.Close()
+func TestProviderChat_HTMLResponsesReturnHelpfulError(t *testing.T) {
+ tests := []struct {
+ name string
+ contentType string
+ statusCode int
+ body string
+ }{
+ {
+ name: "html success response",
+ contentType: "text/html; charset=utf-8",
+ statusCode: http.StatusOK,
+ body: "gateway login",
+ },
+ {
+ name: "html error response",
+ contentType: "text/html; charset=utf-8",
+ statusCode: http.StatusBadGateway,
+ body: "bad gateway",
+ },
+ {
+ name: "mislabeled html success response",
+ contentType: "application/json",
+ statusCode: http.StatusOK,
+ body: " \r\n\tgateway login",
+ },
+ }
- p := NewProvider("key", server.URL, "")
- _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
- if err == nil {
- t.Fatal("expected error, got nil")
- }
- if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
- t.Fatalf("expected helpful HTML error, got %v", err)
- }
- if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
- t.Fatalf("expected configuration hint, got %v", err)
- }
-}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", tt.contentType)
+ w.WriteHeader(tt.statusCode)
+ _, _ = w.Write([]byte(tt.body))
+ }))
+ defer server.Close()
-func TestProviderChat_HTMLErrorResponseReturnsHelpfulError(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.WriteHeader(http.StatusBadGateway)
- _, _ = w.Write([]byte("bad gateway"))
- }))
- defer server.Close()
-
- p := NewProvider("key", server.URL, "")
- _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
- if err == nil {
- t.Fatal("expected error, got nil")
- }
- if !strings.Contains(err.Error(), "Status: 502") {
- t.Fatalf("expected status code in error, got %v", err)
- }
- if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
- t.Fatalf("expected helpful HTML error, got %v", err)
- }
- if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
- t.Fatalf("expected configuration hint, got %v", err)
- }
-}
-
-func TestProviderChat_MislabeledHTMLSuccessResponseReturnsHelpfulError(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(" \r\n\tgateway login"))
- }))
- defer server.Close()
-
- p := NewProvider("key", server.URL, "")
- _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
- if err == nil {
- t.Fatal("expected error, got nil")
- }
- if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
- t.Fatalf("expected helpful HTML error, got %v", err)
- }
- if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
- t.Fatalf("expected configuration hint, got %v", err)
+ p := NewProvider("key", server.URL, "")
+ _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), fmt.Sprintf("Status: %d", tt.statusCode)) {
+ t.Fatalf("expected status code in error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "returned HTML instead of JSON") {
+ t.Fatalf("expected helpful HTML error, got %v", err)
+ }
+ if !strings.Contains(err.Error(), "check api_base or proxy configuration") {
+ t.Fatalf("expected configuration hint, got %v", err)
+ }
+ })
}
}