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>
162 lines
4.5 KiB
Go
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)
|
|
}
|