fix(providers): improve error handling and add edge case tests
- fix ToolCalls nil vs empty slice issue to ensure consistent JSON serialization - add detailed HTTP error handling for common status codes (401, 429, 400, 404, 500, 503) - add edge case tests for buildRequestBody and parseResponseBody - clarify anthropic vs anthropic-messages protocol differences in docs
This commit is contained in:
parent
94c7d879f8
commit
a22e027905
4 changed files with 186 additions and 10 deletions
|
|
@ -1111,9 +1111,11 @@ For direct Anthropic API access or custom endpoints that only support Anthropic'
|
|||
```
|
||||
|
||||
> Use `anthropic-messages` protocol when:
|
||||
> - Connecting directly to Anthropic's API (fixes 404 errors with `/v1/messages` endpoint)
|
||||
> - Using custom endpoints that only support Anthropic's native format
|
||||
> - Avoiding OpenAI-compatible wrapper layers
|
||||
> - Using third-party proxies that only support Anthropic's native `/v1/messages` endpoint (not OpenAI-compatible `/v1/chat/completions`)
|
||||
> - Connecting to services like MiniMax, Synthetic that require Anthropic's native message format
|
||||
> - The existing `anthropic` protocol returns 404 errors (indicating the endpoint doesn't support OpenAI-compatible format)
|
||||
>
|
||||
> **Note:** The `anthropic` protocol uses OpenAI-compatible format (`/v1/chat/completions`), while `anthropic-messages` uses Anthropic's native format (`/v1/messages`). Choose based on your endpoint's supported format.
|
||||
|
||||
**Ollama (local)**
|
||||
|
||||
|
|
|
|||
|
|
@ -607,9 +607,11 @@ Agent 读取 HEARTBEAT.md
|
|||
```
|
||||
|
||||
> 使用 `anthropic-messages` 协议的场景:
|
||||
> - 直接连接 Anthropic API(修复 `/v1/messages` 端点的 404 错误)
|
||||
> - 使用仅支持 Anthropic 原生格式的自定义端点
|
||||
> - 避免 OpenAI 兼容包装层
|
||||
> - 使用仅支持 Anthropic 原生 `/v1/messages` 端点的第三方代理(不支持 OpenAI 兼容的 `/v1/chat/completions`)
|
||||
> - 连接到 MiniMax、Synthetic 等需要 Anthropic 原生消息格式的服务
|
||||
> - 现有的 `anthropic` 协议返回 404 错误(说明端点不支持 OpenAI 兼容格式)
|
||||
>
|
||||
> **注意:** `anthropic` 协议使用 OpenAI 兼容格式(`/v1/chat/completions`),而 `anthropic-messages` 使用 Anthropic 原生格式(`/v1/messages`)。请根据端点支持的格式选择。
|
||||
|
||||
**Ollama (本地)**
|
||||
|
||||
|
|
|
|||
|
|
@ -119,10 +119,25 @@ func (p *Provider) Chat(
|
|||
return nil, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
// Check for HTTP errors
|
||||
// Check for HTTP errors with detailed messages
|
||||
switch resp.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return nil, fmt.Errorf("authentication failed (401): check your API key")
|
||||
case http.StatusTooManyRequests:
|
||||
return nil, fmt.Errorf("rate limited (429): %s", string(body))
|
||||
case http.StatusBadRequest:
|
||||
return nil, fmt.Errorf("bad request (400): %s", string(body))
|
||||
case http.StatusNotFound:
|
||||
return nil, fmt.Errorf("endpoint not found (404): %s", string(body))
|
||||
case http.StatusInternalServerError:
|
||||
return nil, fmt.Errorf("internal server error (500): %s", string(body))
|
||||
case http.StatusServiceUnavailable:
|
||||
return nil, fmt.Errorf("service unavailable (503): %s", string(body))
|
||||
default:
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse response
|
||||
return parseResponseBody(body)
|
||||
|
|
@ -274,7 +289,7 @@ func parseResponseBody(body []byte) (*LLMResponse, error) {
|
|||
|
||||
// Extract content and tool calls
|
||||
var content strings.Builder
|
||||
var toolCalls []ToolCall
|
||||
toolCalls := make([]ToolCall, 0) // Initialize as empty slice (not nil) for consistent JSON serialization
|
||||
|
||||
for _, block := range resp.Content {
|
||||
switch block.Type {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -431,6 +432,162 @@ func TestGetDefaultModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBuildRequestBodyEdgeCases tests edge cases for buildRequestBody.
|
||||
func TestBuildRequestBodyEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
messages []Message
|
||||
tools []ToolDefinition
|
||||
model string
|
||||
options map[string]any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty message list",
|
||||
messages: []Message{},
|
||||
model: "test-model",
|
||||
options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "very long system message",
|
||||
messages: []Message{
|
||||
{Role: "system", Content: strings.Repeat("This is a very long system prompt. ", 1000)},
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
model: "test-model",
|
||||
options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple consecutive system messages",
|
||||
messages: []Message{
|
||||
{Role: "system", Content: "First system message"},
|
||||
{Role: "system", Content: "Second system message"},
|
||||
{Role: "system", Content: "Third system message"},
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
model: "test-model",
|
||||
options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "tool result without tool call",
|
||||
messages: []Message{
|
||||
{Role: "user", Content: "Use a tool"},
|
||||
{Role: "assistant", Content: "", ToolCalls: []ToolCall{
|
||||
{ID: "tool-1", Name: "test_tool", Arguments: map[string]any{"arg": "value"}},
|
||||
}},
|
||||
{Role: "user", ToolCallID: "tool-1", Content: "Tool result"},
|
||||
},
|
||||
model: "test-model",
|
||||
options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := buildRequestBody(tt.messages, tt.tools, tt.model, tt.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("buildRequestBody() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify basic structure
|
||||
if got == nil {
|
||||
t.Error("buildRequestBody() returned nil")
|
||||
return
|
||||
}
|
||||
if got["model"] != tt.model {
|
||||
t.Errorf("model = %v, want %v", got["model"], tt.model)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseResponseBodyEdgeCases tests edge cases for parseResponseBody.
|
||||
func TestParseResponseBodyEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body []byte
|
||||
wantErr bool
|
||||
check func(*testing.T, *LLMResponse)
|
||||
}{
|
||||
{
|
||||
name: "empty content blocks",
|
||||
body: []byte(`{
|
||||
"id": "msg-empty",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [],
|
||||
"stop_reason": "end_turn",
|
||||
"model": "test-model",
|
||||
"usage": {"input_tokens": 5, "output_tokens": 0}
|
||||
}`),
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, resp *LLMResponse) {
|
||||
if resp.Content != "" {
|
||||
t.Errorf("Content = %q, want empty string", resp.Content)
|
||||
}
|
||||
if len(resp.ToolCalls) != 0 {
|
||||
t.Errorf("ToolCalls length = %d, want 0", len(resp.ToolCalls))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple tool use blocks",
|
||||
body: []byte(`{
|
||||
"id": "msg-multi",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "tool-1", "name": "func1", "input": {"arg": "val1"}},
|
||||
{"type": "tool_use", "id": "tool-2", "name": "func2", "input": {"arg": "val2"}}
|
||||
],
|
||||
"stop_reason": "tool_use",
|
||||
"model": "test-model",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 20}
|
||||
}`),
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, resp *LLMResponse) {
|
||||
if len(resp.ToolCalls) != 2 {
|
||||
t.Errorf("ToolCalls length = %d, want 2", len(resp.ToolCalls))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "malformed JSON response",
|
||||
body: []byte(`{invalid json`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseResponseBody(tt.body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseResponseBody() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.check != nil && err == nil {
|
||||
tt.check(t, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProviderChatErrors tests error handling in Chat.
|
||||
// Note: apiBase check removed as it's dead code - normalizeBaseURL() always provides a default.
|
||||
func TestProviderChatErrors(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue