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

162 lines
4.5 KiB
Go

package convert
import (
"encoding/json"
"errors"
"openteam/server/internal/proxy/openai"
)
// responsesInputToChat converts Responses API input items into chat messages.
func responsesInputToChat(raw json.RawMessage) ([]openai.ChatMessage, error) {
var msgs []openai.ChatMessage
// `input` may be a plain string.
var s string
if json.Unmarshal(raw, &s) == nil {
content, _ := json.Marshal(s)
msgs = append(msgs, openai.ChatMessage{Role: "user", Content: content})
return msgs, nil
}
// Or an array of content parts (text/image).
var parts []map[string]json.RawMessage
if json.Unmarshal(raw, &parts) == nil {
chat, err := contentPartsToChat(parts)
if err == nil {
return chat, nil
}
}
var items []map[string]json.RawMessage
if err := json.Unmarshal(raw, &items); err != nil {
return nil, err
}
for _, item := range items {
var typ string
_ = json.Unmarshal(item["type"], &typ)
switch typ {
case "message":
var role string
_ = json.Unmarshal(item["role"], &role)
content, err := contentPartsToText(item["content"])
if err != nil {
continue
}
msgs = append(msgs, openai.ChatMessage{Role: role, Content: content})
case "function_call":
tc := map[string]any{
"id": rawString(item["call_id"]),
"type": "function",
"function": map[string]any{
"name": rawString(item["name"]),
"arguments": rawString(item["arguments"]),
},
}
tcArr, _ := json.Marshal([]any{tc})
msgs = append(msgs, openai.ChatMessage{Role: "assistant", ToolCalls: tcArr})
case "function_call_output":
content, _ := json.Marshal(rawString(item["output"]))
msgs = append(msgs, openai.ChatMessage{Role: "tool", ToolCallID: rawString(item["call_id"]), Content: content})
case "reasoning", "computer_call", "web_search_call":
// Not representable in chat; drop.
}
}
if len(msgs) == 0 {
content, _ := json.Marshal("")
msgs = append(msgs, openai.ChatMessage{Role: "user", Content: content})
}
return msgs, nil
}
// contentPartsToText flattens an array of content parts into a string.
func contentPartsToText(raw json.RawMessage) (json.RawMessage, error) {
var parts []map[string]json.RawMessage
if err := json.Unmarshal(raw, &parts); err != nil {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
c, _ := json.Marshal(s)
return c, nil
}
return nil, err
}
var buf string
for _, p := range parts {
var typ string
_ = json.Unmarshal(p["type"], &typ)
switch typ {
case "input_text", "output_text", "text":
buf += rawString(p["text"])
}
}
c, _ := json.Marshal(buf)
return c, nil
}
func contentPartsToChat(parts []map[string]json.RawMessage) ([]openai.ChatMessage, error) {
out := []map[string]any{}
for _, p := range parts {
var typ string
_ = json.Unmarshal(p["type"], &typ)
switch typ {
case "input_text", "output_text", "text":
out = append(out, map[string]any{"type": "text", "text": rawString(p["text"])})
case "input_image":
out = append(out, map[string]any{
"type": "image_url",
"image_url": map[string]any{
"url": "data:" + rawString(p["media_type"]) + ";base64," + rawString(p["image_url"]),
},
})
}
}
if len(out) == 0 {
return nil, jsonError("empty content")
}
arr, _ := json.Marshal(out)
msg := openai.ChatMessage{Role: "user", Content: arr}
return []openai.ChatMessage{msg}, nil
}
// chatToResponsesOutput builds Responses output items from a chat completion.
func chatToResponsesOutput(cc *openai.ChatCompletion) ([]map[string]any, string) {
var items []map[string]any
var status = "completed"
if len(cc.Choices) == 0 {
return items, status
}
ch := cc.Choices[0]
if ch.FinishReason == "length" {
status = "incomplete"
}
content := []map[string]any{}
if text := contentString(ch.Message.Content); text != "" {
content = append(content, map[string]any{"type": "output_text", "text": text})
}
items = append(items, map[string]any{"type": "message", "role": "assistant", "content": content})
if len(ch.Message.ToolCalls) > 0 {
var calls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
} `json:"function"`
}
if json.Unmarshal(ch.Message.ToolCalls, &calls) == nil {
for _, call := range calls {
items = append(items, map[string]any{
"type": "function_call",
"call_id": call.ID,
"name": call.Function.Name,
"arguments": rawString(call.Function.Arguments),
})
}
}
}
return items, status
}
func jsonError(msg string) error {
return errors.New(msg)
}