Files
team-api/relay/handler/chat_handler.go
T

216 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/gogf/gf/v2/frame/g"
"github.com/qianfree/team-api/relay/common"
"github.com/qianfree/team-api/relay/constant"
"github.com/qianfree/team-api/relay/dto"
"github.com/qianfree/team-api/relay/helper"
)
// HandleChatCompletions 处理 /v1/chat/completions 请求
func HandleChatCompletions(ctx context.Context, body []byte, path string, headers http.Header, rc *RelayContext, provider common.DataProvider, billing common.BillingProvider) (*common.Usage, *BillingResult, error) {
return RelayHandler(ctx, body, path, headers, rc, provider, billing)
}
// HandleEmbeddings 处理 /v1/embeddings 请求
func HandleEmbeddings(ctx context.Context, body []byte, path string, headers http.Header, rc *RelayContext, provider common.DataProvider, billing common.BillingProvider) (*common.Usage, *BillingResult, error) {
return RelayHandler(ctx, body, path, headers, rc, provider, billing)
}
// HandleImagesGenerations 处理 /v1/images/generations 请求
func HandleImagesGenerations(ctx context.Context, body []byte, path string, headers http.Header, rc *RelayContext, provider common.DataProvider, billing common.BillingProvider) (*common.Usage, *BillingResult, error) {
return RelayHandler(ctx, body, path, headers, rc, provider, billing)
}
// HandleCompletions 处理 /v1/completions 请求
func HandleCompletions(ctx context.Context, body []byte, path string, headers http.Header, rc *RelayContext, provider common.DataProvider, billing common.BillingProvider) (*common.Usage, *BillingResult, error) {
return RelayHandler(ctx, body, path, headers, rc, provider, billing)
}
// HandleResponses 处理 /v1/responses 请求(OpenAI Responses API)
func HandleResponses(ctx context.Context, body []byte, path string, headers http.Header, rc *RelayContext, provider common.DataProvider, billing common.BillingProvider) (*common.Usage, *BillingResult, error) {
return RelayHandler(ctx, body, path, headers, rc, provider, billing)
}
// HandleModels 处理 /v1/models 请求(列出可用模型)
// tenantID 用于过滤该租户有权使用的模型,apiKeyID 用于进一步按 Key 的模型范围过滤
// userID 用于进一步按成员的模型范围过滤
func HandleModels(ctx context.Context, tenantID, apiKeyID, userID int64, provider common.DataProvider) (*dto.ModelsResponse, error) {
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID, userID)
if err != nil {
return nil, err
}
data := make([]dto.ModelDTO, 0, len(models))
for _, m := range models {
item := dto.ModelDTO{
ID: m.ModelId,
Object: "model",
Created: 0,
OwnedBy: "platform",
ModelName: m.ModelName,
Category: m.Category,
ContextWindow: m.MaxContextTokens,
MaxOutputTokens: m.MaxOutputTokens,
Capabilities: m.Capabilities,
Modalities: buildModalities(m.Category, m.Capabilities),
}
data = append(data, item)
}
return &dto.ModelsResponse{
Object: "list",
Data: data,
}, nil
}
// HandleModelDetail 处理 /v1/models/{model_id} 请求(获取单个模型详情)
// apiKeyID / userID 用于进一步按 API Key / 成员的模型范围过滤
func HandleModelDetail(ctx context.Context, tenantID, apiKeyID, userID int64, modelName string, provider common.DataProvider) (*dto.ModelDetailResponse, error) {
detail, err := provider.GetModelDetail(ctx, tenantID, apiKeyID, userID, modelName)
if err != nil {
return nil, err
}
return &dto.ModelDetailResponse{
ID: detail.ID,
Object: detail.Object,
Created: detail.Created,
OwnedBy: detail.OwnedBy,
ModelName: detail.ModelName,
Description: detail.Description,
Category: detail.Category,
Status: detail.Status,
ContextWindow: detail.MaxContextTokens,
MaxOutputTokens: detail.MaxOutputTokens,
Capabilities: detail.Capabilities,
Modalities: buildModalities(detail.Category, detail.Capabilities),
Deprecated: detail.Status == "deprecated",
}, nil
}
func buildModalities(category string, capabilities map[string]bool) *dto.ModelModalities {
input := []string{"text"}
output := []string{"text"}
switch category {
case "embedding":
output = []string{"embedding"}
case "image":
input = []string{"text"}
output = []string{"image"}
case "audio":
input = []string{"audio"}
output = []string{"text"}
}
if capabilities != nil {
if capabilities["vision"] {
input = appendUnique(input, "image")
}
if capabilities["audio_input"] {
input = appendUnique(input, "audio")
}
if capabilities["audio_output"] {
output = appendUnique(output, "audio")
}
if capabilities["pdf_input"] {
input = appendUnique(input, "pdf")
}
}
return &dto.ModelModalities{Input: input, Output: output}
}
func appendUnique(slice []string, item string) []string {
for _, s := range slice {
if s == item {
return slice
}
}
return append(slice, item)
}
// WriteRelayError 写入 relay 错误响应(导出,供 GoFrame handler 调用)
func WriteRelayError(w http.ResponseWriter, err error) {
// 流式中断(客户端已断开):降级为 INFO,跳过写响应
if errors.Is(err, common.ErrStreamInterrupted) {
g.Log().Infof(context.Background(), "[RelayError] Client disconnected during stream")
return
}
if responseCommitted(w) {
return
}
// adaptor 已直接写入响应体(如 Gemini 原生格式透传),跳过二次写入
var prewritten *constant.RelayError
if errors.As(err, &prewritten) && prewritten.ResponseWritten {
return
}
var relayErr *constant.RelayError
var rateLimitErr *RelayErrorWithRateLimit
statusCode := http.StatusInternalServerError
errMsg := helper.SafeUpstreamErrorMessage(err)
errType := "internal_error"
if errors.As(err, &rateLimitErr) {
statusCode = rateLimitErr.StatusCode
errMsg = rateLimitErr.Message
errType = "rate_limit_error"
w.Header().Set("X-RateLimit-Limit", "0")
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", rateLimitErr.Remaining))
w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", rateLimitErr.ResetAt))
} else if errors.As(err, &relayErr) {
statusCode = relayErr.StatusCode
errMsg = relayErr.Message
if relayErr.Cause != nil {
// 传输层错误(client.Do 的 *url.Error)的 Cause 含上游域名,必须脱敏后再暴露给用户;
// 日志侧仍用 originalError=%v 打印完整错误供运维定位。
errMsg = relayErr.Message + ": " + helper.SafeUpstreamErrorMessage(relayErr.Cause)
}
errType = relayErr.Type
}
if statusCode < 100 || statusCode > 599 {
statusCode = http.StatusInternalServerError
}
// 无可用渠道是正常业务条件,已在 handleChannelUnavailable 中以 Warning 记录,此处跳过避免重复日志;
// 其余 5xx 为真实错误,保留 ERROR 但禁用堆栈打印(此处调用栈固定,无调试价值)
if statusCode >= 500 && !errors.Is(err, common.ErrChannelUnavailable) {
g.Log().Stack(false).Errorf(context.Background(), "[RelayError] statusCode=%d type=%s message=%s originalError=%v",
statusCode, errType, errMsg, err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
errBody, _ := json.Marshal(map[string]any{
"error": map[string]any{
"message": errMsg,
"type": errType,
"param": nil,
"code": nil,
},
})
_, _ = w.Write(errBody)
}
type responseCommitter interface {
ResponseCommitted() bool
}
func responseCommitted(w http.ResponseWriter) bool {
committer, ok := w.(responseCommitter)
return ok && committer.ResponseCommitted()
}