fix(asr): validate Bailian endpoints by channel scope

Provider validation used the globally active ASR provider when deciding whether to derive Bailian protocol endpoints. Non-active Bailian channels therefore sent their WebSocket endpoint through HTTP-only validation for synchronous models.
This commit is contained in:
Chris233 committed 2026-08-21 23:56:38 +08:00
1 parent 5ba0fa725c
commit c61e3dc6d0
1 file changed
+49 -30
@@ -33,6 +33,18 @@ pub(crate) struct ProviderScope {
channel: Option<String>,
}
fn derive_scoped_bailian_endpoint(
provider_type: &str,
endpoint: &str,
protocol: crate::coordinator::BailianEndpointProtocol,
) -> Result<String, String> {
if provider_type == crate::asr::bailian::PROVIDER_ID {
crate::coordinator::derive_bailian_endpoint(endpoint, protocol)
} else {
Ok(endpoint.to_string())
}
}
impl ProviderScope {
fn new(kind: &str, channel: Option<String>) -> Result<Self, String> {
let kind = ProviderKind::parse(kind)?;
@@ -705,7 +717,8 @@ async fn validate_dashscope_multimodal_asr_provider(scope: &ProviderScope) -> Re
crate::coordinator::validate_dashscope_multimodal_model(&model)?;
let protocol = crate::asr::dashscope_multimodal::protocol_for_model(&model)
.unwrap_or(crate::asr::dashscope_multimodal::DashScopeBatchProtocol::Multimodal);
let (api_key, base_url) = if crate::coordinator::unified_bailian_is_active() {
let provider_type = scope.provider_type();
let (api_key, base_url) = if provider_type == crate::asr::bailian::PROVIDER_ID {
let api_key = scope
.get(CredentialAccount::AsrApiKey)
.map_err(|e| e.to_string())?
@@ -723,7 +736,8 @@ async fn validate_dashscope_multimodal_asr_provider(scope: &ProviderScope) -> Re
crate::coordinator::BailianEndpointProtocol::AsyncTranscription
}
};
let endpoint = crate::coordinator::derive_bailian_endpoint(&endpoint, endpoint_protocol)?;
let endpoint =
derive_scoped_bailian_endpoint(&provider_type, &endpoint, endpoint_protocol)?;
(api_key, endpoint)
} else {
let config = read_openai_provider_config(scope)?;
@@ -796,14 +810,11 @@ async fn validate_bailian_asr_provider(scope: &ProviderScope) -> Result<(), Stri
.map_err(|e| e.to_string())?
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| crate::asr::bailian::DEFAULT_ENDPOINT.to_string());
let endpoint = if crate::coordinator::unified_bailian_is_active() {
crate::coordinator::derive_bailian_endpoint(
&stored_endpoint,
crate::coordinator::BailianEndpointProtocol::ClassicRealtime,
)?
} else {
stored_endpoint
};
let endpoint = derive_scoped_bailian_endpoint(
&scope.provider_type(),
&stored_endpoint,
crate::coordinator::BailianEndpointProtocol::ClassicRealtime,
)?;
// 协议头先行校验:填成 https://(百炼兼容模式 / 专属域名地址)时,WebSocket
// 握手报的 "URL scheme not supported" 会被前端兜底成笼统的「操作失败」,
// 用户无从定位。这里拦下并返回专用错误码,前端映射成可操作的提示。
@@ -851,22 +862,16 @@ async fn validate_qwen3_realtime_asr_provider(scope: &ProviderScope) -> Result<(
return Err("API Key 为空".to_string());
}
// 统一百炼保留配置中的区域/工作空间主机,并切换到 Qwen Realtime 路径。
let endpoint = if crate::coordinator::unified_bailian_is_active() {
let endpoint = scope
.get(CredentialAccount::AsrEndpoint)
.map_err(|e| e.to_string())?
.unwrap_or_default();
crate::coordinator::derive_bailian_endpoint(
&endpoint,
crate::coordinator::BailianEndpointProtocol::QwenRealtime,
)?
} else {
scope
.get(CredentialAccount::AsrEndpoint)
.map_err(|e| e.to_string())?
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| crate::asr::qwen_realtime::DEFAULT_ENDPOINT.to_string())
};
let endpoint = scope
.get(CredentialAccount::AsrEndpoint)
.map_err(|e| e.to_string())?
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| crate::asr::qwen_realtime::DEFAULT_ENDPOINT.to_string());
let endpoint = derive_scoped_bailian_endpoint(
&scope.provider_type(),
&endpoint,
crate::coordinator::BailianEndpointProtocol::QwenRealtime,
)?;
if !crate::asr::qwen_realtime::endpoint_scheme_is_secure_websocket(&endpoint) {
return Err("qwen3EndpointSchemeInvalid".to_string());
}
@@ -1287,10 +1292,11 @@ mod tests {
// LLM 路径的 SSRF 校验。read_openai_provider_config 依赖凭据库无法纯单测,这里直接对它调用
// 的校验器锁定 ASR 形态 endpoint 的拒绝/放行契约。
use super::{
asr_error_is_no_speech_rejection, fetch_provider_models, models_url,
provider_llm_error_message, provider_log_context, provider_request_error_message,
sanitized_provider_destination, send_dashscope_multimodal_validation,
volcengine_missing_credential_error, ProviderConfig, ProviderScope,
asr_error_is_no_speech_rejection, derive_scoped_bailian_endpoint, fetch_provider_models,
models_url, provider_llm_error_message, provider_log_context,
provider_request_error_message, sanitized_provider_destination,
send_dashscope_multimodal_validation, volcengine_missing_credential_error, ProviderConfig,
ProviderScope,
};
use crate::endpoint_security::validate_http_endpoint;
@@ -1324,6 +1330,19 @@ mod tests {
}
}
#[test]
fn non_active_unified_bailian_channel_derives_the_selected_model_endpoint() {
assert_eq!(
derive_scoped_bailian_endpoint(
crate::asr::bailian::PROVIDER_ID,
"wss://dashscope.aliyuncs.com/api-ws/v1/inference",
crate::coordinator::BailianEndpointProtocol::Multimodal,
)
.unwrap(),
crate::asr::dashscope_multimodal::DEFAULT_ENDPOINT
);
}
#[test]
fn volcengine_missing_credential_error_follows_auth_mode() {
use crate::asr::volcengine::VolcengineAuthMode;