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:
parent
9a682c8524
commit
c65f357a6a
3 changed files with 113 additions and 2 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue