Spaces:
Runtime error
Runtime error
| package controller | |
| import ( | |
| "bytes" | |
| "compress/gzip" | |
| "encoding/json" | |
| "fmt" | |
| "github.com/gin-gonic/gin" | |
| "github.com/samber/lo" | |
| "io" | |
| "kilo2api/common" | |
| "kilo2api/common/config" | |
| logger "kilo2api/common/loggger" | |
| "kilo2api/cycletls" | |
| "kilo2api/kilo-api" | |
| "kilo2api/model" | |
| "net/http" | |
| "strings" | |
| "time" | |
| ) | |
| const ( | |
| errServerErrMsg = "Service Unavailable" | |
| responseIDFormat = "chatcmpl-%s" | |
| ) | |
| // ChatForOpenAI @Summary OpenAI对话接口 | |
| // @Description OpenAI对话接口 | |
| // @Tags OpenAI | |
| // @Accept json | |
| // @Produce json | |
| // @Param req body model.OpenAIChatCompletionRequest true "OpenAI对话请求" | |
| // @Param Authorization header string true "Authorization API-KEY" | |
| // @Router /v1/chat/completions [post] | |
| func ChatForOpenAI(c *gin.Context) { | |
| client := cycletls.Init() | |
| defer safeClose(client) | |
| var openAIReq model.OpenAIChatCompletionRequest | |
| if err := c.BindJSON(&openAIReq); err != nil { | |
| logger.Errorf(c.Request.Context(), err.Error()) | |
| c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ | |
| OpenAIError: model.OpenAIError{ | |
| Message: "Invalid request parameters", | |
| Type: "request_error", | |
| Code: "500", | |
| }, | |
| }) | |
| return | |
| } | |
| openAIReq.RemoveEmptyContentMessages() | |
| modelInfo, b := common.GetModelInfo(openAIReq.Model) | |
| if !b { | |
| c.JSON(http.StatusBadRequest, model.OpenAIErrorResponse{ | |
| OpenAIError: model.OpenAIError{ | |
| Message: fmt.Sprintf("Model %s not supported", openAIReq.Model), | |
| Type: "invalid_request_error", | |
| Code: "invalid_model", | |
| }, | |
| }) | |
| return | |
| } | |
| if openAIReq.MaxTokens > modelInfo.MaxTokens { | |
| c.JSON(http.StatusBadRequest, model.OpenAIErrorResponse{ | |
| OpenAIError: model.OpenAIError{ | |
| Message: fmt.Sprintf("Max tokens %d exceeds limit %d", openAIReq.MaxTokens, modelInfo.MaxTokens), | |
| Type: "invalid_request_error", | |
| Code: "invalid_max_tokens", | |
| }, | |
| }) | |
| return | |
| } | |
| if openAIReq.Stream { | |
| handleStreamRequest(c, client, openAIReq, modelInfo) | |
| } else { | |
| handleNonStreamRequest(c, client, openAIReq, modelInfo) | |
| } | |
| } | |
| func handleNonStreamRequest(c *gin.Context, client cycletls.CycleTLS, openAIReq model.OpenAIChatCompletionRequest, modelInfo common.ModelInfo) { | |
| ctx := c.Request.Context() | |
| cookieManager := config.NewCookieManager() | |
| maxRetries := len(cookieManager.Cookies) | |
| cookie, err := cookieManager.GetRandomCookie() | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| for attempt := 0; attempt < maxRetries; attempt++ { | |
| requestBody, err := createRequestBody(c, &openAIReq, modelInfo) | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": "Failed to marshal request body"}) | |
| return | |
| } | |
| sseChan, err := kilo_api.MakeStreamChatRequest(c, client, jsonData, cookie, modelInfo) | |
| if err != nil { | |
| logger.Errorf(ctx, "MakeStreamChatRequest err on attempt %d: %v", attempt+1, err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| isRateLimit := false | |
| var delta string | |
| var assistantMsgContent string | |
| var shouldContinue bool | |
| thinkStartType := new(bool) | |
| thinkEndType := new(bool) | |
| SSELoop: | |
| for response := range sseChan { | |
| data := response.Data | |
| if data == "" { | |
| continue | |
| } | |
| if response.Done { | |
| switch { | |
| case common.IsUsageLimitExceeded(data): | |
| if config.CheatEnabled { | |
| split := strings.Split(cookie, "=") | |
| if len(split) == 2 { | |
| cookieSession := split[1] | |
| cheatResp, err := client.Do(config.CheatUrl, cycletls.Options{ | |
| Timeout: 10 * 60 * 60, | |
| Proxy: config.ProxyUrl, // 在每个请求中设置代理 | |
| Body: "", | |
| Headers: map[string]string{ | |
| "Cookie": cookieSession, | |
| }, | |
| }, "POST") | |
| if err != nil { | |
| logger.Errorf(ctx, "Cheat err Cookie: %s err: %v", cookie, err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| if cheatResp.Status == 200 { | |
| logger.Debug(c, fmt.Sprintf("Cheat Success Cookie: %s", cookie)) | |
| attempt-- // 抵消循环结束时的attempt++ | |
| break SSELoop | |
| } | |
| if cheatResp.Status == 402 { | |
| logger.Warnf(ctx, "Cookie Unlink Card Cookie: %s", cookie) | |
| } else { | |
| logger.Errorf(ctx, "Cheat err Cookie: %s Resp: %v", cookie, cheatResp.Body) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Cheat Resp.Status:%v Resp.Body:%v", cheatResp.Status, cheatResp.Body)}) | |
| return | |
| } | |
| } | |
| } | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie Usage limit exceeded, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| config.RemoveCookie(cookie) | |
| break SSELoop | |
| case common.IsServerError(data): | |
| logger.Errorf(ctx, errServerErrMsg) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": errServerErrMsg}) | |
| return | |
| case common.IsNotLogin(data): | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie Not Login, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| break SSELoop | |
| case common.IsRateLimit(data): | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie rate limited, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| config.AddRateLimitCookie(cookie, time.Now().Add(time.Duration(config.RateLimitCookieLockDuration)*time.Second)) | |
| break SSELoop | |
| } | |
| logger.Warnf(ctx, response.Data) | |
| return | |
| } | |
| logger.Debug(ctx, strings.TrimSpace(data)) | |
| streamDelta, streamShouldContinue := processNoStreamData(c, data, modelInfo, thinkStartType, thinkEndType) | |
| delta = streamDelta | |
| shouldContinue = streamShouldContinue | |
| // 处理事件流数据 | |
| if !shouldContinue { | |
| promptTokens := model.CountTokenText(string(jsonData), openAIReq.Model) | |
| completionTokens := model.CountTokenText(assistantMsgContent, openAIReq.Model) | |
| finishReason := "stop" | |
| c.JSON(http.StatusOK, model.OpenAIChatCompletionResponse{ | |
| ID: fmt.Sprintf(responseIDFormat, time.Now().Format("20060102150405")), | |
| Object: "chat.completion", | |
| Created: time.Now().Unix(), | |
| Model: openAIReq.Model, | |
| Choices: []model.OpenAIChoice{{ | |
| Message: model.OpenAIMessage{ | |
| Role: "assistant", | |
| Content: assistantMsgContent, | |
| }, | |
| FinishReason: &finishReason, | |
| }}, | |
| Usage: model.OpenAIUsage{ | |
| PromptTokens: promptTokens, | |
| CompletionTokens: completionTokens, | |
| TotalTokens: promptTokens + completionTokens, | |
| }, | |
| }) | |
| return | |
| } else { | |
| assistantMsgContent = assistantMsgContent + delta | |
| } | |
| } | |
| if !isRateLimit { | |
| return | |
| } | |
| // 获取下一个可用的cookie继续尝试 | |
| cookie, err = cookieManager.GetNextCookie() | |
| if err != nil { | |
| logger.Errorf(ctx, "No more valid cookies available after attempt %d", attempt+1) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| } | |
| logger.Errorf(ctx, "All cookies exhausted after %d attempts", maxRetries) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "All cookies are temporarily unavailable."}) | |
| return | |
| } | |
| func createRequestBody(c *gin.Context, openAIReq *model.OpenAIChatCompletionRequest, modelInfo common.ModelInfo) (map[string]interface{}, error) { | |
| client := cycletls.Init() | |
| defer safeClose(client) | |
| if config.PRE_MESSAGES_JSON != "" { | |
| err := openAIReq.PrependMessagesFromJSON(config.PRE_MESSAGES_JSON) | |
| if err != nil { | |
| return nil, fmt.Errorf("PrependMessagesFromJSON err: %v JSON:%s", err, config.PRE_MESSAGES_JSON) | |
| } | |
| } | |
| if openAIReq.MaxTokens <= 1 { | |
| openAIReq.MaxTokens = 8192 | |
| } | |
| var data []byte | |
| var err error | |
| if modelInfo.Source == "claude" { | |
| claudeRequest, err := model.ConvertOpenAIToClaudeRequest(*openAIReq, modelInfo) | |
| if err != nil { | |
| return nil, fmt.Errorf("ConvertOpenAIToClaudeRequest err: %v", err) | |
| } | |
| data, err = json.Marshal(claudeRequest) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } else if modelInfo.Source == "openrouter" { | |
| geminiReq, err := model.ConvertOpenAIToGeminiRequest(*openAIReq, modelInfo) | |
| if err != nil { | |
| return nil, fmt.Errorf("ConvertOpenAIToGeminiRequest err: %v", err) | |
| } | |
| data, err = json.Marshal(geminiReq) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| requestBody := make(map[string]interface{}) | |
| err = json.Unmarshal(data, &requestBody) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // 创建请求体 | |
| logger.Debug(c.Request.Context(), fmt.Sprintf("RequestBody: %v", requestBody)) | |
| return requestBody, nil | |
| } | |
| // createStreamResponse 创建流式响应 | |
| func createStreamResponse(responseId, modelName string, jsonData []byte, delta model.OpenAIDelta, finishReason *string) model.OpenAIChatCompletionResponse { | |
| promptTokens := model.CountTokenText(string(jsonData), modelName) | |
| completionTokens := model.CountTokenText(delta.Content, modelName) | |
| return model.OpenAIChatCompletionResponse{ | |
| ID: responseId, | |
| Object: "chat.completion.chunk", | |
| Created: time.Now().Unix(), | |
| Model: modelName, | |
| Choices: []model.OpenAIChoice{ | |
| { | |
| Index: 0, | |
| Delta: delta, | |
| FinishReason: finishReason, | |
| }, | |
| }, | |
| Usage: model.OpenAIUsage{ | |
| PromptTokens: promptTokens, | |
| CompletionTokens: completionTokens, | |
| TotalTokens: promptTokens + completionTokens, | |
| }, | |
| } | |
| } | |
| // handleDelta 处理消息字段增量 | |
| func handleDelta(c *gin.Context, delta string, responseId, modelName string, jsonData []byte) error { | |
| // 创建基础响应 | |
| createResponse := func(content string) model.OpenAIChatCompletionResponse { | |
| return createStreamResponse( | |
| responseId, | |
| modelName, | |
| jsonData, | |
| model.OpenAIDelta{Content: content, Role: "assistant"}, | |
| nil, | |
| ) | |
| } | |
| // 发送基础事件 | |
| var err error | |
| if err = sendSSEvent(c, createResponse(delta)); err != nil { | |
| return err | |
| } | |
| return err | |
| } | |
| // handleMessageResult 处理消息结果 | |
| func handleMessageResult(c *gin.Context, responseId, modelName string, jsonData []byte) bool { | |
| finishReason := "stop" | |
| var delta string | |
| promptTokens := 0 | |
| completionTokens := 0 | |
| streamResp := createStreamResponse(responseId, modelName, jsonData, model.OpenAIDelta{Content: delta, Role: "assistant"}, &finishReason) | |
| streamResp.Usage = model.OpenAIUsage{ | |
| PromptTokens: promptTokens, | |
| CompletionTokens: completionTokens, | |
| TotalTokens: promptTokens + completionTokens, | |
| } | |
| if err := sendSSEvent(c, streamResp); err != nil { | |
| logger.Warnf(c.Request.Context(), "sendSSEvent err: %v", err) | |
| return false | |
| } | |
| c.SSEvent("", " [DONE]") | |
| return false | |
| } | |
| // sendSSEvent 发送SSE事件 | |
| func sendSSEvent(c *gin.Context, response model.OpenAIChatCompletionResponse) error { | |
| jsonResp, err := json.Marshal(response) | |
| if err != nil { | |
| logger.Errorf(c.Request.Context(), "Failed to marshal response: %v", err) | |
| return err | |
| } | |
| c.SSEvent("", " "+string(jsonResp)) | |
| c.Writer.Flush() | |
| return nil | |
| } | |
| func handleStreamRequest(c *gin.Context, client cycletls.CycleTLS, openAIReq model.OpenAIChatCompletionRequest, modelInfo common.ModelInfo) { | |
| c.Header("Content-Type", "text/event-stream") | |
| c.Header("Cache-Control", "no-cache") | |
| c.Header("Connection", "keep-alive") | |
| responseId := fmt.Sprintf(responseIDFormat, time.Now().Format("20060102150405")) | |
| ctx := c.Request.Context() | |
| cookieManager := config.NewCookieManager() | |
| maxRetries := len(cookieManager.Cookies) | |
| cookie, err := cookieManager.GetRandomCookie() | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| thinkStartType := new(bool) | |
| thinkEndType := new(bool) | |
| c.Stream(func(w io.Writer) bool { | |
| for attempt := 0; attempt < maxRetries; attempt++ { | |
| requestBody, err := createRequestBody(c, &openAIReq, modelInfo) | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": err.Error()}) | |
| return false | |
| } | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| c.JSON(500, gin.H{"error": "Failed to marshal request body"}) | |
| return false | |
| } | |
| sseChan, err := kilo_api.MakeStreamChatRequest(c, client, jsonData, cookie, modelInfo) | |
| if err != nil { | |
| logger.Errorf(ctx, "MakeStreamChatRequest err on attempt %d: %v", attempt+1, err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return false | |
| } | |
| isRateLimit := false | |
| SSELoop: | |
| for response := range sseChan { | |
| if response.Status == 403 { | |
| compressedData := []byte(response.Data) | |
| compressedReader := bytes.NewReader(compressedData) | |
| gzipReader, err := gzip.NewReader(compressedReader) | |
| if err != nil { | |
| logger.Errorf(c.Request.Context(), "Failed to create gzip reader: %v", err) | |
| c.JSON(500, gin.H{"error": "Failed to create gzip reader"}) | |
| return false | |
| } | |
| // 读取解压后的数据 | |
| uncompressedData, err := io.ReadAll(gzipReader) | |
| if err != nil { | |
| logger.Errorf(c.Request.Context(), "Failed to read uncompressed data: %v", err) | |
| c.JSON(500, gin.H{"error": "Failed to read uncompressed data"}) | |
| return false | |
| } | |
| gzipReader.Close() | |
| logger.Errorf(c, string(uncompressedData)) | |
| //c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"}) | |
| config.RemoveCookie(cookie) | |
| isRateLimit = true | |
| break SSELoop | |
| } | |
| data := response.Data | |
| if data == "" { | |
| continue | |
| } | |
| if response.Done { | |
| switch { | |
| case common.IsUsageLimitExceeded(data): | |
| if config.CheatEnabled { | |
| split := strings.Split(cookie, "=") | |
| if len(split) == 2 { | |
| cookieSession := split[1] | |
| cheatResp, err := client.Do(config.CheatUrl, cycletls.Options{ | |
| Timeout: 10 * 60 * 60, | |
| Proxy: config.ProxyUrl, // 在每个请求中设置代理 | |
| Body: "", | |
| Headers: map[string]string{ | |
| "Cookie": cookieSession, | |
| }, | |
| }, "POST") | |
| if err != nil { | |
| logger.Errorf(ctx, "Cheat err Cookie: %s err: %v", cookie, err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return false | |
| } | |
| if cheatResp.Status == 200 { | |
| logger.Debug(c, fmt.Sprintf("Cheat Success Cookie: %s", cookie)) | |
| attempt-- // 抵消循环结束时的attempt++ | |
| break SSELoop | |
| } | |
| if cheatResp.Status == 402 { | |
| logger.Warnf(ctx, "Cheat failed. Cookie: %s Resp: %v", cookie, cheatResp.Body) | |
| } else { | |
| logger.Errorf(ctx, "Cheat err Cookie: %s Resp: %v", cookie, cheatResp.Body) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Cheat Resp.Status:%v Resp.Body:%v", cheatResp.Status, cheatResp.Body)}) | |
| return false | |
| } | |
| } | |
| } | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie Usage limit exceeded, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| config.RemoveCookie(cookie) | |
| break SSELoop | |
| case common.IsServerError(data): | |
| logger.Errorf(ctx, errServerErrMsg) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": errServerErrMsg}) | |
| return false | |
| case common.IsNotLogin(data): | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie Not Login, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| break SSELoop // 使用 label 跳出 SSE 循环 | |
| case common.IsRateLimit(data): | |
| isRateLimit = true | |
| logger.Warnf(ctx, "Cookie rate limited, switching to next cookie, attempt %d/%d, COOKIE:%s", attempt+1, maxRetries, cookie) | |
| config.AddRateLimitCookie(cookie, time.Now().Add(time.Duration(config.RateLimitCookieLockDuration)*time.Second)) | |
| break SSELoop | |
| } | |
| logger.Warnf(ctx, response.Data) | |
| return false | |
| } | |
| logger.Debug(ctx, strings.TrimSpace(data)) | |
| _, shouldContinue := processStreamData(c, data, responseId, openAIReq.Model, modelInfo, jsonData, thinkStartType, thinkEndType) | |
| // 处理事件流数据 | |
| if !shouldContinue { | |
| return false | |
| } | |
| } | |
| if !isRateLimit { | |
| return true | |
| } | |
| // 获取下一个可用的cookie继续尝试 | |
| cookie, err = cookieManager.GetNextCookie() | |
| if err != nil { | |
| logger.Errorf(ctx, "No more valid cookies available after attempt %d", attempt+1) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return false | |
| } | |
| } | |
| logger.Errorf(ctx, "All cookies exhausted after %d attempts", maxRetries) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "All cookies are temporarily unavailable."}) | |
| return false | |
| }) | |
| } | |
| // 处理流式数据的辅助函数,返回bool表示是否继续处理 | |
| func processStreamData(c *gin.Context, data, responseId, model string, modelInfo common.ModelInfo, jsonData []byte, thinkStartType, thinkEndType *bool) (string, bool) { | |
| data = strings.TrimSpace(data) | |
| data = strings.TrimPrefix(data, "data: ") | |
| // 处理[DONE]标记 | |
| if data == "[DONE]" { | |
| return "", false | |
| } | |
| var event map[string]interface{} | |
| if err := json.Unmarshal([]byte(data), &event); err != nil { | |
| logger.Errorf(c.Request.Context(), "Failed to unmarshal event: %v", err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return "", false | |
| } | |
| if modelInfo.Source == "claude" { | |
| eventType, ok := event["type"] | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Event type not found") | |
| return "", false | |
| } | |
| if eventType == "message_stop" { | |
| handleMessageResult(c, responseId, model, jsonData) | |
| return "", false | |
| } | |
| var text string | |
| deltaMap, ok := event["delta"].(map[string]interface{}) | |
| if ok { | |
| thinking, ok := deltaMap["thinking"].(string) | |
| if ok { | |
| if !*thinkStartType { | |
| text = "<think>\n\n" + thinking | |
| *thinkStartType = true | |
| *thinkEndType = false | |
| } else { | |
| text = thinking | |
| } | |
| } | |
| deltaText, ok := deltaMap["text"].(string) | |
| if ok { | |
| if *thinkStartType && !*thinkEndType { | |
| text = "</think>\n\n" + deltaText | |
| *thinkStartType = false | |
| *thinkEndType = true | |
| } else { | |
| text = deltaText | |
| } | |
| } | |
| if err := handleDelta(c, text, responseId, model, jsonData); err != nil { | |
| logger.Errorf(c.Request.Context(), "handleDelta err: %v", err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return "", false | |
| } | |
| return text, true | |
| } | |
| return "", true | |
| } else if modelInfo.Source == "openrouter" { | |
| // 检查是否有choices数组 | |
| choices, ok := event["choices"].([]interface{}) | |
| if !ok || len(choices) == 0 { | |
| // 如果有usage信息但没有内容,可能是最后一个消息 | |
| if _, hasUsage := event["usage"]; hasUsage { | |
| return "", false | |
| } | |
| logger.Errorf(c.Request.Context(), "Invalid openrouter response format: choices not found or empty") | |
| return "", false | |
| } | |
| // 获取第一个choice | |
| choice, ok := choices[0].(map[string]interface{}) | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Invalid choice format in openrouter response") | |
| return "", false | |
| } | |
| // 获取delta内容 | |
| delta, ok := choice["delta"].(map[string]interface{}) | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Delta not found in openrouter response") | |
| return "", false | |
| } | |
| // 获取内容文本 | |
| content, ok := delta["content"].(string) | |
| if !ok { | |
| // 没有内容,可能是其他类型的更新 | |
| return "", true | |
| } | |
| // 处理文本内容 | |
| if err := handleDelta(c, content, responseId, model, jsonData); err != nil { | |
| logger.Errorf(c.Request.Context(), "handleDelta err: %v", err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return "", false | |
| } | |
| // 检查是否完成 - 在处理内容后检查 | |
| finishReason, hasFinishReason := choice["finish_reason"] | |
| if hasFinishReason && finishReason != nil && finishReason != "" { | |
| // 处理完成的消息 | |
| handleMessageResult(c, responseId, model, jsonData) | |
| return content, false // 返回内容但标记为结束 | |
| } | |
| return content, true | |
| } | |
| return "", false | |
| } | |
| func processNoStreamData(c *gin.Context, data string, modelInfo common.ModelInfo, thinkStartType *bool, thinkEndType *bool) (string, bool) { | |
| data = strings.TrimSpace(data) | |
| data = strings.TrimPrefix(data, "data: ") | |
| // 处理[DONE]标记 | |
| if data == "[DONE]" { | |
| return "", false | |
| } | |
| var event map[string]interface{} | |
| if err := json.Unmarshal([]byte(data), &event); err != nil { | |
| logger.Errorf(c.Request.Context(), "Failed to unmarshal event: %v", err) | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return "", false | |
| } | |
| if modelInfo.Source == "claude" { | |
| eventType, ok := event["type"] | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Event type not found") | |
| return "", false | |
| } | |
| if eventType == "message_stop" { | |
| return "", false | |
| } | |
| var text string | |
| deltaMap, ok := event["delta"].(map[string]interface{}) | |
| if ok { | |
| thinking, ok := deltaMap["thinking"].(string) | |
| if ok { | |
| if !*thinkStartType { | |
| text = "<think>\n\n" + thinking | |
| *thinkStartType = true | |
| *thinkEndType = false | |
| } else { | |
| text = thinking | |
| } | |
| } | |
| deltaText, ok := deltaMap["text"].(string) | |
| if ok { | |
| if *thinkStartType && !*thinkEndType { | |
| text = "</think>\n\n" + deltaText | |
| *thinkStartType = false | |
| *thinkEndType = true | |
| } else { | |
| text = deltaText | |
| } | |
| } | |
| return text, true | |
| } | |
| return "", true | |
| } else if modelInfo.Source == "openrouter" { | |
| // 检查是否有choices数组 | |
| choices, ok := event["choices"].([]interface{}) | |
| if !ok || len(choices) == 0 { | |
| // 如果有usage信息但没有内容,可能是最后一个消息 | |
| if _, hasUsage := event["usage"]; hasUsage { | |
| return "", false | |
| } | |
| logger.Errorf(c.Request.Context(), "Invalid openrouter response format: choices not found or empty") | |
| return "", false | |
| } | |
| // 获取第一个choice | |
| choice, ok := choices[0].(map[string]interface{}) | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Invalid choice format in openrouter response") | |
| return "", false | |
| } | |
| // 获取delta内容 | |
| delta, ok := choice["delta"].(map[string]interface{}) | |
| if !ok { | |
| logger.Errorf(c.Request.Context(), "Delta not found in openrouter response") | |
| return "", false | |
| } | |
| // 获取内容文本 | |
| content, ok := delta["content"].(string) | |
| if !ok { | |
| // 没有内容,可能是其他类型的更新 | |
| return "", true | |
| } | |
| // 检查是否完成 - 在处理内容后检查 | |
| finishReason, hasFinishReason := choice["finish_reason"] | |
| if hasFinishReason && finishReason != nil && finishReason != "" { | |
| // 处理完成的消息 | |
| return content, false // 返回内容但标记为结束 | |
| } | |
| return content, true | |
| } | |
| return "", false | |
| } | |
| // OpenaiModels @Summary OpenAI模型列表接口 | |
| // @Description OpenAI模型列表接口 | |
| // @Tags OpenAI | |
| // @Accept json | |
| // @Produce json | |
| // @Param Authorization header string true "Authorization API-KEY" | |
| // @Success 200 {object} common.ResponseResult{data=model.OpenaiModelListResponse} "成功" | |
| // @Router /v1/models [get] | |
| func OpenaiModels(c *gin.Context) { | |
| var modelsResp []string | |
| modelsResp = lo.Union(common.GetModelList()) | |
| var openaiModelListResponse model.OpenaiModelListResponse | |
| var openaiModelResponse []model.OpenaiModelResponse | |
| openaiModelListResponse.Object = "list" | |
| for _, modelResp := range modelsResp { | |
| openaiModelResponse = append(openaiModelResponse, model.OpenaiModelResponse{ | |
| ID: modelResp, | |
| Object: "model", | |
| }) | |
| } | |
| openaiModelListResponse.Data = openaiModelResponse | |
| c.JSON(http.StatusOK, openaiModelListResponse) | |
| return | |
| } | |
| func safeClose(client cycletls.CycleTLS) { | |
| if client.ReqChan != nil { | |
| close(client.ReqChan) | |
| } | |
| if client.RespChan != nil { | |
| close(client.RespChan) | |
| } | |
| } | |
| // | |
| //func processUrl(c *gin.Context, client cycletls.CycleTLS, chatId, cookie string, url string) (string, error) { | |
| // // 判断是否为URL | |
| // if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { | |
| // // 下载文件 | |
| // bytes, err := fetchImageBytes(url) | |
| // if err != nil { | |
| // logger.Errorf(c.Request.Context(), fmt.Sprintf("fetchImageBytes err %v\n", err)) | |
| // return "", fmt.Errorf("fetchImageBytes err %v\n", err) | |
| // } | |
| // | |
| // base64Str := base64.StdEncoding.EncodeToString(bytes) | |
| // | |
| // finalUrl, err := processBytes(c, client, chatId, cookie, base64Str) | |
| // if err != nil { | |
| // logger.Errorf(c.Request.Context(), fmt.Sprintf("processBytes err %v\n", err)) | |
| // return "", fmt.Errorf("processBytes err %v\n", err) | |
| // } | |
| // return finalUrl, nil | |
| // } else { | |
| // finalUrl, err := processBytes(c, client, chatId, cookie, url) | |
| // if err != nil { | |
| // logger.Errorf(c.Request.Context(), fmt.Sprintf("processBytes err %v\n", err)) | |
| // return "", fmt.Errorf("processBytes err %v\n", err) | |
| // } | |
| // return finalUrl, nil | |
| // } | |
| //} | |
| // | |
| //func fetchImageBytes(url string) ([]byte, error) { | |
| // resp, err := http.Get(url) | |
| // if err != nil { | |
| // return nil, fmt.Errorf("http.Get err: %v\n", err) | |
| // } | |
| // defer resp.Body.Close() | |
| // | |
| // return io.ReadAll(resp.Body) | |
| //} | |
| // | |
| //func processBytes(c *gin.Context, client cycletls.CycleTLS, chatId, cookie string, base64Str string) (string, error) { | |
| // // 检查类型 | |
| // fileType := common.DetectFileType(base64Str) | |
| // if !fileType.IsValid { | |
| // return "", fmt.Errorf("invalid file type %s", fileType.Extension) | |
| // } | |
| // signUrl, err := kilo-api.GetSignURL(client, cookie, chatId, fileType.Extension) | |
| // if err != nil { | |
| // logger.Errorf(c.Request.Context(), fmt.Sprintf("GetSignURL err %v\n", err)) | |
| // return "", fmt.Errorf("GetSignURL err: %v\n", err) | |
| // } | |
| // | |
| // err = kilo-api.UploadToS3(client, signUrl, base64Str, fileType.MimeType) | |
| // if err != nil { | |
| // logger.Errorf(c.Request.Context(), fmt.Sprintf("UploadToS3 err %v\n", err)) | |
| // return "", err | |
| // } | |
| // | |
| // u, err := url.Parse(signUrl) | |
| // if err != nil { | |
| // return "", err | |
| // } | |
| // | |
| // return fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path), nil | |
| //} | |