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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b0c7439c01
commit
d0e31b198f
@@ -0,0 +1,161 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user