Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeReplicate
case constant.ChannelTypeCodex:
apiType = constant.APITypeCodex
case constant.ChannelTypeXiaomi:
apiType = constant.APITypeXiaomi
}
if apiType == -1 {
return constant.APITypeOpenAI, false
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
APITypeMiniMax
APITypeReplicate
APITypeCodex
APITypeXiaomi
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeXiaomi = 58
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"https://api.xiaomimimo.com", //58
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeXiaomi: "Xiaomi",
}

func GetChannelTypeName(channelType int) string {
Expand Down
2 changes: 2 additions & 0 deletions model/pricing_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var defaultVendorRules = map[string]string{
"glm-": "智谱",
"qwen": "阿里巴巴",
"deepseek": "DeepSeek",
"mimo": "小米",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"abab": "MiniMax",
"ernie": "百度",
"spark": "讯飞",
Expand Down Expand Up @@ -46,6 +47,7 @@ var defaultVendorIcons = map[string]string{
"智谱": "Zhipu.Color",
"阿里巴巴": "Qwen.Color",
"DeepSeek": "DeepSeek.Color",
"小米": "XiaomiMiMo",
"MiniMax": "Minimax.Color",
"百度": "Wenxin.Color",
"讯飞": "Spark.Color",
Expand Down
75 changes: 75 additions & 0 deletions relay/channel/xiaomi/adaptor.go
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 3

Repository: 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 -20

Repository: 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.go

Repository: 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 2

Repository: 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 3

Repository: QuantumNous/new-api

Length of output: 579


ConvertOpenAIRequest bypasses required base adaptor logic for StreamOptions handling.

The override returns the request unchanged without delegating to a.Adaptor.ConvertOpenAIRequest. The base OpenAI adaptor includes logic (line 234) to clear StreamOptions from requests for all non-OpenAI/Azure channels:

if info.ChannelType != constant.ChannelTypeOpenAI && info.ChannelType != constant.ChannelTypeAzure {
    request.StreamOptions = nil
}

Since Xiaomi's ChannelType is ChannelTypeXiaomi, the base adaptor would clear StreamOptions to prevent it from being sent upstream. By returning the request unchanged, this override silently bypasses that clearing logic, leaving StreamOptions in the request body when it should be removed.

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
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/xiaomi/adaptor.go` around lines 36 - 41, ConvertOpenAIRequest
currently returns the incoming request unchanged, bypassing the base adaptor's
logic that clears StreamOptions for non-OpenAI/Azure channels; update Xiaomi's
ConvertOpenAIRequest to delegate to the base implementation (call
a.Adaptor.ConvertOpenAIRequest(c, info, request)) and return its result so the
base logic (which nils request.StreamOptions when info.ChannelType is not
constant.ChannelTypeOpenAI or constant.ChannelTypeAzure) is applied for
ChannelTypeXiaomi; alternatively ensure the method explicitly clears
request.StreamOptions for non-OpenAI/Azure before returning, but prefer
delegating to the base adaptor to keep behavior consistent.


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
}
17 changes: 17 additions & 0 deletions relay/channel/xiaomi/constants.go
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",
}
140 changes: 140 additions & 0 deletions relay/channel/xiaomi/tts.go
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
}
1 change: 1 addition & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ var streamSupportedChannels = map[int]bool{
constant.ChannelTypeMoonshot: true,
constant.ChannelTypeMiniMax: true,
constant.ChannelTypeSiliconFlow: true,
constant.ChannelTypeXiaomi: true,
}

func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
Expand Down
3 changes: 3 additions & 0 deletions relay/relay_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/claude"
"github.com/QuantumNous/new-api/relay/channel/cloudflare"
"github.com/QuantumNous/new-api/relay/channel/codex"
"github.com/QuantumNous/new-api/relay/channel/xiaomi"
"github.com/QuantumNous/new-api/relay/channel/cohere"
"github.com/QuantumNous/new-api/relay/channel/coze"
"github.com/QuantumNous/new-api/relay/channel/deepseek"
Expand Down Expand Up @@ -120,6 +121,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &replicate.Adaptor{}
case constant.APITypeCodex:
return &codex.Adaptor{}
case constant.APITypeXiaomi:
return &xiaomi.Adaptor{}
}
return nil
}
Expand Down
5 changes: 5 additions & 0 deletions setting/ratio_setting/model_ratio.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ var defaultModelRatio = map[string]float64{
"deepseek-chat": 0.27 / 2,
"deepseek-coder": 0.27 / 2,
"deepseek-reasoner": 0.55 / 2, // 0.55 / 1k tokens
"mimo-v2.5-pro": 0.5,
"mimo-v2-pro": 0.5,
"mimo-v2.5": 0.2,
"mimo-v2-flash": 0.05,
"mimo-v2-omni": 0.2,
// Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用
"llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD,
"llama-3-sonar-small-32k-online": 0.2 / 1000 * USD,
Expand Down
1 change: 1 addition & 0 deletions web/classic/src/constants/channel.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const CHANNEL_OPTIONS = [
},
{ value: 39, color: 'grey', label: 'Cloudflare' },
{ value: 43, color: 'blue', label: 'DeepSeek' },
{ value: 58, color: 'orange', label: 'Xiaomi MiMo' },
{
value: 15,
color: 'blue',
Expand Down
Loading