CrossRelay stage 0+1: Go gateway with Anthropic<->Chat Completions relay, Tauri shell, bilingual management UI

- 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
This commit is contained in:
kate522 committed 2026-10-18 17:19:13 +08:00
1 parent 8ee5e98ea3
commit f8acf31ebc
71 files changed
+15727 -1

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
// 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
}