- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens, management API /api/* (providers CRUD, apply/restore, settings, backups, logs) - Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming state machine keyed by content block index (text, tool_use, parallel calls) - ~/.claude/settings.json guarded read/write: timestamped backups, managed env keys only, one-click restore - Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout, stdin pipe watchdog prevents orphans, kills the child on exit - React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query - Tests: Go table-driven + httptest integration (all green), vitest + RTL
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
// Package anthropic describes the Anthropic Messages API that Claude
|
|
// Code speaks and converts it to and from Chat Completions.
|
|
package anthropic
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
)
|
|
|
|
// MessagesRequest is the Anthropic Messages API request body.
|
|
type MessagesRequest struct {
|
|
Model string `json:"model"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
Messages []Message `json:"messages"`
|
|
System json.RawMessage `json:"system,omitempty"`
|
|
Tools []Tool `json:"tools,omitempty"`
|
|
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
TopK *int `json:"top_k,omitempty"`
|
|
StopSequences []string `json:"stop_sequences,omitempty"`
|
|
Stream bool `json:"stream,omitempty"`
|
|
}
|
|
|
|
// Message is one conversation turn. Content is either a plain string or
|
|
// an array of blocks.
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content json.RawMessage `json:"content"`
|
|
}
|
|
|
|
// Tool is an Anthropic tool definition.
|
|
type Tool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
InputSchema json.RawMessage `json:"input_schema,omitempty"`
|
|
}
|
|
|
|
// Block is a single content block inside a message. Only the fields
|
|
// belonging to Type are populated.
|
|
type Block struct {
|
|
Type string `json:"type"`
|
|
// text
|
|
Text string `json:"text,omitempty"`
|
|
// image
|
|
Source *ImageSource `json:"source,omitempty"`
|
|
// tool_use
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Input json.RawMessage `json:"input,omitempty"`
|
|
// tool_result
|
|
ToolUseID string `json:"tool_use_id,omitempty"`
|
|
Content json.RawMessage `json:"content,omitempty"`
|
|
IsError bool `json:"is_error,omitempty"`
|
|
// cache_control is accepted but never forwarded upstream.
|
|
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
|
}
|
|
|
|
// ImageSource is an image referenced by a user message.
|
|
type ImageSource struct {
|
|
Type string `json:"type"`
|
|
MediaType string `json:"media_type,omitempty"`
|
|
Data string `json:"data,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
// MessagesResponse is the Anthropic Messages API response body.
|
|
type MessagesResponse struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Role string `json:"role"`
|
|
Content []Block `json:"content"`
|
|
Model string `json:"model"`
|
|
StopReason string `json:"stop_reason"`
|
|
StopSequence *string `json:"stop_sequence"`
|
|
Usage Usage `json:"usage"`
|
|
}
|
|
|
|
// Usage reports token counts.
|
|
type Usage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
}
|
|
|
|
// ErrorResponse is the Anthropic error envelope.
|
|
type ErrorResponse struct {
|
|
Type string `json:"type"`
|
|
Error Detail `json:"error"`
|
|
}
|
|
|
|
// Detail is the error payload.
|
|
type Detail struct {
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// CountTokensResponse is the response of /v1/messages/count_tokens.
|
|
type CountTokensResponse struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
}
|
|
|
|
// ParseContent decodes a message content field into blocks. A plain
|
|
// string becomes a single text block.
|
|
func ParseContent(raw json.RawMessage) ([]Block, error) {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) == 0 || string(trimmed) == "null" {
|
|
return nil, nil
|
|
}
|
|
if trimmed[0] == '"' {
|
|
var s string
|
|
if err := json.Unmarshal(trimmed, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
return []Block{{Type: "text", Text: s}}, nil
|
|
}
|
|
var blocks []Block
|
|
if err := json.Unmarshal(trimmed, &blocks); err != nil {
|
|
return nil, err
|
|
}
|
|
return blocks, nil
|
|
}
|
|
|
|
// SystemText flattens a system field (string or block array) into plain
|
|
// text.
|
|
func SystemText(raw json.RawMessage) string {
|
|
blocks, err := ParseContent(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var parts []string
|
|
for _, b := range blocks {
|
|
if b.Type == "text" && b.Text != "" {
|
|
parts = append(parts, b.Text)
|
|
}
|
|
}
|
|
return joinBlocks(parts)
|
|
}
|
|
|
|
// TextOf extracts plain text from a tool_result content field.
|
|
func TextOf(raw json.RawMessage) string {
|
|
blocks, err := ParseContent(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var sb bytes.Buffer
|
|
for _, b := range blocks {
|
|
if b.Type == "text" {
|
|
sb.WriteString(b.Text)
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func joinBlocks(parts []string) string {
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
out := parts[0]
|
|
for _, p := range parts[1:] {
|
|
out += "\n\n" + p
|
|
}
|
|
return out
|
|
}
|