fix(omni): 为百炼多模态音频添加 data URL 并限定主机匹配

This commit is contained in:
sim committed 2026-09-26 00:22:05 +08:00
1 parent 06db7d2978
commit 6ebf1f1ad1
2 files changed
+91 -1

No files matched your search

@@ -40,6 +40,17 @@ impl OmniConfig {
self.provider_id.trim() == OMNI_GEMINI_PROVIDER_ID
|| self.base_url.contains("generativelanguage.googleapis.com")
}
/// 百炼/DashScope 兼容端点把 `input_audio.data` 按 URL/data-URL 解析,裸 Base64
/// 会被 400 拒绝("The provided URL does not appear to be valid")。与
/// `asr::dashscope_multimodal` 转写通道同款,Base64 须带 data-URL 前缀;
/// 沿用 polish 的主机名关键词,但只检查 URL 解析后的真实 host。
fn audio_requires_data_url(&self) -> bool {
reqwest::Url::parse(self.base_url.trim())
.ok()
.and_then(|url| url.host_str().map(str::to_ascii_lowercase))
.is_some_and(|host| host.contains("dashscope") || host.contains("aliyuncs"))
}
}
/// 一次 Omni 调用的构建时快照(provider id + model),落历史归因用。
@@ -103,7 +114,13 @@ impl OpenAICompatibleOmni {
) -> Vec<Value> {
let user_content = match wav_bytes {
Some(wav) => {
let data = base64::engine::general_purpose::STANDARD.encode(wav);
let encoded = base64::engine::general_purpose::STANDARD.encode(wav);
// 百炼系端点要求 data-URL 前缀;OpenAI 官方等其他兼容端点保持裸 Base64。
let data = if self.config.audio_requires_data_url() {
format!("data:audio/wav;base64,{encoded}")
} else {
encoded
};
let mut parts = vec![json!({
"type": "input_audio",
"input_audio": { "data": data, "format": "wav" },
@@ -458,6 +475,74 @@ mod tests {
assert_eq!(parts[1]["text"], "翻译成中文");
}
#[test]
fn build_messages_wraps_audio_as_data_url_for_dashscope() {
let mut dashscope = config();
dashscope.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1".into();
let provider = OpenAICompatibleOmni::new(dashscope);
let messages = provider.build_messages("system-prompt", "", Some(&[1u8, 2, 3, 4]));
let parts = messages[1]["content"].as_array().expect("audio parts");
let data = parts[0]["input_audio"]["data"]
.as_str()
.expect("audio data");
let payload = data
.strip_prefix("data:audio/wav;base64,")
.expect("data-url prefix for DashScope");
let decoded = base64::engine::general_purpose::STANDARD
.decode(payload)
.expect("valid base64");
assert_eq!(decoded, vec![1u8, 2, 3, 4]);
}
#[test]
fn build_messages_wraps_audio_as_data_url_for_bailian_dedicated_endpoint() {
let mut maas = config();
maas.base_url =
"https://llm-example.cn-beijing.maas.aliyuncs.com/compatible-mode/v1".into();
let provider = OpenAICompatibleOmni::new(maas);
let messages = provider.build_messages("system-prompt", "", Some(&[1u8, 2, 3, 4]));
let parts = messages[1]["content"].as_array().expect("audio parts");
assert!(parts[0]["input_audio"]["data"]
.as_str()
.expect("audio data")
.starts_with("data:audio/wav;base64,"));
}
#[test]
fn omni_audio_data_url_matches_dashscope_hosts_only() {
assert!(!config().audio_requires_data_url());
let mut dashscope = config();
dashscope.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1".into();
assert!(dashscope.audio_requires_data_url());
let mut maas = config();
maas.base_url = "https://LLM-EXAMPLE.cn-beijing.maas.aliyuncs.com/v1".into();
assert!(maas.audio_requires_data_url());
// host 之外的关键字(路径里碰巧含 dashscope)不触发。
let mut path_only = config();
path_only.base_url = "https://example.com/proxy/dashscope/v1".into();
assert!(!path_only.audio_requires_data_url());
let mut empty = config();
empty.base_url = String::new();
assert!(!empty.audio_requires_data_url());
}
#[test]
fn omni_audio_data_url_ignores_non_host_url_components() {
for base_url in [
"https://dashscope@api.example.com/v1",
"https://user:aliyuncs@api.example.com/v1",
"https://api.example.com?provider=dashscope",
"https://api.example.com#aliyuncs",
] {
let mut other = config();
other.base_url = base_url.into();
assert!(!other.audio_requires_data_url(), "{base_url}");
let provider = OpenAICompatibleOmni::new(other);
let messages = provider.build_messages("system", "", Some(&[1u8, 2, 3, 4]));
assert_eq!(messages[1]["content"][0]["input_audio"]["data"], "AQIDBA==");
}
}
#[test]
fn omni_body_has_stream_model_and_temperature() {
let provider = OpenAICompatibleOmni::new(config());