* feat: Refactor response parsing logic to support multiple formats The parsing logic for responses in relay.go and relay-audio.go was refactored to support multiple response formats - 'json', 'text', 'srt', 'verbose_json', and 'vtt'. The existing `WhisperResponse` struct was renamed to `WhisperJsonResponse` and a new struct `WhisperVerboseJsonResponse` was added to support the 'verbose_json' format. Additional parsing functions were added to extract text from these new response types. This change was necessary to make the parsing logic more flexible and extendable for different types of responses. * chore: update name --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
345 lines
10 KiB
Go
345 lines
10 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"one-api/common"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content any `json:"content"`
|
|
Name *string `json:"name,omitempty"`
|
|
}
|
|
|
|
type ImageURL struct {
|
|
Url string `json:"url,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
type TextContent struct {
|
|
Type string `json:"type,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
type ImageContent struct {
|
|
Type string `json:"type,omitempty"`
|
|
ImageURL *ImageURL `json:"image_url,omitempty"`
|
|
}
|
|
|
|
func (m Message) StringContent() string {
|
|
content, ok := m.Content.(string)
|
|
if ok {
|
|
return content
|
|
}
|
|
contentList, ok := m.Content.([]any)
|
|
if ok {
|
|
var contentStr string
|
|
for _, contentItem := range contentList {
|
|
contentMap, ok := contentItem.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if contentMap["type"] == "text" {
|
|
if subStr, ok := contentMap["text"].(string); ok {
|
|
contentStr += subStr
|
|
}
|
|
}
|
|
}
|
|
return contentStr
|
|
}
|
|
return ""
|
|
}
|
|
|
|
const (
|
|
RelayModeUnknown = iota
|
|
RelayModeChatCompletions
|
|
RelayModeCompletions
|
|
RelayModeEmbeddings
|
|
RelayModeModerations
|
|
RelayModeImagesGenerations
|
|
RelayModeEdits
|
|
RelayModeAudioSpeech
|
|
RelayModeAudioTranscription
|
|
RelayModeAudioTranslation
|
|
)
|
|
|
|
// https://platform.openai.com/docs/api-reference/chat
|
|
|
|
type ResponseFormat struct {
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
type GeneralOpenAIRequest struct {
|
|
Model string `json:"model,omitempty"`
|
|
Messages []Message `json:"messages,omitempty"`
|
|
Prompt any `json:"prompt,omitempty"`
|
|
Stream bool `json:"stream,omitempty"`
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
|
Temperature float64 `json:"temperature,omitempty"`
|
|
TopP float64 `json:"top_p,omitempty"`
|
|
N int `json:"n,omitempty"`
|
|
Input any `json:"input,omitempty"`
|
|
Instruction string `json:"instruction,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
Functions any `json:"functions,omitempty"`
|
|
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
|
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
|
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
|
Seed float64 `json:"seed,omitempty"`
|
|
Tools any `json:"tools,omitempty"`
|
|
ToolChoice any `json:"tool_choice,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
}
|
|
|
|
func (r GeneralOpenAIRequest) ParseInput() []string {
|
|
if r.Input == nil {
|
|
return nil
|
|
}
|
|
var input []string
|
|
switch r.Input.(type) {
|
|
case string:
|
|
input = []string{r.Input.(string)}
|
|
case []any:
|
|
input = make([]string, 0, len(r.Input.([]any)))
|
|
for _, item := range r.Input.([]any) {
|
|
if str, ok := item.(string); ok {
|
|
input = append(input, str)
|
|
}
|
|
}
|
|
}
|
|
return input
|
|
}
|
|
|
|
type ChatRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []Message `json:"messages"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
}
|
|
|
|
type TextRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []Message `json:"messages"`
|
|
Prompt string `json:"prompt"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
//Stream bool `json:"stream"`
|
|
}
|
|
|
|
// ImageRequest docs: https://platform.openai.com/docs/api-reference/images/create
|
|
type ImageRequest struct {
|
|
Model string `json:"model"`
|
|
Prompt string `json:"prompt" binding:"required"`
|
|
N int `json:"n,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
Quality string `json:"quality,omitempty"`
|
|
ResponseFormat string `json:"response_format,omitempty"`
|
|
Style string `json:"style,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
}
|
|
|
|
type WhisperJSONResponse struct {
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
type WhisperVerboseJSONResponse struct {
|
|
Task string `json:"task,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
Duration float64 `json:"duration,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
Segments []Segment `json:"segments,omitempty"`
|
|
}
|
|
|
|
type Segment struct {
|
|
Id int `json:"id"`
|
|
Seek int `json:"seek"`
|
|
Start float64 `json:"start"`
|
|
End float64 `json:"end"`
|
|
Text string `json:"text"`
|
|
Tokens []int `json:"tokens"`
|
|
Temperature float64 `json:"temperature"`
|
|
AvgLogprob float64 `json:"avg_logprob"`
|
|
CompressionRatio float64 `json:"compression_ratio"`
|
|
NoSpeechProb float64 `json:"no_speech_prob"`
|
|
}
|
|
|
|
type TextToSpeechRequest struct {
|
|
Model string `json:"model" binding:"required"`
|
|
Input string `json:"input" binding:"required"`
|
|
Voice string `json:"voice" binding:"required"`
|
|
Speed float64 `json:"speed"`
|
|
ResponseFormat string `json:"response_format"`
|
|
}
|
|
|
|
type Usage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
}
|
|
|
|
type OpenAIError struct {
|
|
Message string `json:"message"`
|
|
Type string `json:"type"`
|
|
Param string `json:"param"`
|
|
Code any `json:"code"`
|
|
}
|
|
|
|
type OpenAIErrorWithStatusCode struct {
|
|
OpenAIError
|
|
StatusCode int `json:"status_code"`
|
|
}
|
|
|
|
type TextResponse struct {
|
|
Choices []OpenAITextResponseChoice `json:"choices"`
|
|
Usage `json:"usage"`
|
|
Error OpenAIError `json:"error"`
|
|
}
|
|
|
|
type OpenAITextResponseChoice struct {
|
|
Index int `json:"index"`
|
|
Message `json:"message"`
|
|
FinishReason string `json:"finish_reason"`
|
|
}
|
|
|
|
type OpenAITextResponse struct {
|
|
Id string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
Choices []OpenAITextResponseChoice `json:"choices"`
|
|
Usage `json:"usage"`
|
|
}
|
|
|
|
type OpenAIEmbeddingResponseItem struct {
|
|
Object string `json:"object"`
|
|
Index int `json:"index"`
|
|
Embedding []float64 `json:"embedding"`
|
|
}
|
|
|
|
type OpenAIEmbeddingResponse struct {
|
|
Object string `json:"object"`
|
|
Data []OpenAIEmbeddingResponseItem `json:"data"`
|
|
Model string `json:"model"`
|
|
Usage `json:"usage"`
|
|
}
|
|
|
|
type ImageResponse struct {
|
|
Created int `json:"created"`
|
|
Data []struct {
|
|
Url string `json:"url"`
|
|
}
|
|
}
|
|
|
|
type ChatCompletionsStreamResponseChoice struct {
|
|
Delta struct {
|
|
Content string `json:"content"`
|
|
} `json:"delta"`
|
|
FinishReason *string `json:"finish_reason"`
|
|
}
|
|
|
|
type ChatCompletionsStreamResponse struct {
|
|
Id string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
Model string `json:"model"`
|
|
Choices []ChatCompletionsStreamResponseChoice `json:"choices"`
|
|
}
|
|
|
|
type CompletionsStreamResponse struct {
|
|
Choices []struct {
|
|
Text string `json:"text"`
|
|
FinishReason string `json:"finish_reason"`
|
|
} `json:"choices"`
|
|
}
|
|
|
|
func Relay(c *gin.Context) {
|
|
relayMode := RelayModeUnknown
|
|
if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") {
|
|
relayMode = RelayModeChatCompletions
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") {
|
|
relayMode = RelayModeCompletions
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
|
relayMode = RelayModeEmbeddings
|
|
} else if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
|
|
relayMode = RelayModeEmbeddings
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
|
relayMode = RelayModeModerations
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
|
relayMode = RelayModeImagesGenerations
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
|
|
relayMode = RelayModeEdits
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/speech") {
|
|
relayMode = RelayModeAudioSpeech
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") {
|
|
relayMode = RelayModeAudioTranscription
|
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/translations") {
|
|
relayMode = RelayModeAudioTranslation
|
|
}
|
|
var err *OpenAIErrorWithStatusCode
|
|
switch relayMode {
|
|
case RelayModeImagesGenerations:
|
|
err = relayImageHelper(c, relayMode)
|
|
case RelayModeAudioSpeech:
|
|
fallthrough
|
|
case RelayModeAudioTranslation:
|
|
fallthrough
|
|
case RelayModeAudioTranscription:
|
|
err = relayAudioHelper(c, relayMode)
|
|
default:
|
|
err = relayTextHelper(c, relayMode)
|
|
}
|
|
if err != nil {
|
|
requestId := c.GetString(common.RequestIdKey)
|
|
retryTimesStr := c.Query("retry")
|
|
retryTimes, _ := strconv.Atoi(retryTimesStr)
|
|
if retryTimesStr == "" {
|
|
retryTimes = common.RetryTimes
|
|
}
|
|
if retryTimes > 0 {
|
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
|
} else {
|
|
if err.StatusCode == http.StatusTooManyRequests {
|
|
err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试"
|
|
}
|
|
err.OpenAIError.Message = common.MessageWithRequestId(err.OpenAIError.Message, requestId)
|
|
c.JSON(err.StatusCode, gin.H{
|
|
"error": err.OpenAIError,
|
|
})
|
|
}
|
|
channelId := c.GetInt("channel_id")
|
|
common.LogError(c.Request.Context(), fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
|
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
|
if shouldDisableChannel(&err.OpenAIError, err.StatusCode) {
|
|
channelId := c.GetInt("channel_id")
|
|
channelName := c.GetString("channel_name")
|
|
disableChannel(channelId, channelName, err.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func RelayNotImplemented(c *gin.Context) {
|
|
err := OpenAIError{
|
|
Message: "API not implemented",
|
|
Type: "one_api_error",
|
|
Param: "",
|
|
Code: "api_not_implemented",
|
|
}
|
|
c.JSON(http.StatusNotImplemented, gin.H{
|
|
"error": err,
|
|
})
|
|
}
|
|
|
|
func RelayNotFound(c *gin.Context) {
|
|
err := OpenAIError{
|
|
Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path),
|
|
Type: "invalid_request_error",
|
|
Param: "",
|
|
Code: "",
|
|
}
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": err,
|
|
})
|
|
}
|