131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
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"
|
|
)
|
|
|
|
// HandleGeminiGenerateContent 处理 /v1beta/models/{model}:generateContent 请求(Gemini 原生格式)
|
|
func HandleGeminiGenerateContent(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)
|
|
}
|
|
|
|
// HandleGeminiModels 处理 GET /v1beta/models 请求(Gemini 原生格式模型列表)
|
|
func HandleGeminiModels(ctx context.Context, tenantID, apiKeyID, userID int64, provider common.DataProvider) (*dto.GeminiModelsResponse, error) {
|
|
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]dto.GeminiModel, 0, len(models))
|
|
for _, m := range models {
|
|
items = append(items, dto.GeminiModel{
|
|
Name: fmt.Sprintf("models/%s", m.ModelId),
|
|
BaseModelId: m.ModelId,
|
|
DisplayName: m.ModelName,
|
|
SupportedGenerationMethods: []string{"generateContent", "countTokens"},
|
|
})
|
|
}
|
|
|
|
return &dto.GeminiModelsResponse{
|
|
Models: items,
|
|
}, nil
|
|
}
|
|
|
|
// HandleGeminiModelDetail 处理 GET /v1beta/models/{model} 请求(Gemini 原生格式模型详情)
|
|
// apiKeyID / userID 用于进一步按 API Key / 成员的模型范围过滤
|
|
func HandleGeminiModelDetail(ctx context.Context, tenantID, apiKeyID, userID int64, modelName string, provider common.DataProvider) (*dto.GeminiModel, error) {
|
|
detail, err := provider.GetModelDetail(ctx, tenantID, apiKeyID, userID, modelName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dto.GeminiModel{
|
|
Name: fmt.Sprintf("models/%s", detail.ID),
|
|
BaseModelId: detail.ID,
|
|
DisplayName: detail.ModelName,
|
|
Description: detail.Description,
|
|
InputTokenLimit: detail.MaxContextTokens,
|
|
OutputTokenLimit: detail.MaxOutputTokens,
|
|
SupportedGenerationMethods: []string{"generateContent", "countTokens"},
|
|
}, nil
|
|
}
|
|
|
|
// WriteGeminiRelayError 写入 Gemini 格式的错误响应
|
|
func WriteGeminiRelayError(w http.ResponseWriter, err error) {
|
|
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)
|
|
errStatus := "INTERNAL"
|
|
|
|
if errors.As(err, &rateLimitErr) {
|
|
statusCode = rateLimitErr.StatusCode
|
|
errMsg = rateLimitErr.Message
|
|
errStatus = "RESOURCE_EXHAUSTED"
|
|
} 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)
|
|
}
|
|
switch statusCode {
|
|
case 401:
|
|
errStatus = "UNAUTHENTICATED"
|
|
case 403:
|
|
errStatus = "PERMISSION_DENIED"
|
|
case 400:
|
|
errStatus = "INVALID_ARGUMENT"
|
|
case 404:
|
|
errStatus = "NOT_FOUND"
|
|
default:
|
|
errStatus = "INTERNAL"
|
|
}
|
|
}
|
|
|
|
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(), "[GeminiRelayError] statusCode=%d status=%s message=%s originalError=%v",
|
|
statusCode, errStatus, errMsg, err)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
|
|
errBody, _ := json.Marshal(map[string]any{
|
|
"error": map[string]any{
|
|
"code": statusCode,
|
|
"message": errMsg,
|
|
"status": errStatus,
|
|
},
|
|
})
|
|
_, _ = w.Write(errBody)
|
|
}
|