feat(server): API relay gateway backend M0-M4

Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with
quotas, proxy gateway with weighted channel failover and health checks,
usage/billing ledger, cross-protocol conversion (Anthropic Messages /
OpenAI Chat Completions / OpenAI Responses), and channel/model admin API.
Channels declare native API formats and auto-convert the rest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 21:05:02 +08:00
co-authored by Claude Sonnet 5
parent b0c7439c01
commit d0e31b198f
45 changed files with 6222 additions and 0 deletions
+416
View File
@@ -0,0 +1,416 @@
package convert
import (
"encoding/json"
"openteam/server/internal/proxy/claude"
"openteam/server/internal/proxy/openai"
"openteam/server/internal/proxy/stream"
)
// Usage is the streaming token usage snapshot.
type Usage struct {
Input int64
Output int64
CacheRead int64
CacheCreation int64
}
// Translator converts SSE events from an upstream stream into client frames.
type Translator interface {
// Feed handles one upstream SSE event, returning client frames to write.
Feed(ev stream.SSEEvent) ([]stream.SSEEvent, error)
// Finish is called at end-of-stream, returning final frames.
Finish() ([]stream.SSEEvent, error)
// Usage returns the latest known usage.
Usage() *Usage
}
// claudeToChatTranslator converts a Claude stream to OpenAI chat chunks.
type claudeToChatTranslator struct {
model string
usage *Usage
started bool
finishSent bool
toolCallIndex int
toolCallID string
toolCallName string
}
func (t *claudeToChatTranslator) Feed(ev stream.SSEEvent) ([]stream.SSEEvent, error) {
if ev.Done {
return nil, nil
}
var e claude.StreamEvent
if err := json.Unmarshal([]byte(ev.Data), &e); err != nil {
return nil, nil
}
var out []stream.SSEEvent
switch e.Type {
case "message_start":
if e.Message != nil {
var msg struct {
Model string `json:"model"`
}
_ = json.Unmarshal(e.Message, &msg)
t.model = msg.Model
}
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{Role: "assistant"}}},
})
out = append(out, stream.SSEEvent{Data: string(chunk)})
t.started = true
case "content_block_start":
var cb struct {
Index int `json:"index"`
Block json.RawMessage `json:"content_block"`
}
_ = json.Unmarshal([]byte(ev.Data), &cb)
var block struct {
Type string `json:"type"`
ID string `json:"id"`
Name string `json:"name"`
}
_ = json.Unmarshal(cb.Block, &block)
if block.Type == "tool_use" {
t.toolCallIndex = cb.Index
t.toolCallID = block.ID
t.toolCallName = block.Name
tc, _ := json.Marshal([]map[string]any{{
"index": cb.Index, "id": block.ID, "type": "function",
"function": map[string]any{"name": block.Name, "arguments": ""},
}})
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{ToolCalls: tc}}},
})
out = append(out, stream.SSEEvent{Data: string(chunk)})
}
case "content_block_delta":
var d struct {
Index int `json:"index"`
Delta json.RawMessage `json:"delta"`
}
_ = json.Unmarshal([]byte(ev.Data), &d)
var delta struct {
Type string `json:"type"`
Text string `json:"text"`
PartialJSON string `json:"partial_json"`
}
_ = json.Unmarshal(d.Delta, &delta)
if delta.Type == "text_delta" && delta.Text != "" {
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{Content: delta.Text}}},
})
out = append(out, stream.SSEEvent{Data: string(chunk)})
} else if delta.Type == "input_json_delta" && delta.PartialJSON != "" {
tc, _ := json.Marshal([]map[string]any{{
"index": d.Index, "function": map[string]any{"arguments": delta.PartialJSON},
}})
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{ToolCalls: tc}}},
})
out = append(out, stream.SSEEvent{Data: string(chunk)})
}
case "message_delta":
var d struct {
Delta json.RawMessage `json:"delta"`
Usage json.RawMessage `json:"usage"`
}
_ = json.Unmarshal([]byte(ev.Data), &d)
if len(d.Usage) > 0 {
var u claude.Usage
if json.Unmarshal(d.Usage, &u) == nil {
t.usage = &Usage{
Input: u.InputTokens, Output: u.OutputTokens,
CacheRead: u.CacheReadInputTokens, CacheCreation: u.CacheCreationInputTokens,
}
}
}
if len(d.Delta) > 0 && !t.finishSent {
var delta struct {
StopReason string `json:"stop_reason"`
}
_ = json.Unmarshal(d.Delta, &delta)
if delta.StopReason != "" {
reason := mapClaudeStopReason(delta.StopReason)
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{}, FinishReason: &reason}},
})
out = append(out, stream.SSEEvent{Data: string(chunk)})
t.finishSent = true
}
}
}
return out, nil
}
func (t *claudeToChatTranslator) Finish() ([]stream.SSEEvent, error) {
if !t.finishSent {
reason := "stop"
chunk, _ := json.Marshal(openai.ChatChunk{
ID: "chatcmpl-stream", Object: "chat.completion.chunk", Model: t.model,
Choices: []openai.ChatChunkChoice{{Index: 0, Delta: openai.ChatDelta{}, FinishReason: &reason}},
})
t.finishSent = true
return []stream.SSEEvent{{Data: string(chunk)}, {Data: "[DONE]"}}, nil
}
return []stream.SSEEvent{{Data: "[DONE]"}}, nil
}
func (t *claudeToChatTranslator) Usage() *Usage { return t.usage }
// chatToClaudeTranslator converts an OpenAI chat stream to Claude events.
type chatToClaudeTranslator struct {
usage *Usage
started bool
openBlock bool
blockType string
toolIndex int
finishSent bool
}
func (t *chatToClaudeTranslator) Feed(ev stream.SSEEvent) ([]stream.SSEEvent, error) {
if ev.Done {
return nil, nil
}
var chunk openai.ChatChunk
if err := json.Unmarshal([]byte(ev.Data), &chunk); err != nil {
return nil, nil
}
if chunk.Usage != nil {
t.usage = &Usage{Input: chunk.Usage.PromptTokens, Output: chunk.Usage.CompletionTokens}
}
var out []stream.SSEEvent
if len(chunk.Choices) == 0 {
return out, nil
}
ch := chunk.Choices[0]
if !t.started {
msg, _ := json.Marshal(map[string]any{
"id": "msg_stream", "type": "message", "role": "assistant",
"model": chunk.Model, "content": []any{},
})
start, _ := json.Marshal(map[string]any{"type": "message_start", "message": json.RawMessage(msg)})
out = append(out, stream.SSEEvent{Data: string(start)})
t.started = true
}
if ch.Delta.Content != "" {
if !t.openBlock || t.blockType != "text" {
start, _ := json.Marshal(map[string]any{
"type": "content_block_start", "index": 0,
"content_block": map[string]any{"type": "text", "text": ""},
})
out = append(out, stream.SSEEvent{Data: string(start)})
t.openBlock = true
t.blockType = "text"
t.toolIndex = 0
}
delta, _ := json.Marshal(map[string]any{
"type": "content_block_delta", "index": 0,
"delta": map[string]any{"type": "text_delta", "text": ch.Delta.Content},
})
out = append(out, stream.SSEEvent{Data: string(delta)})
}
if len(ch.Delta.ToolCalls) > 0 {
var calls []struct {
Index *int `json:"index"`
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
_ = json.Unmarshal(ch.Delta.ToolCalls, &calls)
for _, call := range calls {
idx := 0
if call.Index != nil {
idx = *call.Index
}
if !t.openBlock || t.blockType != "tool_use" || idx != t.toolIndex {
start, _ := json.Marshal(map[string]any{
"type": "content_block_start", "index": idx,
"content_block": map[string]any{
"type": "tool_use", "id": call.ID, "name": call.Function.Name, "input": map[string]any{},
},
})
out = append(out, stream.SSEEvent{Data: string(start)})
t.openBlock = true
t.blockType = "tool_use"
t.toolIndex = idx
}
if call.Function.Arguments != "" {
delta, _ := json.Marshal(map[string]any{
"type": "content_block_delta", "index": idx,
"delta": map[string]any{"type": "input_json_delta", "partial_json": call.Function.Arguments},
})
out = append(out, stream.SSEEvent{Data: string(delta)})
}
}
}
if ch.FinishReason != nil && !t.finishSent {
reason := mapChatStopReasonToClaude(*ch.FinishReason)
md, _ := json.Marshal(map[string]any{
"type": "message_delta",
"delta": map[string]any{"stop_reason": reason, "stop_sequence": nil},
})
out = append(out, stream.SSEEvent{Data: string(md)})
t.finishSent = true
}
return out, nil
}
func (t *chatToClaudeTranslator) Finish() ([]stream.SSEEvent, error) {
if !t.finishSent {
md, _ := json.Marshal(map[string]any{
"type": "message_delta",
"delta": map[string]any{"stop_reason": "end_turn", "stop_sequence": nil},
})
t.finishSent = true
return []stream.SSEEvent{{Data: string(md)}, {Data: "{\"type\":\"message_stop\"}"}}, nil
}
return []stream.SSEEvent{{Data: "{\"type\":\"message_stop\"}"}}, nil
}
func (t *chatToClaudeTranslator) Usage() *Usage { return t.usage }
// claudeToResponsesTranslator converts a Claude stream to Responses events.
type claudeToResponsesTranslator struct {
usage *Usage
model string
completed bool
}
func (t *claudeToResponsesTranslator) Feed(ev stream.SSEEvent) ([]stream.SSEEvent, error) {
if ev.Done {
return nil, nil
}
var e claude.StreamEvent
if err := json.Unmarshal([]byte(ev.Data), &e); err != nil {
return nil, nil
}
var out []stream.SSEEvent
switch e.Type {
case "message_start":
if e.Message != nil {
var msg struct {
Model string `json:"model"`
}
_ = json.Unmarshal(e.Message, &msg)
t.model = msg.Model
}
created, _ := json.Marshal(map[string]any{
"type": "response.created",
"response": map[string]any{"id": "resp_stream", "object": "response", "status": "in_progress", "model": t.model, "output": []any{}},
})
out = append(out, stream.SSEEvent{Data: string(created)})
case "content_block_delta":
var d struct {
Index int `json:"index"`
Delta json.RawMessage `json:"delta"`
}
_ = json.Unmarshal([]byte(ev.Data), &d)
var delta struct {
Type string `json:"type"`
Text string `json:"text"`
}
_ = json.Unmarshal(d.Delta, &delta)
if delta.Type == "text_delta" && delta.Text != "" {
item, _ := json.Marshal(map[string]any{
"type": "response.output_text.delta", "item_id": "msg_stream", "output_index": 0,
"delta": delta.Text,
})
out = append(out, stream.SSEEvent{Data: string(item)})
}
case "message_delta":
var d struct {
Usage json.RawMessage `json:"usage"`
}
_ = json.Unmarshal([]byte(ev.Data), &d)
if len(d.Usage) > 0 {
var u claude.Usage
if json.Unmarshal(d.Usage, &u) == nil {
t.usage = &Usage{
Input: u.InputTokens, Output: u.OutputTokens,
CacheRead: u.CacheReadInputTokens, CacheCreation: u.CacheCreationInputTokens,
}
}
}
if !t.completed {
item, _ := json.Marshal(map[string]any{
"type": "response.completed",
"response": map[string]any{
"id": "resp_stream", "object": "response", "status": "completed", "model": t.model,
"output": []map[string]any{{"type": "message", "role": "assistant", "content": []any{}}},
},
})
out = append(out, stream.SSEEvent{Data: string(item)})
t.completed = true
}
}
return out, nil
}
func (t *claudeToResponsesTranslator) Finish() ([]stream.SSEEvent, error) {
if !t.completed {
item, _ := json.Marshal(map[string]any{
"type": "response.completed",
"response": map[string]any{
"id": "resp_stream", "object": "response", "status": "completed", "model": t.model,
"output": []map[string]any{{"type": "message", "role": "assistant", "content": []any{}}},
},
})
t.completed = true
return []stream.SSEEvent{{Data: string(item)}, {Data: "[DONE]"}}, nil
}
return []stream.SSEEvent{{Data: "[DONE]"}}, nil
}
func (t *claudeToResponsesTranslator) Usage() *Usage { return t.usage }
func mapChatStopReasonToClaude(reason string) string {
switch reason {
case "stop":
return "end_turn"
case "length":
return "max_tokens"
case "tool_calls":
return "tool_use"
case "content_filter":
return "refusal"
default:
return "end_turn"
}
}
// NewTranslator returns the stream translator for a client protocol +
// upstream provider pair, or nil for passthrough (no translation needed).
func NewTranslator(client ClientProtocol, provider string) Translator {
switch client {
case ClientOpenAIChat:
if provider == "anthropic" {
return &claudeToChatTranslator{}
}
case ClientOpenAIResponses:
if provider == "anthropic" {
return &claudeToResponsesTranslator{}
}
case ClientAnthropic:
if provider == "openai" || provider == "compatible" {
return &chatToClaudeTranslator{}
}
}
return nil
}