refactor: complete backend rewrite for multi-protocol proxy

Major rewrite of the Go backend to support:
- Three API format imports: openai, anthropic, compatible
- Three protocol conversions: Chat Completions, Responses, Messages
- Hub-and-spoke architecture with Chat as intermediate format

Deleted:
- opencat.go (old entry)
- store/, team/, pkg/team/, pkg/store/ (old data layer)
- internal/model/, internal/consts/ (old types)
- internal/service/team/, internal/controller/team/ (old handlers)
- llm/ (removed LLM client library, pure proxy mode)
- dist/, assets/ (old build artifacts)

Added:
- internal/store/ — 9 GORM models + multi-DB support
- internal/pkg/ — crypto (AES-GCM), apikey, jwt, ratelimit, resp, tokenizer
- internal/channel/ — channel selection, weighted LB, health checks
- internal/proxy/convert/ — 6 protocol conversion functions + SSE streaming
- internal/proxy/ — gateway with request dispatch and upstream selection
- internal/usage/ — async usage recorder with batch writes
- internal/api/ — management API (auth, users, keys, channels, models)
- Makefile for build/test/deploy

Fixed API to match frontend expectations:
- Login response wraps token in { data: { token } }
- GET /api/profile route added
- Profile response wraps user in { code, data }
- Role returned as number (10=admin, 1=user)
This commit is contained in:
Sakurasan
2026-08-30 11:49:31 +08:00
parent aa0d87f132
commit ef3025dd80
127 changed files with 4623 additions and 10500 deletions
+298
View File
@@ -0,0 +1,298 @@
package convert
import (
"encoding/json"
"fmt"
)
// ChatToResponses converts a Chat Completions request to Responses API format
func ChatToResponses(req *ChatCompletionRequest) (*ResponsesRequest, error) {
var inputItems []InputItem
var instructions string
for _, m := range req.Messages {
if m.Role == "system" {
if s, ok := m.Content.(string); ok {
if instructions != "" {
instructions += "\n\n"
}
instructions += s
}
continue
}
item := InputItem{
Role: m.Role,
Content: m.Content,
}
inputItems = append(inputItems, item)
}
out := &ResponsesRequest{
Model: req.Model,
Input: inputItems,
Instructions: instructions,
Stream: req.Stream,
}
if req.MaxTokens != nil {
out.MaxOutputTokens = req.MaxTokens
}
if req.Temperature != nil {
out.Temperature = req.Temperature
}
if req.TopP != nil {
out.TopP = req.TopP
}
if req.Tools != nil {
out.Tools = req.Tools
}
return out, nil
}
// ResponsesToChat converts a Responses API response to Chat Completions format
func ResponsesToChat(resp *ResponsesResponse) (*ChatCompletionResponse, error) {
choices := make([]Choice, 0)
for _, output := range resp.Output {
switch output.Type {
case "message":
for _, content := range output.Content {
switch content.Type {
case "output_text":
choices = append(choices, Choice{
Index: len(choices),
Message: Message{
Role: "assistant",
Content: content.Text,
},
FinishReason: "stop",
})
case "function_call":
toolCall := ToolCall{
ID: content.ID,
Type: "function",
Function: FunctionCall{
Name: content.Name,
Arguments: toJSON(content.Input),
},
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
ToolCalls: []ToolCall{toolCall},
},
FinishReason: "tool_calls",
})
} else {
choices[0].Message.ToolCalls = append(choices[0].Message.ToolCalls, toolCall)
choices[0].FinishReason = "tool_calls"
}
}
}
case "function_call_output":
// This would be in a user message context
continue
}
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
Content: "",
},
FinishReason: "stop",
})
}
return &ChatCompletionResponse{
ID: resp.ID,
Object: "chat.completion",
Model: resp.Model,
Choices: choices,
Usage: &Usage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.PromptTokens + resp.Usage.CompletionTokens,
},
}, nil
}
// ResponsesStreamToChatStream converts Responses API streaming to Chat Completions format
func ResponsesStreamToChatStream(events []ResponsesStreamEvent, model string) []ChatCompletionStreamChunk {
var chunks []ChatCompletionStreamChunk
id := fmt.Sprintf("chatcmpl-%d", len(events))
for _, event := range events {
switch event.Type {
case "response.created":
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
},
}},
})
case "response.output_item.added":
if event.Item != nil && event.Item.Type == "message" {
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
},
}},
})
}
case "response.content_part.delta":
if event.Delta != "" {
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Content: event.Delta,
},
}},
})
}
case "response.completed":
finishReason := "stop"
chunk := ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
FinishReason: &finishReason,
}},
}
chunks = append(chunks, chunk)
}
}
return chunks
}
// MessagesToResponses converts an Anthropic Messages request to Responses API format
func MessagesToResponses(req *MessagesRequest) (*ResponsesRequest, error) {
var inputItems []InputItem
var instructions string
// Handle system message
if req.System != nil {
switch v := req.System.(type) {
case string:
instructions = v
case []ContentPart:
for _, p := range v {
if p.Type == "text" {
if instructions != "" {
instructions += "\n\n"
}
instructions += p.Text
}
}
}
}
for _, m := range req.Messages {
item := InputItem{
Role: m.Role,
Content: m.Content,
}
inputItems = append(inputItems, item)
}
out := &ResponsesRequest{
Model: req.Model,
Input: inputItems,
Instructions: instructions,
Stream: req.Stream,
}
out.MaxOutputTokens = &req.MaxTokens
if req.Temperature != nil {
out.Temperature = req.Temperature
}
if req.TopP != nil {
out.TopP = req.TopP
}
if req.Tools != nil {
out.Tools = req.Tools
}
return out, nil
}
// ResponsesToMessages converts a Responses API response to Anthropic Messages format
func ResponsesToMessages(resp *ResponsesResponse) (*MessagesResponse, error) {
var content []ContentBlock
for _, output := range resp.Output {
switch output.Type {
case "message":
for _, c := range output.Content {
switch c.Type {
case "output_text":
content = append(content, ContentBlock{
Type: "text",
Text: c.Text,
})
case "function_call":
content = append(content, ContentBlock{
Type: "tool_use",
ID: c.ID,
Name: c.Name,
})
}
}
}
}
var stopReason string
if len(content) > 0 {
last := content[len(content)-1]
if last.Type == "tool_use" {
stopReason = "tool_use"
} else {
stopReason = "end_turn"
}
} else {
stopReason = "end_turn"
}
return &MessagesResponse{
ID: resp.ID,
Type: "message",
Role: "assistant",
Content: content,
Model: resp.Model,
StopReason: stopReason,
Usage: resp.Usage,
}, nil
}
// toJSON is a helper to convert a value to JSON string
func toJSONStr(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(b)
}