Archived
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:
1 parent
8ee5e98ea3
commit
f8acf31ebc
71 files changed
+15727
-1
No files matched your search
@@ -0,0 +1,106 @@
|
||||
package chatcmpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpstreamError is returned when the upstream provider answers with a
|
||||
// non-2xx status or an error payload.
|
||||
type UpstreamError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *UpstreamError) Error() string {
|
||||
return fmt.Sprintf("upstream %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// Client talks to one Chat Completions endpoint.
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewClient builds a client for the given base URL and API key.
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"),
|
||||
APIKey: apiKey,
|
||||
HTTP: &http.Client{Timeout: 30 * time.Minute},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) endpoint() string {
|
||||
return c.BaseURL + "/chat/completions"
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, req *Request) (*http.Response, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
if c.APIKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
resp, err := c.HTTP.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
defer resp.Body.Close()
|
||||
msg := readErrorBody(resp)
|
||||
return nil, &UpstreamError{StatusCode: resp.StatusCode, Message: msg}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Create performs a non-streaming completion.
|
||||
func (c *Client) Create(ctx context.Context, req *Request) (*Response, error) {
|
||||
resp, err := c.do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out Response
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<20)).Decode(&out); err != nil {
|
||||
return nil, fmt.Errorf("decode upstream response: %w", err)
|
||||
}
|
||||
if out.Error != nil && out.Error.Message != "" {
|
||||
return nil, &UpstreamError{StatusCode: resp.StatusCode, Message: out.Error.Message}
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateStream performs a streaming completion. The caller reads SSE
|
||||
// events from the response body and must close it.
|
||||
func (c *Client) CreateStream(ctx context.Context, req *Request) (*http.Response, error) {
|
||||
return c.do(ctx, req)
|
||||
}
|
||||
|
||||
func readErrorBody(resp *http.Response) string {
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
if err != nil || len(data) == 0 {
|
||||
return resp.Status
|
||||
}
|
||||
var payload struct {
|
||||
Error *Error `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(data, &payload) == nil && payload.Error != nil && payload.Error.Message != "" {
|
||||
return payload.Error.Message
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
Reference in new issue
Block a user