241 lines
6.5 KiB
Go
241 lines
6.5 KiB
Go
package anthropicprovider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/anthropics/anthropic-sdk-go"
|
|
"github.com/anthropics/anthropic-sdk-go/option"
|
|
)
|
|
|
|
type ToolCall struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type,omitempty"`
|
|
Function *FunctionCall `json:"function,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
|
}
|
|
|
|
type FunctionCall struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}
|
|
|
|
type LLMResponse struct {
|
|
Content string `json:"content"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
FinishReason string `json:"finish_reason"`
|
|
Usage *UsageInfo `json:"usage,omitempty"`
|
|
}
|
|
|
|
type UsageInfo struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
}
|
|
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
|
}
|
|
|
|
type ToolDefinition struct {
|
|
Type string `json:"type"`
|
|
Function ToolFunctionDefinition `json:"function"`
|
|
}
|
|
|
|
type ToolFunctionDefinition struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Parameters map[string]interface{} `json:"parameters"`
|
|
}
|
|
|
|
type Provider struct {
|
|
client *anthropic.Client
|
|
tokenSource func() (string, error)
|
|
}
|
|
|
|
func NewProvider(token string) *Provider {
|
|
client := anthropic.NewClient(
|
|
option.WithAuthToken(token),
|
|
option.WithBaseURL("https://api.anthropic.com"),
|
|
)
|
|
return &Provider{client: &client}
|
|
}
|
|
|
|
func NewProviderWithClient(client *anthropic.Client) *Provider {
|
|
return &Provider{client: client}
|
|
}
|
|
|
|
func NewProviderWithTokenSource(token string, tokenSource func() (string, error)) *Provider {
|
|
p := NewProvider(token)
|
|
p.tokenSource = tokenSource
|
|
return p
|
|
}
|
|
|
|
func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
|
var opts []option.RequestOption
|
|
if p.tokenSource != nil {
|
|
tok, err := p.tokenSource()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("refreshing token: %w", err)
|
|
}
|
|
opts = append(opts, option.WithAuthToken(tok))
|
|
}
|
|
|
|
params, err := buildParams(messages, tools, model, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := p.client.Messages.New(ctx, params, opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("claude API call: %w", err)
|
|
}
|
|
|
|
return parseResponse(resp), nil
|
|
}
|
|
|
|
func (p *Provider) GetDefaultModel() string {
|
|
return "claude-sonnet-4-5-20250929"
|
|
}
|
|
|
|
func buildParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (anthropic.MessageNewParams, error) {
|
|
var system []anthropic.TextBlockParam
|
|
var anthropicMessages []anthropic.MessageParam
|
|
|
|
for _, msg := range messages {
|
|
switch msg.Role {
|
|
case "system":
|
|
system = append(system, anthropic.TextBlockParam{Text: msg.Content})
|
|
case "user":
|
|
if msg.ToolCallID != "" {
|
|
anthropicMessages = append(anthropicMessages,
|
|
anthropic.NewUserMessage(anthropic.NewToolResultBlock(msg.ToolCallID, msg.Content, false)),
|
|
)
|
|
} else {
|
|
anthropicMessages = append(anthropicMessages,
|
|
anthropic.NewUserMessage(anthropic.NewTextBlock(msg.Content)),
|
|
)
|
|
}
|
|
case "assistant":
|
|
if len(msg.ToolCalls) > 0 {
|
|
var blocks []anthropic.ContentBlockParamUnion
|
|
if msg.Content != "" {
|
|
blocks = append(blocks, anthropic.NewTextBlock(msg.Content))
|
|
}
|
|
for _, tc := range msg.ToolCalls {
|
|
blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, tc.Arguments, tc.Name))
|
|
}
|
|
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
|
|
} else {
|
|
anthropicMessages = append(anthropicMessages,
|
|
anthropic.NewAssistantMessage(anthropic.NewTextBlock(msg.Content)),
|
|
)
|
|
}
|
|
case "tool":
|
|
anthropicMessages = append(anthropicMessages,
|
|
anthropic.NewUserMessage(anthropic.NewToolResultBlock(msg.ToolCallID, msg.Content, false)),
|
|
)
|
|
}
|
|
}
|
|
|
|
maxTokens := int64(4096)
|
|
if mt, ok := options["max_tokens"].(int); ok {
|
|
maxTokens = int64(mt)
|
|
}
|
|
|
|
params := anthropic.MessageNewParams{
|
|
Model: anthropic.Model(model),
|
|
Messages: anthropicMessages,
|
|
MaxTokens: maxTokens,
|
|
}
|
|
|
|
if len(system) > 0 {
|
|
params.System = system
|
|
}
|
|
|
|
if temp, ok := options["temperature"].(float64); ok {
|
|
params.Temperature = anthropic.Float(temp)
|
|
}
|
|
|
|
if len(tools) > 0 {
|
|
params.Tools = translateTools(tools)
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
|
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
|
for _, t := range tools {
|
|
tool := anthropic.ToolParam{
|
|
Name: t.Function.Name,
|
|
InputSchema: anthropic.ToolInputSchemaParam{
|
|
Properties: t.Function.Parameters["properties"],
|
|
},
|
|
}
|
|
if desc := t.Function.Description; desc != "" {
|
|
tool.Description = anthropic.String(desc)
|
|
}
|
|
if req, ok := t.Function.Parameters["required"].([]interface{}); ok {
|
|
required := make([]string, 0, len(req))
|
|
for _, r := range req {
|
|
if s, ok := r.(string); ok {
|
|
required = append(required, s)
|
|
}
|
|
}
|
|
tool.InputSchema.Required = required
|
|
}
|
|
result = append(result, anthropic.ToolUnionParam{OfTool: &tool})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseResponse(resp *anthropic.Message) *LLMResponse {
|
|
var content string
|
|
var toolCalls []ToolCall
|
|
|
|
for _, block := range resp.Content {
|
|
switch block.Type {
|
|
case "text":
|
|
tb := block.AsText()
|
|
content += tb.Text
|
|
case "tool_use":
|
|
tu := block.AsToolUse()
|
|
var args map[string]interface{}
|
|
if err := json.Unmarshal(tu.Input, &args); err != nil {
|
|
args = map[string]interface{}{"raw": string(tu.Input)}
|
|
}
|
|
toolCalls = append(toolCalls, ToolCall{
|
|
ID: tu.ID,
|
|
Name: tu.Name,
|
|
Arguments: args,
|
|
})
|
|
}
|
|
}
|
|
|
|
finishReason := "stop"
|
|
switch resp.StopReason {
|
|
case anthropic.StopReasonToolUse:
|
|
finishReason = "tool_calls"
|
|
case anthropic.StopReasonMaxTokens:
|
|
finishReason = "length"
|
|
case anthropic.StopReasonEndTurn:
|
|
finishReason = "stop"
|
|
}
|
|
|
|
return &LLMResponse{
|
|
Content: content,
|
|
ToolCalls: toolCalls,
|
|
FinishReason: finishReason,
|
|
Usage: &UsageInfo{
|
|
PromptTokens: int(resp.Usage.InputTokens),
|
|
CompletionTokens: int(resp.Usage.OutputTokens),
|
|
TotalTokens: int(resp.Usage.InputTokens + resp.Usage.OutputTokens),
|
|
},
|
|
}
|
|
}
|