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)
213 lines
4.7 KiB
Go
213 lines
4.7 KiB
Go
package convert
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// ChatToMessages converts a Chat Completions request to Anthropic Messages format
|
|
func ChatToMessages(req *ChatCompletionRequest) (*MessagesRequest, error) {
|
|
msgs := make([]Message, 0, len(req.Messages))
|
|
var systemParts []ContentPart
|
|
|
|
for _, m := range req.Messages {
|
|
if m.Role == "system" {
|
|
// Extract system message content
|
|
switch v := m.Content.(type) {
|
|
case string:
|
|
systemParts = append(systemParts, ContentPart{
|
|
Type: "text",
|
|
Text: v,
|
|
})
|
|
case []interface{}:
|
|
for _, part := range v {
|
|
if p, ok := part.(map[string]interface{}); ok {
|
|
if t, ok := p["type"].(string); ok && t == "text" {
|
|
if text, ok := p["text"].(string); ok {
|
|
systemParts = append(systemParts, ContentPart{
|
|
Type: "text",
|
|
Text: text,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
msgs = append(msgs, m)
|
|
}
|
|
|
|
out := &MessagesRequest{
|
|
Model: req.Model,
|
|
Messages: msgs,
|
|
Stream: req.Stream,
|
|
}
|
|
|
|
if len(systemParts) > 0 {
|
|
out.System = systemParts
|
|
}
|
|
|
|
if req.MaxTokens != nil {
|
|
out.MaxTokens = *req.MaxTokens
|
|
} else {
|
|
defaultMax := 4096
|
|
out.MaxTokens = defaultMax
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// MessagesToChat converts an Anthropic Messages response to Chat Completions format
|
|
func MessagesToChat(resp *MessagesResponse) (*ChatCompletionResponse, error) {
|
|
choices := make([]Choice, 0)
|
|
|
|
for _, block := range resp.Content {
|
|
switch block.Type {
|
|
case "text":
|
|
choices = append(choices, Choice{
|
|
Index: len(choices),
|
|
Message: Message{
|
|
Role: "assistant",
|
|
Content: block.Text,
|
|
},
|
|
FinishReason: mapStopReason(resp.StopReason),
|
|
})
|
|
case "tool_use":
|
|
toolCall := ToolCall{
|
|
ID: block.ID,
|
|
Type: "function",
|
|
Function: FunctionCall{
|
|
Name: block.Name,
|
|
Arguments: toJSON(block.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"
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// MessagesStreamToChatStream converts Anthropic streaming chunks to Chat Completions format
|
|
func MessagesStreamToChatStream(anthropicEvents []AnthropicStreamEvent, model string) []ChatCompletionStreamChunk {
|
|
var chunks []ChatCompletionStreamChunk
|
|
id := fmt.Sprintf("chatcmpl-%d", len(anthropicEvents))
|
|
|
|
for _, event := range anthropicEvents {
|
|
switch event.Type {
|
|
case "message_start":
|
|
// Initial chunk with role
|
|
chunks = append(chunks, ChatCompletionStreamChunk{
|
|
ID: id,
|
|
Object: "chat.completion.chunk",
|
|
Model: model,
|
|
Choices: []StreamChoice{{
|
|
Index: 0,
|
|
Delta: StreamDelta{
|
|
Role: "assistant",
|
|
},
|
|
}},
|
|
})
|
|
case "content_block_delta":
|
|
if event.Delta != nil && event.Delta.Text != "" {
|
|
chunks = append(chunks, ChatCompletionStreamChunk{
|
|
ID: id,
|
|
Object: "chat.completion.chunk",
|
|
Model: model,
|
|
Choices: []StreamChoice{{
|
|
Index: 0,
|
|
Delta: StreamDelta{
|
|
Content: event.Delta.Text,
|
|
},
|
|
}},
|
|
})
|
|
}
|
|
case "message_delta":
|
|
finishReason := "stop"
|
|
if event.Delta != nil && event.Delta.StopReason != "" {
|
|
finishReason = mapStopReason(event.Delta.StopReason)
|
|
}
|
|
chunk := ChatCompletionStreamChunk{
|
|
ID: id,
|
|
Object: "chat.completion.chunk",
|
|
Model: model,
|
|
Choices: []StreamChoice{{
|
|
Index: 0,
|
|
FinishReason: &finishReason,
|
|
}},
|
|
}
|
|
if event.Usage != nil {
|
|
chunk.Usage = event.Usage
|
|
}
|
|
chunks = append(chunks, chunk)
|
|
}
|
|
}
|
|
|
|
return chunks
|
|
}
|
|
|
|
func mapStopReason(reason string) string {
|
|
switch reason {
|
|
case "end_turn", "stop_sequence":
|
|
return "stop"
|
|
case "tool_use":
|
|
return "tool_calls"
|
|
case "max_tokens":
|
|
return "length"
|
|
default:
|
|
return "stop"
|
|
}
|
|
}
|
|
|
|
func toJSON(v interface{}) string {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "{}"
|
|
}
|
|
return string(b)
|
|
}
|