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:
@@ -0,0 +1,231 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChatToMessages(t *testing.T) {
|
||||
maxTokens := 1024
|
||||
temp := 0.7
|
||||
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "claude-3-sonnet-20240229",
|
||||
Messages: []Message{
|
||||
{Role: "system", Content: "You are a helpful assistant."},
|
||||
{Role: "user", Content: "Hello!"},
|
||||
},
|
||||
MaxTokens: &maxTokens,
|
||||
Temperature: &temp,
|
||||
}
|
||||
|
||||
result, err := ChatToMessages(req)
|
||||
if err != nil {
|
||||
t.Fatalf("ChatToMessages() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Model != "claude-3-sonnet-20240229" {
|
||||
t.Errorf("Model = %q, want %q", result.Model, "claude-3-sonnet-20240229")
|
||||
}
|
||||
|
||||
if len(result.Messages) != 1 {
|
||||
t.Errorf("Messages length = %d, want 1", len(result.Messages))
|
||||
}
|
||||
|
||||
if result.Messages[0].Role != "user" {
|
||||
t.Errorf("Messages[0].Role = %q, want %q", result.Messages[0].Role, "user")
|
||||
}
|
||||
|
||||
if result.System == nil {
|
||||
t.Error("System is nil, want non-nil")
|
||||
}
|
||||
|
||||
if result.MaxTokens != 1024 {
|
||||
t.Errorf("MaxTokens = %d, want 1024", result.MaxTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesToChat(t *testing.T) {
|
||||
resp := &MessagesResponse{
|
||||
ID: "msg-123",
|
||||
Model: "claude-3-sonnet-20240229",
|
||||
Content: []ContentBlock{
|
||||
{Type: "text", Text: "Hello! How can I help?"},
|
||||
},
|
||||
StopReason: "end_turn",
|
||||
Usage: Usage{
|
||||
PromptTokens: 10,
|
||||
CompletionTokens: 20,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := MessagesToChat(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("MessagesToChat() error = %v", err)
|
||||
}
|
||||
|
||||
if result.ID != "msg-123" {
|
||||
t.Errorf("ID = %q, want %q", result.ID, "msg-123")
|
||||
}
|
||||
|
||||
if result.Object != "chat.completion" {
|
||||
t.Errorf("Object = %q, want %q", result.Object, "chat.completion")
|
||||
}
|
||||
|
||||
if len(result.Choices) != 1 {
|
||||
t.Errorf("Choices length = %d, want 1", len(result.Choices))
|
||||
return
|
||||
}
|
||||
|
||||
if result.Choices[0].Message.Role != "assistant" {
|
||||
t.Errorf("Choices[0].Message.Role = %q, want %q", result.Choices[0].Message.Role, "assistant")
|
||||
}
|
||||
|
||||
if result.Choices[0].Message.Content != "Hello! How can I help?" {
|
||||
t.Errorf("Choices[0].Message.Content = %q, want %q", result.Choices[0].Message.Content, "Hello! How can I help?")
|
||||
}
|
||||
|
||||
if result.Choices[0].FinishReason != "stop" {
|
||||
t.Errorf("FinishReason = %q, want %q", result.Choices[0].FinishReason, "stop")
|
||||
}
|
||||
|
||||
if result.Usage.TotalTokens != 30 {
|
||||
t.Errorf("Usage.TotalTokens = %d, want 30", result.Usage.TotalTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatToResponses(t *testing.T) {
|
||||
maxTokens := 2048
|
||||
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "gpt-4o",
|
||||
Messages: []Message{
|
||||
{Role: "system", Content: "You are a helpful assistant."},
|
||||
{Role: "user", Content: "What is 2+2?"},
|
||||
},
|
||||
MaxTokens: &maxTokens,
|
||||
}
|
||||
|
||||
result, err := ChatToResponses(req)
|
||||
if err != nil {
|
||||
t.Fatalf("ChatToResponses() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Model != "gpt-4o" {
|
||||
t.Errorf("Model = %q, want %q", result.Model, "gpt-4o")
|
||||
}
|
||||
|
||||
if len(result.Input) != 1 {
|
||||
t.Errorf("Input length = %d, want 1", len(result.Input))
|
||||
}
|
||||
|
||||
if result.Input[0].Role != "user" {
|
||||
t.Errorf("Input[0].Role = %q, want %q", result.Input[0].Role, "user")
|
||||
}
|
||||
|
||||
if result.Instructions != "You are a helpful assistant." {
|
||||
t.Errorf("Instructions = %q, want %q", result.Instructions, "You are a helpful assistant.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesToChat(t *testing.T) {
|
||||
resp := &ResponsesResponse{
|
||||
ID: "resp-123",
|
||||
Model: "gpt-4o",
|
||||
Status: "completed",
|
||||
Output: []OutputItem{
|
||||
{
|
||||
Type: "message",
|
||||
Content: []OutputContent{
|
||||
{Type: "output_text", Text: "2+2 equals 4."},
|
||||
},
|
||||
},
|
||||
},
|
||||
Usage: Usage{
|
||||
PromptTokens: 15,
|
||||
CompletionTokens: 10,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ResponsesToChat(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("ResponsesToChat() error = %v", err)
|
||||
}
|
||||
|
||||
if result.ID != "resp-123" {
|
||||
t.Errorf("ID = %q, want %q", result.ID, "resp-123")
|
||||
}
|
||||
|
||||
if len(result.Choices) != 1 {
|
||||
t.Errorf("Choices length = %d, want 1", len(result.Choices))
|
||||
return
|
||||
}
|
||||
|
||||
if result.Choices[0].Message.Content != "2+2 equals 4." {
|
||||
t.Errorf("Content = %q, want %q", result.Choices[0].Message.Content, "2+2 equals 4.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapStopReason(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"end_turn", "stop"},
|
||||
{"stop_sequence", "stop"},
|
||||
{"tool_use", "tool_calls"},
|
||||
{"max_tokens", "length"},
|
||||
{"unknown", "stop"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := mapStopReason(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("mapStopReason(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesToChatToolUse(t *testing.T) {
|
||||
resp := &MessagesResponse{
|
||||
ID: "msg-456",
|
||||
Model: "claude-3-sonnet-20240229",
|
||||
Content: []ContentBlock{
|
||||
{Type: "text", Text: "Let me search for that."},
|
||||
{Type: "tool_use", ID: "toolu-123", Name: "web_search"},
|
||||
},
|
||||
StopReason: "tool_use",
|
||||
Usage: Usage{
|
||||
PromptTokens: 20,
|
||||
CompletionTokens: 30,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := MessagesToChat(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("MessagesToChat() error = %v", err)
|
||||
}
|
||||
|
||||
if len(result.Choices) != 1 {
|
||||
t.Errorf("Choices length = %d, want 1", len(result.Choices))
|
||||
return
|
||||
}
|
||||
|
||||
if result.Choices[0].FinishReason != "tool_calls" {
|
||||
t.Errorf("FinishReason = %q, want %q", result.Choices[0].FinishReason, "tool_calls")
|
||||
}
|
||||
|
||||
if len(result.Choices[0].Message.ToolCalls) != 1 {
|
||||
t.Errorf("ToolCalls length = %d, want 1", len(result.Choices[0].Message.ToolCalls))
|
||||
return
|
||||
}
|
||||
|
||||
if result.Choices[0].Message.ToolCalls[0].ID != "toolu-123" {
|
||||
t.Errorf("ToolCall ID = %q, want %q", result.Choices[0].Message.ToolCalls[0].ID, "toolu-123")
|
||||
}
|
||||
|
||||
if result.Choices[0].Message.ToolCalls[0].Function.Name != "web_search" {
|
||||
t.Errorf("Function.Name = %q, want %q", result.Choices[0].Message.ToolCalls[0].Function.Name, "web_search")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user