| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924 |
- package service
- import (
- "bufio"
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
- "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
- "github.com/gin-gonic/gin"
- )
- // ForwardAsChatCompletions serves OpenAI Chat Completions clients through
- // Gemini accounts. It keeps the client-facing response in Chat Completions
- // format while routing the upstream call through Gemini native endpoints.
- func (s *GeminiMessagesCompatService) ForwardAsChatCompletions(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- body []byte,
- ) (*ForwardResult, error) {
- startTime := time.Now()
- var ccReq apicompat.ChatCompletionsRequest
- if err := json.Unmarshal(body, &ccReq); err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
- }
- if strings.TrimSpace(ccReq.Model) == "" {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
- }
- originalModel := ccReq.Model
- clientStream := ccReq.Stream
- includeUsage := ccReq.StreamOptions != nil && ccReq.StreamOptions.IncludeUsage
- responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
- }
- anthropicReq, err := apicompat.ResponsesToAnthropicRequest(responsesReq)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
- }
- anthropicReq.Stream = clientStream
- claudeBody, err := json.Marshal(anthropicReq)
- if err != nil {
- return nil, fmt.Errorf("marshal chat completions compat request: %w", err)
- }
- return s.forwardClaudeBodyAsChatCompletions(ctx, c, account, claudeBody, originalModel, clientStream, includeUsage, startTime, body)
- }
- func (s *GeminiMessagesCompatService) forwardClaudeBodyAsChatCompletions(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- claudeBody []byte,
- originalModel string,
- clientStream bool,
- includeUsage bool,
- startTime time.Time,
- originalChatBody []byte,
- ) (*ForwardResult, error) {
- var req struct {
- Model string `json:"model"`
- Stream bool `json:"stream"`
- }
- if err := json.Unmarshal(claudeBody, &req); err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
- }
- if strings.TrimSpace(req.Model) == "" {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
- }
- mappedModel := req.Model
- if account.Type == AccountTypeAPIKey || account.Type == AccountTypeServiceAccount {
- mappedModel = account.GetMappedModel(req.Model)
- }
- geminiReq, err := convertClaudeMessagesToGeminiGenerateContent(claudeBody)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
- }
- geminiReq = ensureGeminiFunctionCallThoughtSignatures(geminiReq)
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- useUpstreamStream := clientStream
- if account.Type == AccountTypeOAuth && !clientStream && strings.TrimSpace(account.GetCredential("project_id")) != "" {
- useUpstreamStream = true
- }
- buildReq, requestIDHeader := s.buildGeminiChatCompletionsUpstreamRequestFunc(
- account,
- mappedModel,
- geminiReq,
- clientStream,
- useUpstreamStream,
- )
- var resp *http.Response
- for attempt := 1; attempt <= geminiMaxRetries; attempt++ {
- upstreamReq, idHeader, err := buildReq(ctx)
- if err != nil {
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
- return nil, err
- }
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", err.Error())
- }
- requestIDHeader = idHeader
- resp, err = s.httpUpstream.Do(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency)
- if err != nil {
- if local := AccountTrafficFailover(err); local != nil {
- return nil, local
- }
- safeErr := sanitizeUpstreamErrorMessage(err.Error())
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: 0,
- Kind: "request_error",
- Message: safeErr,
- })
- if attempt < geminiMaxRetries {
- logger.LegacyPrintf("service.gemini_chat_completions", "Gemini account %d: upstream request failed, retry %d/%d: %v", account.ID, attempt, geminiMaxRetries, err)
- sleepGeminiBackoff(attempt)
- continue
- }
- setOpsUpstreamError(c, 0, safeErr, "")
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed after retries: "+safeErr)
- }
- if matched, rebuilt := s.checkErrorPolicyInLoop(ctx, account, resp, mappedModel); matched {
- resp = rebuilt
- break
- } else {
- resp = rebuilt
- }
- if resp.StatusCode >= 400 && s.shouldRetryGeminiUpstreamError(account, resp.StatusCode) {
- respBody := s.readUpstreamErrorBody(resp)
- _ = resp.Body.Close()
- if resp.StatusCode == http.StatusForbidden && isGeminiInsufficientScope(resp.Header, respBody) {
- resp = &http.Response{
- StatusCode: resp.StatusCode,
- Header: resp.Header.Clone(),
- Body: io.NopCloser(bytes.NewReader(respBody)),
- }
- break
- }
- if resp.StatusCode == http.StatusTooManyRequests {
- s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
- }
- if attempt < geminiMaxRetries {
- upstreamReqID := resp.Header.Get(requestIDHeader)
- if upstreamReqID == "" {
- upstreamReqID = resp.Header.Get("x-goog-request-id")
- }
- upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
- upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: upstreamReqID,
- Kind: "retry",
- Message: upstreamMsg,
- })
- logger.LegacyPrintf("service.gemini_chat_completions", "Gemini account %d: upstream status %d, retry %d/%d", account.ID, resp.StatusCode, attempt, geminiMaxRetries)
- sleepGeminiBackoff(attempt)
- continue
- }
- resp = &http.Response{
- StatusCode: resp.StatusCode,
- Header: resp.Header.Clone(),
- Body: io.NopCloser(bytes.NewReader(respBody)),
- }
- break
- }
- break
- }
- defer func() { _ = resp.Body.Close() }()
- requestID := resp.Header.Get(requestIDHeader)
- if requestID == "" {
- requestID = resp.Header.Get("x-goog-request-id")
- }
- if requestID != "" {
- c.Header("x-request-id", requestID)
- }
- reasoningEffort := extractCCReasoningEffortFromBody(originalChatBody, mappedModel)
- // 国产模型默认 effort 补充(本路径上游是 Gemini,不会命中 passback-required)。
- // 保持与 OpenAI 网关路径调用模式一致,便于未来上游变异时语义一致。
- reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, originalChatBody, mappedModel)
- if resp.StatusCode >= 400 {
- respBody := s.readUpstreamErrorBody(resp)
- policy := ErrorPolicyNone
- if s.rateLimitService != nil {
- policy = s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, respBody, mappedModel)
- }
- // 与 messages 兼容层一致:只有 None / Matched 才走账号状态处理。
- // Skipped(池模式、或自定义错误码未命中)与 TempUnscheduled 已由策略层裁决完毕。
- if policy == ErrorPolicyNone || policy == ErrorPolicyMatched {
- s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
- }
- evBody := unwrapIfNeeded(account.Type == AccountTypeOAuth, respBody)
- if s.shouldFailoverGeminiUpstreamError(resp.StatusCode) {
- upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(evBody)))
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: requestID,
- Kind: "failover",
- Message: upstreamMsg,
- })
- return nil, &UpstreamFailoverError{
- StatusCode: resp.StatusCode,
- ResponseBody: evBody,
- RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
- }
- }
- if policy == ErrorPolicySkipped && account.IsCustomErrorCodesEnabled() {
- return nil, s.writeGeminiCustomCodeSkippedError(c, account, resp.StatusCode, requestID, evBody, func() {
- _ = s.writeChatCompletionsError(c, http.StatusInternalServerError, "api_error", geminiCustomCodeSkippedClientMessage)
- })
- }
- return nil, s.writeGeminiChatCompletionsMappedError(c, account, resp.StatusCode, requestID, evBody)
- }
- var usage *ClaudeUsage
- var firstTokenMs *int
- if clientStream {
- streamRes, err := s.handleChatCompletionsStreamingResponseFromGemini(c, resp, startTime, originalModel, account.Type == AccountTypeOAuth, includeUsage)
- if err != nil {
- return nil, err
- }
- usage = streamRes.usage
- firstTokenMs = streamRes.firstTokenMs
- } else if useUpstreamStream {
- collected, usageObj, err := collectGeminiSSE(resp.Body, account.Type == AccountTypeOAuth)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Failed to read upstream stream")
- }
- collectedBytes, _ := json.Marshal(collected)
- chatResp, usageObj2, err := geminiResponseToChatCompletions(collected, originalModel, collectedBytes, usageObj)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response")
- }
- c.JSON(http.StatusOK, chatResp)
- usage = usageObj2
- } else {
- usageResp, err := s.handleChatCompletionsNonStreamingResponseFromGemini(c, resp, originalModel, account.Type == AccountTypeOAuth)
- if err != nil {
- return nil, err
- }
- usage = usageResp
- }
- if usage == nil {
- usage = &ClaudeUsage{}
- }
- imageCount := 0
- imageInputSize := s.extractImageInputSize(claudeBody)
- imageSize := normalizeOpenAIImageSizeTier(imageInputSize)
- if isImageGenerationModel(originalModel) {
- imageCount = 1
- }
- return &ForwardResult{
- RequestID: requestID,
- UpstreamHeaders: resp.Header,
- Usage: *usage,
- Model: originalModel,
- UpstreamModel: mappedModel,
- Stream: clientStream,
- Duration: time.Since(startTime),
- FirstTokenMs: firstTokenMs,
- ReasoningEffort: reasoningEffort,
- ImageCount: imageCount,
- ImageSize: imageSize,
- ImageInputSize: imageInputSize,
- ClientDisconnect: false,
- }, nil
- }
- func (s *GeminiMessagesCompatService) buildGeminiChatCompletionsUpstreamRequestFunc(
- account *Account,
- mappedModel string,
- geminiReq []byte,
- clientStream bool,
- useUpstreamStream bool,
- ) (func(context.Context) (*http.Request, string, error), string) {
- switch account.Type {
- case AccountTypeAPIKey:
- return func(ctx context.Context) (*http.Request, string, error) {
- apiKey := account.GetCredential("api_key")
- if strings.TrimSpace(apiKey) == "" {
- return nil, "", errors.New("gemini api_key not configured")
- }
- baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
- normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return nil, "", err
- }
- action := "generateContent"
- if clientStream {
- action = "streamGenerateContent"
- }
- fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, action, clientStream)
- if err != nil {
- return nil, "", err
- }
- restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
- upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
- if err != nil {
- return nil, "", err
- }
- upstreamReq.Header.Set("Content-Type", "application/json")
- upstreamReq.Header.Set("x-goog-api-key", apiKey)
- return upstreamReq, "x-request-id", nil
- }, "x-request-id"
- case AccountTypeOAuth:
- return func(ctx context.Context) (*http.Request, string, error) {
- if s.tokenProvider == nil {
- return nil, "", errors.New("gemini token provider not configured")
- }
- accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
- if err != nil {
- return nil, "", err
- }
- projectID := strings.TrimSpace(account.GetCredential("project_id"))
- action := "generateContent"
- if useUpstreamStream {
- action = "streamGenerateContent"
- }
- if projectID != "" {
- baseURL, err := s.validateUpstreamBaseURL(geminicli.GeminiCliBaseURL)
- if err != nil {
- return nil, "", err
- }
- fullURL := fmt.Sprintf("%s/v1internal:%s", strings.TrimRight(baseURL, "/"), action)
- if useUpstreamStream {
- fullURL += "?alt=sse"
- }
- var inner any
- if err := json.Unmarshal(geminiReq, &inner); err != nil {
- return nil, "", fmt.Errorf("failed to parse gemini request: %w", err)
- }
- wrappedBytes, _ := json.Marshal(map[string]any{
- "model": mappedModel,
- "project": projectID,
- "request": inner,
- })
- upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(wrappedBytes))
- if err != nil {
- return nil, "", err
- }
- upstreamReq.Header.Set("Content-Type", "application/json")
- upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
- upstreamReq.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent)
- return upstreamReq, "x-request-id", nil
- }
- baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
- normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return nil, "", err
- }
- fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, action, useUpstreamStream)
- if err != nil {
- return nil, "", err
- }
- restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
- upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
- if err != nil {
- return nil, "", err
- }
- upstreamReq.Header.Set("Content-Type", "application/json")
- upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
- return upstreamReq, "x-request-id", nil
- }, "x-request-id"
- case AccountTypeServiceAccount:
- return func(ctx context.Context) (*http.Request, string, error) {
- if s.tokenProvider == nil {
- return nil, "", errors.New("gemini token provider not configured")
- }
- accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
- if err != nil {
- return nil, "", err
- }
- action := "generateContent"
- if clientStream {
- action = "streamGenerateContent"
- }
- fullURL, err := buildVertexGeminiURL(account.VertexProjectID(), account.VertexLocation(mappedModel), mappedModel, action, clientStream)
- if err != nil {
- return nil, "", err
- }
- restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
- upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
- if err != nil {
- return nil, "", err
- }
- upstreamReq.Header.Set("Content-Type", "application/json")
- upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
- return upstreamReq, "x-request-id", nil
- }, "x-request-id"
- default:
- return func(context.Context) (*http.Request, string, error) {
- return nil, "", fmt.Errorf("unsupported account type: %s", account.Type)
- }, "x-request-id"
- }
- }
- func (s *GeminiMessagesCompatService) handleChatCompletionsNonStreamingResponseFromGemini(
- c *gin.Context,
- resp *http.Response,
- originalModel string,
- isOAuth bool,
- ) (*ClaudeUsage, error) {
- respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
- if err != nil {
- return nil, err
- }
- if isOAuth {
- if unwrappedBody, uwErr := unwrapGeminiResponse(respBody); uwErr == nil {
- respBody = unwrappedBody
- }
- }
- var geminiResp map[string]any
- if err := json.Unmarshal(respBody, &geminiResp); err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response")
- }
- chatResp, usage, err := geminiResponseToChatCompletions(geminiResp, originalModel, respBody, nil)
- if err != nil {
- return nil, s.writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response")
- }
- responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
- c.JSON(http.StatusOK, chatResp)
- return usage, nil
- }
- func geminiResponseToChatCompletions(
- geminiResp map[string]any,
- originalModel string,
- rawData []byte,
- usageOverride *ClaudeUsage,
- ) (*apicompat.ChatCompletionsResponse, *ClaudeUsage, error) {
- claudeRespMap, usage := convertGeminiToClaudeMessage(geminiResp, originalModel, rawData, true)
- if usageOverride != nil && (usageOverride.InputTokens > 0 || usageOverride.OutputTokens > 0 || usageOverride.CacheReadInputTokens > 0) {
- usage = usageOverride
- if usageMap, ok := claudeRespMap["usage"].(map[string]any); ok {
- usageMap["input_tokens"] = usage.InputTokens
- usageMap["output_tokens"] = usage.OutputTokens
- usageMap["cache_read_input_tokens"] = usage.CacheReadInputTokens
- }
- }
- claudeBytes, err := json.Marshal(claudeRespMap)
- if err != nil {
- return nil, nil, err
- }
- var anthropicResp apicompat.AnthropicResponse
- if err := json.Unmarshal(claudeBytes, &anthropicResp); err != nil {
- return nil, nil, err
- }
- responsesResp := apicompat.AnthropicToResponsesResponse(&anthropicResp)
- return apicompat.ResponsesToChatCompletions(responsesResp, originalModel), usage, nil
- }
- func (s *GeminiMessagesCompatService) handleChatCompletionsStreamingResponseFromGemini(
- c *gin.Context,
- resp *http.Response,
- startTime time.Time,
- originalModel string,
- isOAuth bool,
- includeUsage bool,
- ) (*geminiStreamResult, error) {
- if s.responseHeaderFilter != nil {
- responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
- }
- c.Writer.Header().Set("Content-Type", "text/event-stream")
- c.Writer.Header().Set("Cache-Control", "no-cache")
- c.Writer.Header().Set("Connection", "keep-alive")
- c.Writer.Header().Set("X-Accel-Buffering", "no")
- c.Writer.WriteHeader(http.StatusOK)
- flusher, ok := c.Writer.(http.Flusher)
- if !ok {
- return nil, errors.New("streaming not supported")
- }
- anthState := apicompat.NewAnthropicEventToResponsesState()
- anthState.Model = originalModel
- ccState := apicompat.NewResponsesEventToChatState()
- ccState.Model = originalModel
- ccState.IncludeUsage = includeUsage
- var usage ClaudeUsage
- var firstTokenMs *int
- firstChunk := true
- writeChatChunk := func(chunk apicompat.ChatCompletionsChunk) bool {
- sse, err := apicompat.ChatChunkToSSE(chunk)
- if err != nil {
- return false
- }
- if _, err := io.WriteString(c.Writer, sse); err != nil {
- return true
- }
- return false
- }
- emitAnthropicEvent := func(evt *apicompat.AnthropicStreamEvent) bool {
- responsesEvents := apicompat.AnthropicEventToResponsesEvents(evt, anthState)
- for _, resEvt := range responsesEvents {
- chunks := apicompat.ResponsesEventToChatChunks(&resEvt, ccState)
- for _, chunk := range chunks {
- if disconnected := writeChatChunk(chunk); disconnected {
- return true
- }
- }
- }
- flusher.Flush()
- return false
- }
- messageID := generateAnthropicMsgID()
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "message_start",
- Message: &apicompat.AnthropicResponse{
- ID: messageID,
- Type: "message",
- Role: "assistant",
- Model: originalModel,
- Content: []apicompat.AnthropicContentBlock{},
- StopReason: nil, // JSON null
- Usage: apicompat.AnthropicUsage{},
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- finishReason := ""
- sawToolUse := false
- nextBlockIndex := 0
- openBlockIndex := -1
- openBlockType := ""
- seenText := ""
- openToolIndex := -1
- openToolName := ""
- seenToolJSON := ""
- closeOpenBlock := func() bool {
- if openBlockIndex < 0 {
- return false
- }
- disconnected := emitAnthropicEvent(&apicompat.AnthropicStreamEvent{Type: "content_block_stop"})
- openBlockIndex = -1
- openBlockType = ""
- return disconnected
- }
- closeOpenTool := func() bool {
- if openToolIndex < 0 {
- return false
- }
- disconnected := emitAnthropicEvent(&apicompat.AnthropicStreamEvent{Type: "content_block_stop"})
- openToolIndex = -1
- openToolName = ""
- seenToolJSON = ""
- return disconnected
- }
- reader := bufio.NewReader(resp.Body)
- for {
- line, err := reader.ReadString('\n')
- if len(line) > 0 {
- trimmed := strings.TrimRight(line, "\r\n")
- if strings.HasPrefix(trimmed, "data:") {
- payload := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:"))
- if payload != "" && payload != "[DONE]" {
- rawBytes := []byte(payload)
- if isOAuth {
- if innerBytes, uwErr := unwrapGeminiResponse(rawBytes); uwErr == nil {
- rawBytes = innerBytes
- }
- }
- var geminiResp map[string]any
- if err := json.Unmarshal(rawBytes, &geminiResp); err == nil {
- if firstChunk {
- firstChunk = false
- ms := int(time.Since(startTime).Milliseconds())
- firstTokenMs = &ms
- }
- if fr := extractGeminiFinishReason(geminiResp); fr != "" {
- finishReason = fr
- }
- if u := extractGeminiUsage(rawBytes); u != nil {
- usage = *u
- }
- for _, part := range extractGeminiParts(geminiResp) {
- if text, ok := part["text"].(string); ok && text != "" {
- if openToolIndex >= 0 {
- if closeOpenTool() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- delta, newSeen := computeGeminiTextDelta(seenText, text)
- seenText = newSeen
- if delta == "" {
- continue
- }
- if openBlockType != "text" {
- if closeOpenBlock() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- idx := nextBlockIndex
- nextBlockIndex++
- openBlockIndex = idx
- openBlockType = "text"
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "content_block_start",
- Index: &idx,
- ContentBlock: &apicompat.AnthropicContentBlock{
- Type: "text",
- Text: "",
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "content_block_delta",
- Delta: &apicompat.AnthropicDelta{
- Type: "text_delta",
- Text: delta,
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- continue
- }
- if fc, ok := part["functionCall"].(map[string]any); ok && fc != nil {
- name, _ := fc["name"].(string)
- if strings.TrimSpace(name) == "" {
- name = "tool"
- }
- if closeOpenBlock() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- if openToolIndex >= 0 && openToolName != name {
- if closeOpenTool() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- if openToolIndex < 0 {
- idx := nextBlockIndex
- nextBlockIndex++
- openToolIndex = idx
- openToolName = name
- sawToolUse = true
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "content_block_start",
- Index: &idx,
- ContentBlock: &apicompat.AnthropicContentBlock{
- Type: "tool_use",
- ID: "toolu_" + randomHex(8),
- Name: name,
- Input: json.RawMessage(`{}`),
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- argsJSONText := "{}"
- switch v := fc["args"].(type) {
- case nil:
- case string:
- if strings.TrimSpace(v) != "" {
- argsJSONText = v
- }
- default:
- if b, err := json.Marshal(v); err == nil && len(b) > 0 {
- argsJSONText = string(b)
- }
- }
- delta, newSeen := computeGeminiTextDelta(seenToolJSON, argsJSONText)
- seenToolJSON = newSeen
- if delta != "" {
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "content_block_delta",
- Delta: &apicompat.AnthropicDelta{
- Type: "input_json_delta",
- PartialJSON: delta,
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- }
- }
- }
- }
- }
- }
- if errors.Is(err, io.EOF) {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("stream read error: %w", err)
- }
- }
- if closeOpenBlock() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- if closeOpenTool() {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- stopReason := mapGeminiFinishReasonToClaudeStopReason(finishReason)
- if sawToolUse {
- stopReason = "tool_use"
- }
- anthState.InputTokens = usage.InputTokens
- anthState.CacheReadInputTokens = usage.CacheReadInputTokens
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{
- Type: "message_delta",
- Delta: &apicompat.AnthropicDelta{
- Type: "message_delta",
- StopReason: stopReason,
- },
- Usage: &apicompat.AnthropicUsage{
- InputTokens: usage.InputTokens,
- OutputTokens: usage.OutputTokens,
- CacheReadInputTokens: usage.CacheReadInputTokens,
- },
- }) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- if emitAnthropicEvent(&apicompat.AnthropicStreamEvent{Type: "message_stop"}) {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- for _, resEvt := range apicompat.FinalizeAnthropicResponsesStream(anthState) {
- chunks := apicompat.ResponsesEventToChatChunks(&resEvt, ccState)
- for _, chunk := range chunks {
- if disconnected := writeChatChunk(chunk); disconnected {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- }
- for _, chunk := range apicompat.FinalizeResponsesChatStream(ccState) {
- if disconnected := writeChatChunk(chunk); disconnected {
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- }
- _, _ = io.WriteString(c.Writer, "data: [DONE]\n\n")
- flusher.Flush()
- return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
- }
- func (s *GeminiMessagesCompatService) writeGeminiChatCompletionsMappedError(
- c *gin.Context,
- account *Account,
- upstreamStatus int,
- upstreamRequestID string,
- body []byte,
- ) error {
- upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(body)))
- setOpsUpstreamError(c, upstreamStatus, upstreamMsg, "")
- if account != nil {
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: upstreamStatus,
- UpstreamRequestID: upstreamRequestID,
- Kind: "http_error",
- Message: upstreamMsg,
- })
- }
- if status, errType, errMsg, matched := applyErrorPassthroughRule(
- c,
- PlatformGemini,
- upstreamStatus,
- body,
- http.StatusBadGateway,
- "upstream_error",
- "Upstream request failed",
- ); matched {
- return s.writeChatCompletionsError(c, status, errType, errMsg)
- }
- statusCode := http.StatusBadGateway
- errType := "upstream_error"
- errMsg := "Upstream request failed"
- if mapped := mapGeminiErrorBodyToClaudeError(body); mapped != nil {
- if mapped.Type != "" {
- errType = mapped.Type
- }
- if mapped.Message != "" {
- errMsg = mapped.Message
- }
- if mapped.StatusCode > 0 {
- statusCode = mapped.StatusCode
- }
- }
- switch upstreamStatus {
- case http.StatusBadRequest:
- if statusCode == http.StatusBadGateway {
- statusCode = http.StatusBadRequest
- }
- if errType == "upstream_error" {
- errType = "invalid_request_error"
- }
- // 400 是确定性的请求错误:回传上游 message(已脱敏),客户端据此定位非法字段。
- if errMsg == "Upstream request failed" {
- if upstreamMsg != "" {
- errMsg = upstreamMsg
- } else {
- errMsg = "Invalid request"
- }
- }
- case http.StatusNotFound:
- statusCode = http.StatusNotFound
- if errType == "upstream_error" {
- errType = "not_found_error"
- }
- if errMsg == "Upstream request failed" {
- errMsg = "Resource not found"
- }
- case http.StatusTooManyRequests:
- statusCode = http.StatusTooManyRequests
- if errType == "upstream_error" {
- errType = "rate_limit_error"
- }
- if errMsg == "Upstream request failed" {
- errMsg = "Upstream rate limit exceeded, please retry later"
- }
- case 529:
- statusCode = http.StatusServiceUnavailable
- if errType == "upstream_error" {
- errType = "overloaded_error"
- }
- if errMsg == "Upstream request failed" {
- errMsg = "Upstream service overloaded, please retry later"
- }
- }
- if upstreamMsg != "" && errMsg == "Upstream request failed" {
- errMsg = upstreamMsg
- }
- return s.writeChatCompletionsError(c, statusCode, errType, errMsg)
- }
- func (s *GeminiMessagesCompatService) writeChatCompletionsError(c *gin.Context, status int, errType, message string) error {
- c.JSON(status, gin.H{
- "error": gin.H{
- "type": errType,
- "message": message,
- },
- })
- return fmt.Errorf("%s", message)
- }
|