147 lines
4.0 KiB
Go
147 lines
4.0 KiB
Go
// Package llm contains impure LLM provider implementations.
|
|
package llm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
|
|
coretypes "github.com/enmanuel/agents/pkg/llm"
|
|
)
|
|
|
|
const anthropicAPIBase = "https://api.anthropic.com/v1"
|
|
const anthropicVersion = "2023-06-01"
|
|
|
|
// NewAnthropicComplete returns a CompleteFunc backed by the Anthropic API.
|
|
func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
|
if baseURL == "" {
|
|
baseURL = anthropicAPIBase
|
|
}
|
|
|
|
return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
|
|
apiKey := os.Getenv(apiKeyEnv)
|
|
if apiKey == "" {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("env var %s is not set", apiKeyEnv)
|
|
}
|
|
|
|
body := toAnthropicRequest(req)
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/messages", bytes.NewReader(raw))
|
|
if err != nil {
|
|
return coretypes.CompletionResponse{}, err
|
|
}
|
|
httpReq.Header.Set("x-api-key", apiKey)
|
|
httpReq.Header.Set("anthropic-version", anthropicVersion)
|
|
httpReq.Header.Set("content-type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(httpReq)
|
|
if err != nil {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("anthropic request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("read response: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("anthropic error %d: %s", resp.StatusCode, respBytes)
|
|
}
|
|
|
|
return fromAnthropicResponse(respBytes)
|
|
}
|
|
}
|
|
|
|
// ── private conversion helpers ────────────────────────────────────────────
|
|
|
|
type anthropicRequest struct {
|
|
Model string `json:"model"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
System string `json:"system,omitempty"`
|
|
Messages []anthropicMessage `json:"messages"`
|
|
Tools []anthropicTool `json:"tools,omitempty"`
|
|
}
|
|
|
|
type anthropicMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type anthropicTool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
InputSchema map[string]any `json:"input_schema"`
|
|
}
|
|
|
|
type anthropicResponse struct {
|
|
Content []struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
} `json:"content"`
|
|
Usage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
} `json:"usage"`
|
|
StopReason string `json:"stop_reason"`
|
|
}
|
|
|
|
func toAnthropicRequest(req coretypes.CompletionRequest) anthropicRequest {
|
|
msgs := make([]anthropicMessage, 0, len(req.Messages))
|
|
for _, m := range req.Messages {
|
|
if m.Role == coretypes.RoleSystem {
|
|
continue // handled as top-level system param
|
|
}
|
|
msgs = append(msgs, anthropicMessage{
|
|
Role: string(m.Role),
|
|
Content: m.Content,
|
|
})
|
|
}
|
|
|
|
tools := make([]anthropicTool, len(req.Tools))
|
|
for i, t := range req.Tools {
|
|
tools[i] = anthropicTool{
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
InputSchema: t.InputSchema,
|
|
}
|
|
}
|
|
|
|
return anthropicRequest{
|
|
Model: req.Model,
|
|
MaxTokens: req.MaxTokens,
|
|
System: req.SystemPrompt,
|
|
Messages: msgs,
|
|
Tools: tools,
|
|
}
|
|
}
|
|
|
|
func fromAnthropicResponse(raw []byte) (coretypes.CompletionResponse, error) {
|
|
var ar anthropicResponse
|
|
if err := json.Unmarshal(raw, &ar); err != nil {
|
|
return coretypes.CompletionResponse{}, fmt.Errorf("unmarshal response: %w", err)
|
|
}
|
|
var content string
|
|
for _, c := range ar.Content {
|
|
if c.Type == "text" {
|
|
content += c.Text
|
|
}
|
|
}
|
|
return coretypes.CompletionResponse{
|
|
Content: content,
|
|
FinishReason: ar.StopReason,
|
|
Usage: coretypes.TokenUsage{
|
|
InputTokens: ar.Usage.InputTokens,
|
|
OutputTokens: ar.Usage.OutputTokens,
|
|
TotalTokens: ar.Usage.InputTokens + ar.Usage.OutputTokens,
|
|
},
|
|
}, nil
|
|
}
|