- 后端 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>
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
// Package convert 三协议互转:OpenAI Chat / OpenAI Responses / Anthropic Messages。
|
|
// 网关以 OpenAI Chat 形状作为标准中间模型(PLANNING §5.1.1)。
|
|
// 请求与响应(非流式)走 JSON 转换;流式走逐行 SSE 转换(见 stream.go)。
|
|
package convert
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// 协议标识。
|
|
const (
|
|
ProtoChat = "chat"
|
|
ProtoMessages = "messages"
|
|
ProtoResponses = "responses"
|
|
)
|
|
|
|
// ConvertRequest 转换请求体。from==to 时原样返回。
|
|
func ConvertRequest(body []byte, from, to string) ([]byte, error) {
|
|
if from == to {
|
|
return body, nil
|
|
}
|
|
switch {
|
|
case from == ProtoMessages && to == ProtoChat:
|
|
return messagesToChatReq(body)
|
|
case from == ProtoChat && to == ProtoMessages:
|
|
return chatToMessagesReq(body)
|
|
case from == ProtoResponses && to == ProtoChat:
|
|
return responsesToChatReq(body)
|
|
case from == ProtoChat && to == ProtoResponses:
|
|
return chatToResponsesReq(body)
|
|
case from == ProtoResponses && to == ProtoMessages:
|
|
mid, err := responsesToChatReq(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chatToMessagesReq(mid)
|
|
case from == ProtoMessages && to == ProtoResponses:
|
|
mid, err := messagesToChatReq(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chatToResponsesReq(mid)
|
|
}
|
|
return nil, fmt.Errorf("unsupported request conversion %s->%s", from, to)
|
|
}
|
|
|
|
// ConvertResponse 转换响应体(非流式)。from==to 时原样返回。
|
|
func ConvertResponse(body []byte, from, to string) ([]byte, error) {
|
|
if from == to {
|
|
return body, nil
|
|
}
|
|
switch {
|
|
case from == ProtoMessages && to == ProtoChat:
|
|
return messagesToChatResp(body)
|
|
case from == ProtoChat && to == ProtoMessages:
|
|
return chatToMessagesResp(body)
|
|
case from == ProtoResponses && to == ProtoChat:
|
|
return responsesToChatResp(body)
|
|
case from == ProtoChat && to == ProtoResponses:
|
|
return chatToResponsesResp(body)
|
|
case from == ProtoResponses && to == ProtoMessages:
|
|
mid, err := responsesToChatResp(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chatToMessagesResp(mid)
|
|
case from == ProtoMessages && to == ProtoResponses:
|
|
mid, err := messagesToChatResp(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chatToResponsesResp(mid)
|
|
}
|
|
return nil, fmt.Errorf("unsupported response conversion %s->%s", from, to)
|
|
}
|
|
|
|
// NewStreamTransformer 构造流式逐行转换器:输入上游 SSE 一行,返回客户端 SSE 行。
|
|
// 返回 nil 表示丢弃该行;(from==to 时无需转换)。
|
|
func NewStreamTransformer(from, to string) func([]byte) []byte {
|
|
switch {
|
|
case from == ProtoMessages && to == ProtoChat:
|
|
return newMessagesToChat().line
|
|
case from == ProtoChat && to == ProtoMessages:
|
|
return newChatToMessages().line
|
|
case from == ProtoResponses && to == ProtoChat:
|
|
return newResponsesToChat().line
|
|
case from == ProtoChat && to == ProtoResponses:
|
|
return newChatToResponses().line
|
|
case from == ProtoResponses && to == ProtoMessages:
|
|
return newResponsesToMessages().line
|
|
case from == ProtoMessages && to == ProtoResponses:
|
|
return newMessagesToResponses().line
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 工具函数
|
|
|
|
// str 返回字符串字段;json.RawMessage 为字符串字面量时去引号。
|
|
func str(raw json.RawMessage) string {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return ""
|
|
}
|
|
var s string
|
|
if json.Unmarshal(raw, &s) == nil {
|
|
return s
|
|
}
|
|
// 数组/对象:尝试取 type=text 的 text
|
|
var arr []map[string]any
|
|
if json.Unmarshal(raw, &arr) == nil {
|
|
var parts []string
|
|
for _, b := range arr {
|
|
if t, _ := b["type"].(string); t == "text" || t == "input_text" || t == "output_text" {
|
|
if txt, _ := b["text"].(string); txt != "" {
|
|
parts = append(parts, txt)
|
|
}
|
|
}
|
|
}
|
|
return joinNonEmpty(parts, "\n")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func joinNonEmpty(parts []string, sep string) string {
|
|
out := ""
|
|
for i, p := range parts {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if out != "" {
|
|
out += sep
|
|
}
|
|
out += p
|
|
_ = i
|
|
}
|
|
return out
|
|
}
|
|
|
|
// rawJSON 安全取字段;不存在或 null 返回 nil。
|
|
func rawJSON(m map[string]json.RawMessage, key string) json.RawMessage {
|
|
raw, ok := m[key]
|
|
if !ok || string(raw) == "null" {
|
|
return nil
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// decode 把 RawMessage 解到 map。
|
|
func decode(raw json.RawMessage) (map[string]any, error) {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|