package gemini import ( "errors" "fmt" "io" "net/http" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/relayconvert" "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" ) func GeminiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { defer service.CloseResponseBodyGracefully(resp) responseBody, err := io.ReadAll(resp.Body) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } logger.LogDebug(c, "Gemini responses response body: %s", responseBody) var geminiResponse dto.GeminiChatResponse if err := common.Unmarshal(responseBody, &geminiResponse); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } markGeminiGoogleSearchCall(c, &geminiResponse) if len(geminiResponse.Candidates) == 0 { usage := buildUsageFromGeminiResponse(c, info, &geminiResponse) if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { common.SetContextKey(c, constant.ContextKeyAdminRejectReason, fmt.Sprintf("gemini_block_reason=%s", *geminiResponse.PromptFeedback.BlockReason)) return &usage, types.NewOpenAIError( errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest, ) } common.SetContextKey(c, constant.ContextKeyAdminRejectReason, "gemini_empty_candidates") return &usage, types.NewOpenAIError( errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError, ) } chatResp := responseGeminiChat2OpenAI(c, &geminiResponse) chatResp.Model = info.UpstreamModelName if responseID := helper.GetResponseID(c); responseID != "" { chatResp.Id = responseID } usage := buildUsageFromGeminiResponse(c, info, &geminiResponse) chatResp.Usage = usage convertResult, err := relayconvert.ConvertResponse(c, info, types.RelayFormatOpenAIResponses, chatResp) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } responsesResp, ok := convertResult.Value.(*dto.OpenAIResponsesResponse) if !ok { return nil, types.NewOpenAIError(fmt.Errorf("expected OpenAI responses response, got %T", convertResult.Value), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } responsesUsage := convertResult.Usage if responsesUsage == nil || responsesUsage.TotalTokens == 0 { responsesResp.Usage = relayconvert.UsageFromChatUsage(&usage) } responseBody, err = common.Marshal(responsesResp) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeJsonMarshalFailed, http.StatusInternalServerError) } service.IOCopyBytesGracefully(c, resp, responseBody) return &usage, nil } func GeminiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { responseID := helper.GetResponseID(c) created := common.GetTimestamp() state, err := relayconvert.NewResponseStreamState(types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses, relayconvert.ResponseStreamOptions{ ID: responseID, Model: info.UpstreamModelName, Created: created, }) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) } finishReason := constant.FinishReasonStop toolCallIndexByChoice := make(map[int]map[string]int) nextToolCallIndexByChoice := make(map[int]int) var streamErr *types.NewAPIError sendEvent := func(event relayconvert.ChatToResponsesStreamEvent) bool { data, err := common.Marshal(event.Payload) if err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeJsonMarshalFailed, http.StatusInternalServerError) return false } helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: event.Type}, string(data)) return true } sendChunk := func(chunk *dto.ChatCompletionsStreamResponse) bool { results, err := relayconvert.ConvertStreamResponseChunk(c, info, state, chunk) if err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } for _, result := range results { event, ok := result.Value.(relayconvert.ChatToResponsesStreamEvent) if !ok { streamErr = types.NewOpenAIError(fmt.Errorf("expected OAI responses stream event, got %T", result.Value), types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } if !sendEvent(event) { return false } } return true } usage, streamAPIError := geminiStreamHandler(c, info, resp, func(data string, geminiResponse *dto.GeminiChatResponse) bool { response, isStop := streamResponseGeminiChat2OpenAI(geminiResponse) response.Id = responseID response.Created = created response.Model = info.UpstreamModelName if response.IsToolCall() { finishReason = constant.FinishReasonToolCalls } for choiceIdx := range response.Choices { choiceKey := response.Choices[choiceIdx].Index for toolIdx := range response.Choices[choiceIdx].Delta.ToolCalls { tool := &response.Choices[choiceIdx].Delta.ToolCalls[toolIdx] if tool.ID == "" { continue } indexByID := toolCallIndexByChoice[choiceKey] if indexByID == nil { indexByID = make(map[string]int) toolCallIndexByChoice[choiceKey] = indexByID } if idx, ok := indexByID[tool.ID]; ok { tool.SetIndex(idx) continue } idx := nextToolCallIndexByChoice[choiceKey] nextToolCallIndexByChoice[choiceKey] = idx + 1 indexByID[tool.ID] = idx tool.SetIndex(idx) } } if !sendChunk(response) { return false } if isStop { return sendChunk(helper.GenerateStopResponse(responseID, created, info.UpstreamModelName, finishReason)) } return true }) if streamAPIError != nil { return usage, streamAPIError } if streamErr != nil { return nil, streamErr } if usage != nil { state.SetUsage(usage) } finalResults, err := relayconvert.FinalizeStreamResponse(c, info, state) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) } for _, result := range finalResults { event, ok := result.Value.(relayconvert.ChatToResponsesStreamEvent) if !ok { return nil, types.NewOpenAIError(fmt.Errorf("expected OAI responses stream event, got %T", result.Value), types.ErrorCodeBadResponse, http.StatusInternalServerError) } if !sendEvent(event) { return nil, streamErr } } return usage, nil }