fix(audio): 兼容百炼 OpenAI 音频输入
为阿里云百炼 OpenAI-compatible Chat 统一保留标准 input_audio,并将旧版 audio_url 转换为 input_audio,同时根据 MIME 或扩展名补齐音频格式。 补充请求资产水合测试,确认 input_audio.data 会转换为裸 Base64 且保留 format。 验证:在 apps/api 执行 env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1 全部通过。
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
var openAIChatAudioFormatByMIME = map[string]string{
|
||||
"audio/aac": "aac",
|
||||
"audio/flac": "flac",
|
||||
"audio/m4a": "m4a",
|
||||
"audio/mp4": "m4a",
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/ogg": "ogg",
|
||||
"audio/opus": "opus",
|
||||
"audio/wav": "wav",
|
||||
"audio/webm": "webm",
|
||||
"audio/x-m4a": "m4a",
|
||||
"audio/x-wav": "wav",
|
||||
}
|
||||
|
||||
var openAIChatAudioFormatByExtension = map[string]string{
|
||||
"aac": "aac",
|
||||
"flac": "flac",
|
||||
"m4a": "m4a",
|
||||
"mp3": "mp3",
|
||||
"mp4": "m4a",
|
||||
"oga": "ogg",
|
||||
"ogg": "ogg",
|
||||
"opus": "opus",
|
||||
"wav": "wav",
|
||||
"webm": "webm",
|
||||
}
|
||||
|
||||
func normalizeOpenAIChatAudioParts(body map[string]any, candidate store.RuntimeModelCandidate) (map[string]any, error) {
|
||||
if !isAliyunBailianOpenAI(candidate) {
|
||||
return body, nil
|
||||
}
|
||||
messages, ok := body["messages"].([]any)
|
||||
if !ok {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
normalizedMessages := make([]any, 0, len(messages))
|
||||
for _, rawMessage := range messages {
|
||||
message, ok := rawMessage.(map[string]any)
|
||||
if !ok {
|
||||
normalizedMessages = append(normalizedMessages, rawMessage)
|
||||
continue
|
||||
}
|
||||
content, ok := message["content"].([]any)
|
||||
if !ok {
|
||||
normalizedMessages = append(normalizedMessages, rawMessage)
|
||||
continue
|
||||
}
|
||||
|
||||
normalizedContent := make([]any, 0, len(content))
|
||||
for _, rawPart := range content {
|
||||
part, ok := rawPart.(map[string]any)
|
||||
if !ok {
|
||||
normalizedContent = append(normalizedContent, rawPart)
|
||||
continue
|
||||
}
|
||||
normalizedPart, err := normalizeAliyunOpenAIChatAudioPart(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalizedContent = append(normalizedContent, normalizedPart)
|
||||
}
|
||||
copiedMessage := cloneMapAny(message)
|
||||
copiedMessage["content"] = normalizedContent
|
||||
normalizedMessages = append(normalizedMessages, copiedMessage)
|
||||
}
|
||||
|
||||
out := cloneMapAny(body)
|
||||
out["messages"] = normalizedMessages
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func normalizeAliyunOpenAIChatAudioPart(part map[string]any) (map[string]any, error) {
|
||||
switch strings.TrimSpace(stringFromAny(part["type"])) {
|
||||
case "audio_url":
|
||||
audioURL, _ := part["audio_url"].(map[string]any)
|
||||
data := strings.TrimSpace(stringFromAny(audioURL["url"]))
|
||||
format := resolveOpenAIChatAudioFormat(data, audioURL["format"], audioURL["mime_type"], audioURL["mimeType"])
|
||||
if data == "" || format == "" {
|
||||
return nil, invalidOpenAIChatAudioError()
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "input_audio",
|
||||
"input_audio": map[string]any{
|
||||
"data": data,
|
||||
"format": format,
|
||||
},
|
||||
}, nil
|
||||
case "input_audio":
|
||||
inputAudio, _ := part["input_audio"].(map[string]any)
|
||||
data := firstNonEmptyString(inputAudio["data"], inputAudio["url"])
|
||||
format := resolveOpenAIChatAudioFormat(data, inputAudio["format"], inputAudio["mime_type"], inputAudio["mimeType"])
|
||||
if data == "" || format == "" {
|
||||
return nil, invalidOpenAIChatAudioError()
|
||||
}
|
||||
copiedInput := cloneMapAny(inputAudio)
|
||||
copiedInput["data"] = data
|
||||
copiedInput["format"] = format
|
||||
delete(copiedInput, "url")
|
||||
copiedPart := cloneMapAny(part)
|
||||
copiedPart["input_audio"] = copiedInput
|
||||
return copiedPart, nil
|
||||
default:
|
||||
return part, nil
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOpenAIChatAudioFormat(source string, values ...any) string {
|
||||
if explicit := strings.ToLower(strings.TrimSpace(firstNonEmptyString(values...))); explicit != "" {
|
||||
if normalized := openAIChatAudioFormatByExtension[explicit]; normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
if normalized := openAIChatAudioFormatByMIME[strings.TrimSpace(strings.Split(explicit, ";")[0])]; normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
return explicit
|
||||
}
|
||||
if dataMIME := openAIChatAudioDataMIME(source); dataMIME != "" {
|
||||
if normalized := openAIChatAudioFormatByMIME[dataMIME]; normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
if extension := openAIChatAudioSourceExtension(source); extension != "" {
|
||||
return openAIChatAudioFormatByExtension[extension]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openAIChatAudioDataMIME(source string) string {
|
||||
if !strings.HasPrefix(strings.ToLower(source), "data:") {
|
||||
return ""
|
||||
}
|
||||
header := strings.TrimPrefix(source[:strings.Index(source+",", ",")], "data:")
|
||||
return strings.ToLower(strings.TrimSpace(strings.Split(header, ";")[0]))
|
||||
}
|
||||
|
||||
func openAIChatAudioSourceExtension(source string) string {
|
||||
if source == "" || strings.HasPrefix(strings.ToLower(source), "data:") {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(source); err == nil && parsed.Path != "" {
|
||||
source = parsed.Path
|
||||
}
|
||||
source = strings.TrimSuffix(strings.Split(strings.Split(source, "?")[0], "#")[0], "/")
|
||||
if index := strings.LastIndex(source, "."); index >= 0 && index+1 < len(source) {
|
||||
return strings.ToLower(source[index+1:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func invalidOpenAIChatAudioError() error {
|
||||
return &ClientError{
|
||||
Code: "invalid_parameter",
|
||||
Message: "input_audio requires data and a resolvable format",
|
||||
Param: "messages.content.input_audio",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Retryable: false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user