Files
SakurasanandClaude Sonnet 5 d0e31b198f 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>
2026-08-15 21:05:02 +08:00

270 lines
7.4 KiB
Go

package convert
import (
"encoding/json"
"openteam/server/internal/proxy/openai"
)
// claudeMessagesToChat converts Claude messages JSON into OpenAI chat messages.
func claudeMessagesToChat(raw json.RawMessage, out *[]openai.ChatMessage) error {
var msgs []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}
if err := json.Unmarshal(raw, &msgs); err != nil {
return err
}
for _, m := range msgs {
var text string
if err := json.Unmarshal(m.Content, &text); err == nil {
content, _ := json.Marshal(text)
*out = append(*out, openai.ChatMessage{Role: m.Role, Content: content})
continue
}
var blocks []map[string]json.RawMessage
if err := json.Unmarshal(m.Content, &blocks); err != nil {
return err
}
// Split blocks into: plain content parts, tool_use (→ tool_calls),
// and tool_result (→ separate role=tool messages).
var parts []json.RawMessage
var toolCalls []json.RawMessage
for _, b := range blocks {
var typ string
_ = json.Unmarshal(b["type"], &typ)
switch typ {
case "tool_use":
tc := map[string]any{
"id": rawString(b["id"]),
"type": "function",
"function": map[string]any{
"name": rawString(b["name"]),
"arguments": string(b["input"]),
},
}
encoded, _ := json.Marshal(tc)
toolCalls = append(toolCalls, encoded)
case "tool_result":
msg := openai.ChatMessage{
Role: "tool",
ToolCallID: rawString(b["tool_use_id"]),
}
// content may be a string or array of text blocks.
if b["content"] != nil {
var s string
if json.Unmarshal(b["content"], &s) == nil {
content, _ := json.Marshal(s)
msg.Content = content
} else {
var texts []struct {
Type string `json:"type"`
Text string `json:"text"`
}
if json.Unmarshal(b["content"], &texts) == nil {
var buf string
for _, t := range texts {
buf += t.Text
}
content, _ := json.Marshal(buf)
msg.Content = content
}
}
}
encoded, _ := json.Marshal(msg)
*out = append(*out, msg)
_ = encoded
default:
// text / image blocks → OpenAI content part.
part, err := claudeBlockToOpenAIContent(b)
if err != nil {
return err
}
if part != nil {
parts = append(parts, part)
}
}
}
if len(toolCalls) > 0 {
msg := openai.ChatMessage{Role: "assistant"}
if len(parts) > 0 {
content, _ := json.Marshal(partsToText(parts))
msg.Content = content
}
tcArr, _ := json.Marshal(toolCalls)
msg.ToolCalls = tcArr
*out = append(*out, msg)
} else if len(parts) > 0 {
if len(parts) == 1 {
// Collapse a single text part back to a plain string.
var s string
if json.Unmarshal(parts[0], &s) == nil {
*out = append(*out, openai.ChatMessage{Role: m.Role, Content: parts[0]})
} else {
arr, _ := json.Marshal(parts)
*out = append(*out, openai.ChatMessage{Role: m.Role, Content: arr})
}
} else {
arr, _ := json.Marshal(parts)
*out = append(*out, openai.ChatMessage{Role: m.Role, Content: arr})
}
}
}
return nil
}
func claudeBlockToOpenAIContent(b map[string]json.RawMessage) (json.RawMessage, error) {
var typ string
_ = json.Unmarshal(b["type"], &typ)
switch typ {
case "text":
part := map[string]any{"type": "text", "text": rawString(b["text"])}
return json.Marshal(part)
case "image":
// b["source"] may not be present; be defensive.
if b["source"] != nil {
var src struct {
Type string `json:"type"`
URL string `json:"url"`
Data string `json:"data"`
MediaType string `json:"media_type"`
}
_ = json.Unmarshal(b["source"], &src)
if src.URL != "" {
return json.Marshal(map[string]any{"type": "image_url", "image_url": map[string]any{"url": src.URL}})
}
if src.Data != "" {
return json.Marshal(map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:" + src.MediaType + ";base64," + src.Data}})
}
}
return nil, nil
default:
return nil, nil
}
}
func partsToText(parts []json.RawMessage) string {
var out string
for _, p := range parts {
var t struct {
Type string `json:"type"`
Text string `json:"text"`
}
if json.Unmarshal(p, &t) == nil {
out += t.Text
}
}
return out
}
func rawString(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var s string
if err := json.Unmarshal(raw, &s); err != nil {
return string(raw)
}
return s
}
// chatToClaudeMessages converts chat messages (with system already removed)
// into Claude Messages body.
func chatToClaudeMessages(msgs []openai.ChatMessage, system string) (map[string]any, error) {
claudeMsgs := []map[string]any{}
for _, m := range msgs {
if m.Role == "system" {
continue
}
// Tool calls on assistant messages → tool_use blocks.
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
content := []map[string]any{}
// Preserve any text content.
if text := contentString(m.Content); text != "" {
content = append(content, map[string]any{"type": "text", "text": text})
}
var calls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
} `json:"function"`
}
_ = json.Unmarshal(m.ToolCalls, &calls)
for _, c := range calls {
var input map[string]any
if err := json.Unmarshal(c.Function.Arguments, &input); err != nil {
input = map[string]any{"raw": string(c.Function.Arguments)}
}
content = append(content, map[string]any{
"type": "tool_use",
"id": c.ID,
"name": c.Function.Name,
"input": input,
})
}
claudeMsgs = append(claudeMsgs, map[string]any{"role": "assistant", "content": content})
continue
}
// Tool role messages → tool_result blocks.
if m.Role == "tool" {
content := []map[string]any{{
"type": "tool_result",
"tool_use_id": m.ToolCallID,
"content": contentString(m.Content),
}}
claudeMsgs = append(claudeMsgs, map[string]any{"role": "user", "content": content})
continue
}
claudeMsgs = append(claudeMsgs, map[string]any{"role": m.Role, "content": contentString(m.Content)})
}
if len(claudeMsgs) == 0 {
claudeMsgs = append(claudeMsgs, map[string]any{"role": "user", "content": "Hi"})
}
body := map[string]any{"messages": claudeMsgs}
if system != "" {
body["system"] = system
}
return body, nil
}
// toolsToClaude converts OpenAI function tools to Claude tools.
func toolsToClaude(tools []openai.Tool) []map[string]any {
out := []map[string]any{}
for _, t := range tools {
if t.Type != "" && t.Type != "function" {
continue
}
out = append(out, map[string]any{
"name": t.Function.Name,
"description": t.Function.Description,
"input_schema": json.RawMessage(t.Function.Parameters),
})
}
return out
}
// claudeToolsToOpenAI converts Claude tools to OpenAI function tools.
func claudeToolsToOpenAI(tools json.RawMessage) []openai.Tool {
var list []struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema json.RawMessage `json:"input_schema"`
}
if err := json.Unmarshal(tools, &list); err != nil {
return nil
}
out := []openai.Tool{}
for _, t := range list {
out = append(out, openai.Tool{
Type: "function",
Function: openai.FunctionTool{
Name: t.Name,
Description: t.Description,
Parameters: t.InputSchema,
},
})
}
return out
}