-
-
Notifications
You must be signed in to change notification settings - Fork 6.7k
feat: Add Xiaomi channel support and update related model ratios #4590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9acde19
d6f8a9b
c011935
ba77b90
3129946
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package xiaomi | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| "github.com/QuantumNous/new-api/dto" | ||
| "github.com/QuantumNous/new-api/relay/channel" | ||
| "github.com/QuantumNous/new-api/relay/channel/openai" | ||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | ||
| relayconstant "github.com/QuantumNous/new-api/relay/constant" | ||
| "github.com/QuantumNous/new-api/types" | ||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| type Adaptor struct { | ||
| openai.Adaptor | ||
| } | ||
|
|
||
| func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { | ||
| if info.RelayMode != relayconstant.RelayModeAudioSpeech { | ||
| return a.Adaptor.GetRequestURL(info) | ||
| } | ||
| return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil | ||
| } | ||
|
|
||
| func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { | ||
| if err := a.Adaptor.SetupRequestHeader(c, req, info); err != nil { | ||
| return err | ||
| } | ||
| req.Set("api-key", info.ApiKey) | ||
| return nil | ||
| } | ||
|
|
||
| func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { | ||
| if request == nil { | ||
| return nil, fmt.Errorf("request is nil") | ||
| } | ||
| return request, nil | ||
| } | ||
|
Comment on lines
+36
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect openai.Adaptor.ConvertOpenAIRequest for StreamOptions injection
# and any other request mutations that Xiaomi's override would silently bypass.
# 1. Find the full implementation of ConvertOpenAIRequest in the openai adaptor
ast-grep --pattern 'func (a *Adaptor) ConvertOpenAIRequest($_, $_, $_) ($_, $_) {
$$$
}'
# 2. Confirm which channels are in streamSupportedChannels and whether Xiaomi is present
rg -n "streamSupportedChannels\|ChannelTypeXiaomi" relay/common/relay_info.go -A 3 -B 1
# 3. Find where StreamOptions is set / injected into the request body
rg -n "StreamOptions\|stream_options\|StreamOption" relay/channel/openai/adaptor.go -A 3 -B 3Repository: QuantumNous/new-api Length of output: 34700 🏁 Script executed: # Check the base OpenAI adaptor's StreamOptions handling logic
rg -n "StreamOptions" relay/channel/openai/adaptor.go -B 3 -A 3
# Verify if Xiaomi is in streamSupportedChannels
rg -n "streamSupportedChannels" relay/common/relay_info.go -A 30
# Check Xiaomi's channel type constant
rg -n "ChannelTypeXiaomi\|Xiaomi" relay/ -t go | head -20Repository: QuantumNous/new-api Length of output: 2983 🏁 Script executed: # Get full context around lines 76-80 to understand which function sets StreamOptions
sed -n '60,90p' relay/channel/openai/adaptor.go
# Check if there are multiple ConvertOpenAIRequest calls or if one delegates to another
rg -n "ConvertOpenAIRequest" relay/channel/xiaomi/adaptor.go -B 5 -A 10
# Verify Xiaomi's base class/embedding structure
head -50 relay/channel/xiaomi/adaptor.goRepository: QuantumNous/new-api Length of output: 3316 🏁 Script executed: # Check if there are tests or documentation indicating what Xiaomi expects
find relay/channel/xiaomi -type f -name "*.go" -o -name "*.md" -o -name "*.txt" | xargs ls -la
# Look for usage patterns or comments about StreamOptions in Xiaomi context
rg -n "StreamOptions\|stream_options\|include_usage" relay/channel/xiaomi/ -t go
# Check if Xiaomi needs to clear StreamOptions like the base does
rg -n "ChannelTypeXiaomi" relay/channel/ -t go -B 2 -A 2Repository: QuantumNous/new-api Length of output: 304 🏁 Script executed: # View the exact base adaptor logic again to confirm the StreamOptions clearing behavior
sed -n '229,240p' relay/channel/openai/adaptor.go
# Check if there's any conditional logic that would preserve StreamOptions for Xiaomi
rg -n "SupportStreamOptions\|info.ChannelType.*Xiaomi" relay/channel/openai/adaptor.go -A 3 -B 3Repository: QuantumNous/new-api Length of output: 579
The override returns the request unchanged without delegating to if info.ChannelType != constant.ChannelTypeOpenAI && info.ChannelType != constant.ChannelTypeAzure {
request.StreamOptions = nil
}Since Xiaomi's The simplest fix is to delegate to the base adaptor: 🔧 Proposed fix func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
- if request == nil {
- return nil, fmt.Errorf("request is nil")
- }
- return request, nil
+ return a.Adaptor.ConvertOpenAIRequest(c, info, request)
}🤖 Prompt for AI Agents |
||
|
|
||
| func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { | ||
| if info.RelayMode != relayconstant.RelayModeAudioSpeech { | ||
| return a.Adaptor.ConvertAudioRequest(c, info, request) | ||
| } | ||
| return convertTTSRequest(c, request) | ||
| } | ||
|
|
||
| func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { | ||
| if info.RelayMode == relayconstant.RelayModeAudioTranscription || | ||
| info.RelayMode == relayconstant.RelayModeAudioTranslation || | ||
| info.RelayMode == relayconstant.RelayModeImagesEdits { | ||
| return channel.DoFormRequest(a, c, info, requestBody) | ||
| } | ||
| if info.RelayMode == relayconstant.RelayModeRealtime { | ||
| return channel.DoWssRequest(a, c, info, requestBody) | ||
| } | ||
| return channel.DoApiRequest(a, c, info, requestBody) | ||
| } | ||
|
|
||
| func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { | ||
| if info.RelayMode == relayconstant.RelayModeAudioSpeech { | ||
| return handleTTSResponse(c, resp, info) | ||
| } | ||
| return a.Adaptor.DoResponse(c, resp, info) | ||
| } | ||
|
|
||
| func (a *Adaptor) GetModelList() []string { | ||
| return ModelList | ||
| } | ||
|
|
||
| func (a *Adaptor) GetChannelName() string { | ||
| return ChannelName | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package xiaomi | ||
|
|
||
| const ( | ||
| ChannelName = "xiaomi" | ||
| contextKeyAudioFormat = "xiaomi_audio_format" | ||
| defaultMimoVoice = "mimo_default" | ||
| ) | ||
|
|
||
| var ModelList = []string{ | ||
| "mimo-v2-pro", | ||
| "mimo-v2-flash", | ||
| "mimo-v2-omni", | ||
| "mimo-v2-tts", | ||
| "mimo-v2.5-tts", | ||
| "mimo-v2.5-pro", | ||
| "mimo-v2.5", | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| package xiaomi | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/base64" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/dto" | ||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | ||
| "github.com/QuantumNous/new-api/types" | ||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| type xiaomiTTSMessage struct { | ||
| Role string `json:"role"` | ||
| Content string `json:"content"` | ||
| } | ||
|
|
||
| type xiaomiTTSAudio struct { | ||
| Voice string `json:"voice"` | ||
| Format string `json:"format"` | ||
| } | ||
|
|
||
| type xiaomiTTSRequest struct { | ||
| Model string `json:"model"` | ||
| Messages []xiaomiTTSMessage `json:"messages"` | ||
| Audio xiaomiTTSAudio `json:"audio"` | ||
| } | ||
|
|
||
| type xiaomiTTSResponse struct { | ||
| Choices []struct { | ||
| Message struct { | ||
| Audio struct { | ||
| Data string `json:"data"` | ||
| } `json:"audio"` | ||
| } `json:"message"` | ||
| } `json:"choices"` | ||
| Usage dto.Usage `json:"usage"` | ||
| } | ||
|
|
||
| func convertTTSRequest(c *gin.Context, request dto.AudioRequest) (io.Reader, error) { | ||
| audioFormat := normalizeMimoAudioFormat(request.ResponseFormat) | ||
| c.Set(contextKeyAudioFormat, audioFormat) | ||
|
|
||
| voice := request.Voice | ||
| if voice == "" { | ||
| voice = defaultMimoVoice | ||
| } | ||
|
|
||
| messages := make([]xiaomiTTSMessage, 0, 2) | ||
| if request.Instructions != "" { | ||
| messages = append(messages, xiaomiTTSMessage{Role: "user", Content: request.Instructions}) | ||
| } | ||
| messages = append(messages, xiaomiTTSMessage{Role: "assistant", Content: request.Input}) | ||
|
|
||
| jsonData, err := common.Marshal(xiaomiTTSRequest{ | ||
| Model: request.Model, | ||
| Messages: messages, | ||
| Audio: xiaomiTTSAudio{Voice: voice, Format: audioFormat}, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return bytes.NewReader(jsonData), nil | ||
| } | ||
|
|
||
| func normalizeMimoAudioFormat(format string) string { | ||
| switch format { | ||
| case "": | ||
| return "wav" | ||
| case "pcm": | ||
| return "pcm16" | ||
| default: | ||
| return format | ||
| } | ||
| } | ||
|
|
||
| func getTTSContentType(format string) string { | ||
| switch format { | ||
| case "mp3": | ||
| return "audio/mpeg" | ||
| case "pcm", "pcm16": | ||
| return "audio/pcm" | ||
| default: | ||
| return "audio/wav" | ||
| } | ||
| } | ||
|
|
||
| func handleTTSResponse(c *gin.Context, resp *http.Response, _ *relaycommon.RelayInfo) (any, *types.NewAPIError) { | ||
| if resp == nil || resp.Body == nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("invalid xiaomi TTS response"), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
| defer resp.Body.Close() | ||
| audioFormat := c.GetString(contextKeyAudioFormat) | ||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("read xiaomi TTS response: %w", err), | ||
| types.ErrorCodeReadResponseBodyFailed, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
|
|
||
| var ttsResp xiaomiTTSResponse | ||
| if err := common.Unmarshal(body, &ttsResp); err != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("unmarshal xiaomi TTS response: %w", err), | ||
| types.ErrorCodeBadResponseBody, | ||
| http.StatusBadGateway, | ||
| ) | ||
| } | ||
|
|
||
| if len(ttsResp.Choices) == 0 || ttsResp.Choices[0].Message.Audio.Data == "" { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("xiaomi TTS response missing audio data"), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusBadGateway, | ||
| ) | ||
| } | ||
|
|
||
| audioData, err := base64.StdEncoding.DecodeString(ttsResp.Choices[0].Message.Audio.Data) | ||
| if err != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("decode xiaomi TTS audio payload: %w", err), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusBadGateway, | ||
| ) | ||
| } | ||
|
|
||
| c.Data(http.StatusOK, getTTSContentType(audioFormat), audioData) | ||
|
|
||
| return &ttsResp.Usage, nil | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.