package api import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "oci-portal/internal/aiwire" "oci-portal/internal/service" ) // audioSpeech 是 OpenAI /ai/v1/audio/speech 端点(TTS,直通兼容面)。 // // @Summary OpenAI Audio Speech 兼容端点(文本转语音) // @Tags AI 网关 // @Param body body aiwire.SpeechRequest true "OpenAI audio speech 请求体(model/input 必填,voice 见 xAI Grok Voice 列表,language 缺省 auto;未列字段原样透传)" // @Success 200 {file} binary "音频字节(Content-Type 透传上游,默认 audio/mpeg)" // @Router /ai/v1/audio/speech [post] func (h *aiGatewayHandler) audioSpeech(c *gin.Context) { raw, err := c.GetRawData() if err != nil { aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return } modelName, body, err := service.SpeechBodyNormalize(raw) if err != nil { aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return } if !checkKeyModel(c, modelName) { return } start := time.Now() audio, contentType, meta, err := h.gw.Speech(c.Request.Context(), modelName, body, keyGroup(c)) entry := h.logEntry(c, "speech", modelName, false, meta, start) if err != nil { upstreamError(c, err) entry.ErrMsg = err.Error() h.logFailure(c, entry, string(raw)) return } entry.Status = http.StatusOK callID := h.gw.LogCall(entry) h.maybeLogContent(c, callID, "speech", modelName, false, string(raw), nil) if contentType == "" { contentType = "audio/mpeg" } c.Data(http.StatusOK, contentType, audio) } // rerank 是 /ai/v1/rerank 端点(Jina / Cohere 风格文档重排)。 // // @Summary 文档重排端点(Cohere Rerank) // @Tags AI 网关 // @Param body body aiwire.RerankRequest true "重排请求体(model/query/documents 必填,可选 top_n/return_documents)" // @Success 200 {object} aiwire.RerankResponse "重排结果(results[].index 指向入参下标)" // @Router /ai/v1/rerank [post] func (h *aiGatewayHandler) rerank(c *gin.Context) { var req aiwire.RerankRequest if err := c.ShouldBindJSON(&req); err != nil { aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return } if strings.TrimSpace(req.Model) == "" || strings.TrimSpace(req.Query) == "" || len(req.Documents) == 0 { aiError(c, http.StatusBadRequest, "invalid_request_error", "model、query 与 documents 不能为空") return } if !checkKeyModel(c, req.Model) { return } start := time.Now() resp, meta, err := h.gw.Rerank(c.Request.Context(), req, keyGroup(c)) entry := h.logEntry(c, "rerank", req.Model, false, meta, start) if err != nil { upstreamError(c, err) entry.ErrMsg = err.Error() h.logFailure(c, entry, req) return } entry.Status = http.StatusOK callID := h.gw.LogCall(entry) h.maybeLogContent(c, callID, "rerank", req.Model, false, req, resp) c.JSON(http.StatusOK, resp) } // moderations 是 /ai/v1/moderations 端点(OpenAI 外壳映射 OCI Guardrails)。 // // @Summary 内容审核端点(OCI Guardrails) // @Tags AI 网关 // @Param body body aiwire.ModerationsRequest true "审核请求体(input 为字符串或字符串数组,单次至多 8 条;model 接受但忽略)" // @Success 200 {object} aiwire.ModerationsResponse "审核结果(categories/category_scores 为 overall/blocklist/prompt_injection,pii 为扩展字段)" // @Router /ai/v1/moderations [post] func (h *aiGatewayHandler) moderations(c *gin.Context) { var req aiwire.ModerationsRequest if err := c.ShouldBindJSON(&req); err != nil { aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return } inputs, err := service.ModerationInputs(req.Input) if err != nil { aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return } start := time.Now() resp, meta, err := h.gw.Moderations(c.Request.Context(), aiRandID("modr_"), inputs, keyGroup(c)) entry := h.logEntry(c, "moderations", "oci-guardrails", false, meta, start) if err != nil { upstreamError(c, err) entry.ErrMsg = err.Error() h.logFailure(c, entry, req) return } entry.Status = http.StatusOK callID := h.gw.LogCall(entry) h.maybeLogContent(c, callID, "moderations", "oci-guardrails", false, req, resp) c.JSON(http.StatusOK, resp) }