发布 0.3.0:恢复 Chat 接口并增强云端事件通知
This commit is contained in:
+108
-4
@@ -281,7 +281,7 @@ func (h *aiGatewayHandler) streamAnthropic(c *gin.Context, body []byte, req aiwi
|
||||
defer upstream.Close()
|
||||
sseHeaders(c)
|
||||
bridge := service.NewAnthRespBridge(aiRandID("msg_"), req.Model)
|
||||
if err := forwardAnthSSE(c, upstream, bridge); err != nil {
|
||||
if err := forwardSSEData(upstream, func(data []byte) { writeAnthEvents(c, bridge.Feed(data)) }); err != nil {
|
||||
entry.ErrMsg = err.Error()
|
||||
}
|
||||
writeAnthEvents(c, bridge.Finish())
|
||||
@@ -295,15 +295,16 @@ func (h *aiGatewayHandler) streamAnthropic(c *gin.Context, body []byte, req aiwi
|
||||
h.maybeLogContent(c, callID, "anthropic", req.Model, true, req, nil)
|
||||
}
|
||||
|
||||
// forwardAnthSSE 逐行读上游 SSE,把 data 行喂给桥并即时写出转换后的事件。
|
||||
func forwardAnthSSE(c *gin.Context, upstream io.Reader, bridge *service.AnthRespBridge) error {
|
||||
// forwardSSEData 逐行读上游 SSE,把 data 行交给 emit 即时转换写出;
|
||||
// Anthropic 与 Chat Completions 两条流式桥共用。
|
||||
func forwardSSEData(upstream io.Reader, emit func([]byte)) error {
|
||||
reader := bufio.NewReader(upstream)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if len(line) > 0 {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
if data, ok := bytes.CutPrefix(trimmed, []byte("data: ")); ok {
|
||||
writeAnthEvents(c, bridge.Feed(data))
|
||||
emit(data)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -328,6 +329,109 @@ func writeAnthEvents(c *gin.Context, events []service.AnthEvent) {
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
// chatCompletions 是 OpenAI /ai/v1/chat/completions 端点(Tier 2 兼容层,
|
||||
// 承接只会说 Chat Completions 的存量客户端)。
|
||||
//
|
||||
// @Summary OpenAI Chat Completions 兼容端点
|
||||
// @Tags AI 网关
|
||||
// @Param body body object true "OpenAI chat/completions 请求体(支持 stream;经 Responses 转换直通)"
|
||||
// @Success 200 {object} map[string]any "OpenAI 兼容响应(流式为 SSE,末尾 data: [DONE])"
|
||||
// @Router /ai/v1/chat/completions [post]
|
||||
func (h *aiGatewayHandler) chatCompletions(c *gin.Context) {
|
||||
var req aiwire.ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Model) == "" || len(req.Messages) == 0 {
|
||||
aiError(c, http.StatusBadRequest, "invalid_request_error", "model 与 messages 不能为空")
|
||||
return
|
||||
}
|
||||
if !checkKeyModel(c, req.Model) {
|
||||
return
|
||||
}
|
||||
body, err := service.ChatToResponsesBody(req)
|
||||
if err != nil {
|
||||
aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
return
|
||||
}
|
||||
if req.Stream {
|
||||
h.streamChat(c, body, req)
|
||||
return
|
||||
}
|
||||
h.chatOnce(c, body, req)
|
||||
}
|
||||
|
||||
// chatOnce 处理非流式:直通上游 → 转回 chat.completion → 记账。
|
||||
func (h *aiGatewayHandler) chatOnce(c *gin.Context, body []byte, req aiwire.ChatRequest) {
|
||||
start := time.Now()
|
||||
payload, meta, err := h.gw.RespPassthrough(c.Request.Context(), body, req.Model, keyGroup(c))
|
||||
entry := h.logEntry(c, "openai", req.Model, false, meta, start)
|
||||
if err != nil {
|
||||
upstreamError(c, err)
|
||||
entry.ErrMsg = err.Error()
|
||||
h.logFailure(c, entry, req)
|
||||
return
|
||||
}
|
||||
out, err := service.ResponsesToChat(payload, aiRandID("chatcmpl-"), time.Now().Unix())
|
||||
if err != nil {
|
||||
aiError(c, http.StatusBadGateway, "api_error", err.Error())
|
||||
entry.ErrMsg = err.Error()
|
||||
h.logFailure(c, entry, req)
|
||||
return
|
||||
}
|
||||
entry.Status = http.StatusOK
|
||||
fillUsage(&entry, out.Usage)
|
||||
callID := h.gw.LogCall(entry)
|
||||
h.maybeLogContent(c, callID, "openai", req.Model, false, req, out)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// streamChat 流式直通并桥接:上游 Responses SSE 经 ChatRespBridge 转为
|
||||
// chat.completion.chunk 序列逐块写出,末尾发 [DONE];usage 从 completed 事件记账。
|
||||
func (h *aiGatewayHandler) streamChat(c *gin.Context, body []byte, req aiwire.ChatRequest) {
|
||||
start := time.Now()
|
||||
upstream, meta, err := h.gw.RespPassthroughStream(c.Request.Context(), body, req.Model, keyGroup(c))
|
||||
entry := h.logEntry(c, "openai", req.Model, true, meta, start)
|
||||
if err != nil {
|
||||
upstreamError(c, err)
|
||||
entry.ErrMsg = err.Error()
|
||||
h.logFailure(c, entry, req)
|
||||
return
|
||||
}
|
||||
defer upstream.Close()
|
||||
sseHeaders(c)
|
||||
includeUsage := req.StreamOptions != nil && req.StreamOptions.IncludeUsage
|
||||
bridge := service.NewChatRespBridge(aiRandID("chatcmpl-"), req.Model, time.Now().Unix(), includeUsage)
|
||||
if err := forwardSSEData(upstream, func(data []byte) { writeChatChunks(c, bridge.Feed(data)) }); err != nil {
|
||||
entry.ErrMsg = err.Error()
|
||||
}
|
||||
writeChatChunks(c, bridge.Finish())
|
||||
c.Writer.WriteString("data: [DONE]\n\n")
|
||||
c.Writer.Flush()
|
||||
entry.Status = http.StatusOK
|
||||
entry.LatencyMs = time.Since(start).Milliseconds()
|
||||
fillUsage(&entry, bridge.Usage())
|
||||
callID := h.gw.LogCall(entry)
|
||||
h.maybeLogContent(c, callID, "openai", req.Model, true, req, nil)
|
||||
}
|
||||
|
||||
// writeChatChunks 逐块写出 chunk 的 SSE data 行并 flush。
|
||||
func writeChatChunks(c *gin.Context, chunks []aiwire.ChatChunk) {
|
||||
for _, ch := range chunks {
|
||||
b, err := json.Marshal(ch)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c.Writer.WriteString("data: ")
|
||||
c.Writer.Write(b)
|
||||
c.Writer.WriteString("\n\n")
|
||||
}
|
||||
if len(chunks) > 0 {
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// listModels 是 /ai/v1/models 端点(OpenAI 格式,从启用渠道的模型缓存聚合)。
|
||||
//
|
||||
// @Summary 可用模型列表
|
||||
|
||||
Reference in New Issue
Block a user