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>
270 lines
7.4 KiB
Go
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
|
|
}
|