feat: add anthropic-messages protocol for native Anthropic API

- Add new 'anthropic-messages' protocol prefix to support Anthropic's native Messages API format (/v1/messages endpoint)
- Use anthropicprovider.NewProviderWithBaseURL() for native format requests
- Modify anthropicprovider to use streaming API (Messages.NewStreaming) for compatibility with services requiring streaming
- Aggregate streaming events into complete response for backward compatibility
- Update config.example.json with usage documentation
- Maintain full backward compatibility with existing 'anthropic' protocol (OpenAI-compatible /chat/completions format)

This change enables picoclaw to work with Anthropic-compatible APIs that only support the native Messages format, such as Weibo's GLM-4.7 service.

BREAKING-CHANGE: anthropicprovider now uses streaming API internally, but maintains the same interface (aggregates to complete response). This should not affect existing users.
This commit is contained in:
ZaneTung 2026-03-05 17:11:45 +08:00
parent 9a682c8524
commit c65f357a6a
3 changed files with 113 additions and 2 deletions

View file

@ -25,6 +25,13 @@
"api_base": "https://api.anthropic.com/v1",
"thinking_level": "high"
},
{
"model_name": "claude-native",
"model": "anthropic-messages/claude-sonnet-4.6",
"api_key": "sk-ant-your-key",
"api_base": "https://api.anthropic.com",
"thinking_level": "high"
},
{
"model_name": "gemini",
"model": "antigravity/gemini-2.0-flash",

View file

@ -88,12 +88,24 @@ func (p *Provider) Chat(
return nil, err
}
resp, err := p.client.Messages.New(ctx, params, opts...)
// 使用流式 API 以兼容要求流式请求的服务端
// 流式响应会被聚合为完整响应后返回
stream := p.client.Messages.NewStreaming(ctx, params, opts...)
defer stream.Close()
// 收集所有流式事件以便后续处理
var events []anthropic.MessageStreamEventUnion
for stream.Next() {
events = append(events, stream.Current())
}
err = stream.Err()
if err != nil {
return nil, fmt.Errorf("claude API call: %w", err)
}
return parseResponse(resp), nil
// 从收集的事件中提取完整的响应
return parseStreamingEvents(events), nil
}
func (p *Provider) GetDefaultModel() string {
@ -336,6 +348,74 @@ func parseResponse(resp *anthropic.Message) *LLMResponse {
}
}
// parseStreamingEvents 从流式事件中提取完整的 Message 对象
func parseStreamingEvents(events []anthropic.MessageStreamEventUnion) *LLMResponse {
var content strings.Builder
var reasoning strings.Builder
var toolCalls []ToolCall
var stopReason anthropic.StopReason
var usage anthropic.Usage
for _, evt := range events {
switch evt.Type {
case "message_start":
if msg := evt.AsMessageStart(); msg.Message.ID != "" {
usage = msg.Message.Usage
}
case "content_block_start":
block := evt.AsContentBlockStart()
switch block.ContentBlock.Type {
case "tool_use":
// 工具调用开始,在 delta 中处理
}
case "content_block_delta":
delta := evt.AsContentBlockDelta()
switch delta.Delta.Type {
case "thinking_delta":
reasoning.WriteString(delta.Delta.Thinking)
case "text_delta":
content.WriteString(delta.Delta.Text)
case "input_json_delta":
// 工具调用参数增量,需要累积
// TODO: 实现完整的工具调用支持
}
case "content_block_stop":
// 内容块结束
case "message_delta":
msgDelta := evt.AsMessageDelta()
stopReason = msgDelta.Delta.StopReason
// 更新 usage 字段
usage.OutputTokens = msgDelta.Usage.OutputTokens
case "message_stop":
// 消息完成
case "error":
log.Printf("anthropic: streaming error: %v", evt)
}
}
finishReason := "stop"
switch stopReason {
case anthropic.StopReasonToolUse:
finishReason = "tool_calls"
case anthropic.StopReasonMaxTokens:
finishReason = "length"
case anthropic.StopReasonEndTurn:
finishReason = "stop"
}
return &LLMResponse{
Content: content.String(),
Reasoning: reasoning.String(),
ToolCalls: toolCalls,
FinishReason: finishReason,
Usage: &UsageInfo{
PromptTokens: int(usage.InputTokens),
CompletionTokens: int(usage.OutputTokens),
TotalTokens: int(usage.InputTokens + usage.OutputTokens),
},
}
}
func normalizeBaseURL(apiBase string) string {
base := strings.TrimSpace(apiBase)
if base == "" {

View file

@ -10,6 +10,7 @@ import (
"strings"
"github.com/sipeed/picoclaw/pkg/config"
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
)
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
@ -136,6 +137,29 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
cfg.RequestTimeout,
), modelID, nil
case "anthropic-messages":
// Anthropic 原生 Messages API 格式 (使用 /v1/messages 端点)
// 适用于需要使用 Anthropic 原生 API 格式的服务
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
// 使用 OAuth 凭据(从 auth store 获取)
provider, err := createClaudeAuthProvider()
if err != nil {
return nil, "", err
}
return provider, modelID, nil
}
// 使用 API key
if cfg.APIKey == "" {
return nil, "", fmt.Errorf("api_key is required for anthropic-messages protocol (model: %s)", cfg.Model)
}
apiBase := cfg.APIBase
if apiBase == "" {
apiBase = "https://api.anthropic.com"
}
// 使用 anthropicprovider 包中的原生 Anthropic provider
provider := anthropicprovider.NewProviderWithBaseURL(cfg.APIKey, apiBase)
return provider, modelID, nil
case "antigravity":
return NewAntigravityProvider(), modelID, nil