M0-M4: 推倒重来基线(基建+用户/密钥/核心代理+前端+管理后台+三协议互转)

- 后端 Go+Gin+GORM: 配置(OT_ env)/SQLite/Postgres 双驱动、用户体系(argon2id+JWT access/refresh)、
  API Key(sk- 48位, 仅存 SHA-256 哈希)
- 代理网关: /v1/chat/completions、/v1/responses、/v1/messages、/v1/models;错误按客户端协议返回
- 三协议互转(convert 包): Chat↔Messages↔Responses 请求/响应 + 流式 SSE 逐事件转换(直通优先)
- 用量计费: 异步批量记账、余额扣减、balance_logs、usage_daily 日聚合
- 管理 API: 用户/渠道 CRUD+测试+模型导入/模型定价+绑定/统计/系统配置
- 前端 Vue3+TS+Tailwind(taste-skill 设计 tokens): Landing/登录注册/控制台/管理后台,
  自建组件+Phosphor 图标+自建 SVG 趋势图, 已过 web-design-guidelines 复查
- mock 上游: OpenAI+Anthropic 双协议模拟(含流式)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 15:34:06 +08:00
co-authored by Claude
parent b25e9ec8a7
commit ec4de8d913
92 changed files with 6203 additions and 3064 deletions
@@ -0,0 +1,318 @@
package convert
import (
"encoding/json"
"strings"
)
// ---------------------------------------------------------------------------
// 请求:Responses → Chat
// responsesToChatReq 将 OpenAI Responses 请求转为 Chat 请求。
func responsesToChatReq(body []byte) ([]byte, error) {
var m map[string]json.RawMessage
if err := json.Unmarshal(body, &m); err != nil {
return nil, err
}
out := map[string]any{"model": str(rawJSON(m, "model"))}
if v, ok := m["stream"]; ok && string(v) == "true" {
out["stream"] = true
}
if v, ok := m["temperature"]; ok {
out["temperature"] = v
}
if v, ok := m["top_p"]; ok {
out["top_p"] = v
}
if v, ok := m["max_output_tokens"]; ok {
out["max_tokens"] = v
}
var msgs []any
if ins := str(rawJSON(m, "instructions")); ins != "" {
msgs = append(msgs, map[string]any{"role": "system", "content": ins})
}
msgs = append(msgs, responsesInputToChat(rawJSON(m, "input"))...)
out["messages"] = msgs
if raw := rawJSON(m, "tools"); raw != nil {
var tools []map[string]any
if json.Unmarshal(raw, &tools) == nil {
chatTools := make([]any, 0, len(tools))
for _, t := range tools {
chatTools = append(chatTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t["name"],
"description": t["description"],
"parameters": t["parameters"],
},
})
}
out["tools"] = chatTools
}
}
return json.Marshal(out)
}
// responsesInputToChat 把 Responses input 转成 Chat messages。
func responsesInputToChat(raw json.RawMessage) []any {
if len(raw) == 0 || string(raw) == "null" {
return nil
}
// 字符串输入
if s := str(raw); s != "" {
return []any{map[string]any{"role": "user", "content": s}}
}
var items []map[string]any
if err := json.Unmarshal(raw, &items); err != nil || items == nil {
return nil
}
var out []any
for _, item := range items {
switch item["type"] {
case "function_call":
var args any
_ = json.Unmarshal([]byte(strField(item["arguments"])), &args)
out = append(out, map[string]any{
"role": "assistant",
"content": "",
"tool_calls": []any{map[string]any{
"id": strField(item["call_id"]),
"type": "function",
"function": map[string]any{
"name": strField(item["name"]),
"arguments": strField(item["arguments"]),
},
}},
})
case "function_call_output":
out = append(out, map[string]any{
"role": "tool",
"tool_call_id": strField(item["call_id"]),
"content": strField(item["output"]),
})
default: // message 条目
role, _ := item["role"].(string)
if role == "" {
role = "user"
}
if content, ok := item["content"].(string); ok {
out = append(out, map[string]any{"role": role, "content": content})
} else if blocks, ok := item["content"].([]any); ok {
var text []string
for _, b := range blocks {
if bm, ok := b.(map[string]any); ok {
if t, _ := bm["text"].(string); t != "" {
text = append(text, t)
}
}
}
out = append(out, map[string]any{"role": role, "content": strings.Join(text, "")})
}
}
}
return out
}
// ---------------------------------------------------------------------------
// 请求:Chat → Responses
// chatToResponsesReq 将 Chat 请求转为 Responses 请求。
func chatToResponsesReq(body []byte) ([]byte, error) {
var req chatReq
if err := json.Unmarshal(body, &req); err != nil {
return nil, err
}
out := map[string]any{"model": req.Model}
if req.Stream {
out["stream"] = true
}
if req.Temperature != nil {
out["temperature"] = *req.Temperature
}
if req.TopP != nil {
out["top_p"] = *req.TopP
}
if req.MaxTokens != nil {
out["max_output_tokens"] = *req.MaxTokens
}
var system []string
var input []any
for _, m := range req.Messages {
if m.Role == "system" {
if s := str(m.Content); s != "" {
system = append(system, s)
}
continue
}
switch m.Role {
case "tool":
input = append(input, map[string]any{
"type": "function_call_output",
"call_id": m.ToolCallID,
"output": str(m.Content),
})
case "assistant":
if len(m.ToolCalls) > 0 {
for _, tc := range m.ToolCalls {
input = append(input, map[string]any{
"type": "function_call",
"call_id": tc.ID,
"name": tc.Function.Name,
"arguments": tc.Function.Arguments,
})
}
} else if s := str(m.Content); s != "" {
input = append(input, map[string]any{"type": "message", "role": "assistant", "content": []any{
map[string]any{"type": "input_text", "text": s},
}})
}
default:
if s := str(m.Content); s != "" {
input = append(input, map[string]any{"type": "message", "role": "user", "content": []any{
map[string]any{"type": "input_text", "text": s},
}})
}
}
}
if len(system) > 0 {
out["instructions"] = strings.Join(system, "\n")
}
if len(input) == 1 {
out["input"] = input[0] // 单条消息项
} else {
out["input"] = input
}
if len(req.Tools) > 0 {
tools := make([]any, 0, len(req.Tools))
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"name": t.Function.Name,
"description": t.Function.Description,
"parameters": rawOrObject(t.Function.Parameters),
})
}
out["tools"] = tools
}
return json.Marshal(out)
}
// ---------------------------------------------------------------------------
// 响应:Responses → Chat
// responsesToChatResp 将 Responses 响应(非流式)转为 Chat 响应。
func responsesToChatResp(body []byte) ([]byte, error) {
var m map[string]json.RawMessage
if err := json.Unmarshal(body, &m); err != nil {
return nil, err
}
var text string
var toolCalls []any
if raw := rawJSON(m, "output"); raw != nil {
var outputs []map[string]any
if json.Unmarshal(raw, &outputs) == nil {
for _, o := range outputs {
switch o["type"] {
case "message":
if content, ok := o["content"].([]any); ok {
for _, c := range content {
if cm, ok := c.(map[string]any); ok {
if t, _ := cm["text"].(string); t != "" {
text += t
}
}
}
}
case "function_call":
toolCalls = append(toolCalls, map[string]any{
"id": strField(o["call_id"]),
"type": "function",
"function": map[string]any{
"name": strField(o["name"]),
"arguments": strField(o["arguments"]),
},
})
}
}
}
}
msg := map[string]any{"role": "assistant", "content": text}
if len(toolCalls) > 0 {
msg["tool_calls"] = toolCalls
}
finish := "stop"
if string(rawJSON(m, "status")) == `"incomplete"` {
finish = "length"
}
var prompt, completion int64
if u := rawJSON(m, "usage"); u != nil {
var us struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
}
_ = json.Unmarshal(u, &us)
prompt, completion = us.InputTokens, us.OutputTokens
}
return json.Marshal(map[string]any{
"id": "chatcmpl-" + strings.TrimPrefix(str(rawJSON(m, "id")), "resp_"),
"object": "chat.completion",
"model": str(rawJSON(m, "model")),
"choices": []any{map[string]any{"index": 0, "message": msg, "finish_reason": finish}},
"usage": map[string]any{
"prompt_tokens": prompt,
"completion_tokens": completion,
"total_tokens": prompt + completion,
},
})
}
// ---------------------------------------------------------------------------
// 响应:Chat → Responses
// chatToResponsesResp 将 Chat 响应(非流式)转为 Responses 响应。
func chatToResponsesResp(body []byte) ([]byte, error) {
var r chatResp
if err := json.Unmarshal(body, &r); err != nil {
return nil, err
}
output := make([]any, 0, 2)
var finish = "completed"
if len(r.Choices) > 0 {
msg := r.Choices[0].Message
if msg.Content != "" {
output = append(output, map[string]any{
"type": "message",
"role": "assistant",
"content": []any{map[string]any{"type": "output_text", "text": msg.Content}},
})
}
for _, tc := range msg.ToolCalls {
output = append(output, map[string]any{
"type": "function_call",
"call_id": tc.ID,
"name": tc.Function.Name,
"arguments": tc.Function.Arguments,
})
}
if r.Choices[0].FinishReason == "length" {
finish = "incomplete"
}
}
status := finish
return json.Marshal(map[string]any{
"id": "resp_" + strings.TrimPrefix(r.ID, "chatcmpl-"),
"object": "response",
"model": r.Model,
"status": status,
"output": output,
"usage": map[string]any{
"input_tokens": r.Usage.PromptTokens,
"output_tokens": r.Usage.CompletionTokens,
"total_tokens": r.Usage.PromptTokens + r.Usage.CompletionTokens,
},
})
}