fix(llm): classify DashScope 'url error' as client error + broaden dot-version purge

This commit is contained in:
matevip 2026-04-16 18:15:35 +08:00
parent 8a25d723f0
commit adfe23cd2c
5 changed files with 102 additions and 15 deletions

View File

@ -152,6 +152,19 @@ public class NodeStreamingChatHelper {
|| msg.contains("invalid_request_error") || msg.contains("unsupported")) {
return ErrorType.CLIENT_ERROR;
}
// DashScope-specific "model name does not map to a valid endpoint" reported as
// "[InvalidParameter] url error, please check url" (see
// https://help.aliyun.com/zh/model-studio/error-code#error-url). Despite the wording
// it's not a URL issue it's the provider rejecting an unknown/unsupported model id
// on the native protocol. Treat as client error so we do NOT retry.
if (msg.contains("[InvalidParameter]")
|| msg.contains("InvalidParameter")
|| msg.contains("url error")
|| msg.contains("Model not exist")
|| msg.contains("model_not_found")
|| msg.contains("Model not found")) {
return ErrorType.CLIENT_ERROR;
}
// Server errors
if (msg.contains("500") || msg.contains("502") || msg.contains("503") || msg.contains("504")
|| msg.contains("APITimeoutError") || msg.contains("APIConnectionError")
@ -211,10 +224,15 @@ public class NodeStreamingChatHelper {
if (lastResult.errorType() == ErrorType.THINKING_BLOCK_ERROR) {
return lastResult; // 已经重试过了
}
// 成功或不可重试
// 成功
if (lastResult.errorMessage() == null || lastResult.errorType() == ErrorType.NONE) {
return lastResult;
}
// Any other non-null errored result with a classified type that doStreamCall
// chose NOT to retry (i.e. UNKNOWN, or RATE_LIMIT/SERVER_ERROR past MAX_RETRIES)
// must exit otherwise we silently spin through attempts and waste seconds
// per turn on unrecoverable errors like DashScope's "url error" / unknown model.
return lastResult;
}
// lastResult == null 表示需要重试
}
@ -640,6 +658,13 @@ public class NodeStreamingChatHelper {
private static String extractUserFriendlyError(Throwable error) {
String msg = error.getMessage();
if (msg == null) return error.getClass().getSimpleName();
// DashScope "url error" is really "model name not mapped to any valid endpoint".
// Translate it so users see the real cause and the actionable next step.
if (msg.contains("url error") || msg.contains("[InvalidParameter]")
|| msg.contains("Model not exist") || msg.contains("model_not_found")
|| msg.contains("Model not found")) {
return "Model name not available on this provider — verify the model exists and is supported (Settings → Models)";
}
// Jackson 反序列化错误提取关键信息
if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later";
if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead";

View File

@ -51,6 +51,15 @@ public class ModelDiscoveryService {
"qwen3.5-plus"
);
/**
* Pattern matching DashScope model ids that use a dot-versioned family (e.g.
* "qwen3.5-max", "qwen3.6-plus"). These are only offered on compatible-mode
* and consistently fail on the native endpoint with
* "[InvalidParameter] url error". Block them regardless of exact name.
*/
private static final java.util.regex.Pattern DASHSCOPE_NATIVE_UNSUPPORTED_PATTERN =
java.util.regex.Pattern.compile("^qwen\\d+\\.\\d+.*", java.util.regex.Pattern.CASE_INSENSITIVE);
/**
* Allow-list prefixes for DashScope models that are known to work on the native
* protocol. An empty set means "no prefix filter" (we still apply DENY).
@ -120,15 +129,7 @@ public class ModelDiscoveryService {
}
int before = discovered.size();
List<ModelInfoDTO> filtered = discovered.stream()
.filter(m -> {
String id = m.getId();
if (id == null || id.isBlank()) return false;
String lower = id.toLowerCase();
if (DASHSCOPE_NATIVE_DENY.contains(lower)) return false;
// Allow if any allowed prefix matches; if allow-list is empty, permit everything
if (DASHSCOPE_NATIVE_ALLOW_PREFIXES.isEmpty()) return true;
return DASHSCOPE_NATIVE_ALLOW_PREFIXES.stream().anyMatch(lower::startsWith);
})
.filter(m -> isDashScopeModelIdAcceptable(m.getId()))
.toList();
if (filtered.size() < before) {
log.info("[ModelDiscovery] Filtered {} -> {} DashScope models for provider={} (allow/deny rules)",
@ -137,6 +138,39 @@ public class ModelDiscoveryService {
return filtered;
}
/**
* Return true if a DashScope model id is allowed on the native protocol:
* not in the explicit DENY set, doesn't match the dot-version unsupported
* pattern, and matches at least one ALLOW prefix (or the allow list is empty).
*/
private static boolean isDashScopeModelIdAcceptable(String modelId) {
if (modelId == null || modelId.isBlank()) return false;
String lower = modelId.toLowerCase();
if (DASHSCOPE_NATIVE_DENY.contains(lower)) return false;
if (DASHSCOPE_NATIVE_UNSUPPORTED_PATTERN.matcher(lower).matches()) return false;
if (DASHSCOPE_NATIVE_ALLOW_PREFIXES.isEmpty()) return true;
return DASHSCOPE_NATIVE_ALLOW_PREFIXES.stream().anyMatch(lower::startsWith);
}
/**
* Defensive guard for code paths that persist a model id without going through
* discovery (e.g. the manual "Add model" form). Throws a MateClawException with
* a user-friendly message if the id is known to be unusable under the provider's
* runtime protocol.
*/
public static void assertModelIdAcceptable(String providerId, ModelProviderEntity provider, String modelId) {
if (provider == null) return;
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
if (protocol == ModelProtocol.DASHSCOPE_NATIVE && !isDashScopeModelIdAcceptable(modelId)) {
throw new MateClawException(
"err.llm.model_not_supported",
"Model id '" + modelId + "' is not supported on DashScope native protocol. " +
"Dot-versioned families (e.g. qwen3.5-*, qwen3.6-*) are only available via compatible-mode. " +
"Use an allowed id such as qwen-max / qwen-plus / qwen3-max."
);
}
}
/**
* Probe each discovered model in parallel (bounded concurrency) using the same
* protocol the runtime will use. Populates {@code probeOk}/{@code probeError}
@ -253,10 +287,9 @@ public class ModelDiscoveryService {
for (String modelId : modelIds) {
if (modelId == null || modelId.isBlank()) continue;
if (existingIds.contains(modelId)) continue;
// Defense-in-depth: never add a DashScope model that is on the native protocol deny list
if (protocol == ModelProtocol.DASHSCOPE_NATIVE
&& DASHSCOPE_NATIVE_DENY.contains(modelId.toLowerCase())) {
log.warn("[ModelDiscovery] Refusing to add {} — on DashScope native deny list", modelId);
// Defense-in-depth: never add a DashScope model that fails the protocol-aware check
if (protocol == ModelProtocol.DASHSCOPE_NATIVE && !isDashScopeModelIdAcceptable(modelId)) {
log.warn("[ModelDiscovery] Refusing to add {} — blocked by DashScope native protocol filter", modelId);
skipped++;
continue;
}

View File

@ -121,7 +121,15 @@ public class ModelProviderService {
public ProviderInfoDTO addModel(String providerId, AddProviderModelRequest request) {
getProvider(providerId);
modelConfigService.addModelToProvider(providerId, request.getId(), request.getName(), false);
// Defense-in-depth: the manual "Add model" form must apply the same
// protocol-level safety as auto-discovery otherwise users can freely
// type an unknown model id (e.g. "qwen3.6-plus") that DashScope native
// rejects at runtime with the opaque "[InvalidParameter] url error".
String modelId = request.getId();
if (modelId != null && !modelId.isBlank()) {
ModelDiscoveryService.assertModelIdAcceptable(providerId, this.getProvider(providerId), modelId);
}
modelConfigService.addModelToProvider(providerId, modelId, request.getName(), false);
return toProviderInfo(getProvider(providerId), modelConfigService.listModelsByProvider(providerId));
}

View File

@ -0,0 +1,13 @@
-- V16: Broaden the DashScope native-protocol purge beyond the two explicit ids in V15.
-- Any model whose name matches "qwenN.M-*" (dot-versioned family like qwen3.5-*, qwen3.6-*)
-- only exists on compatible-mode and fails on the native endpoint with
-- "[InvalidParameter] url error". Users who manually added such ids through the
-- "Add model" form before V16's runtime guard was in place end up with conversations
-- that silently fail. Clear them here so those users get a fresh start.
DELETE FROM mate_model_config
WHERE provider = 'dashscope'
AND (model_name LIKE 'qwen1.%'
OR model_name LIKE 'qwen2.%'
OR model_name LIKE 'qwen3.%'
OR model_name LIKE 'qwen4.%'
OR model_name LIKE 'qwen5.%');

View File

@ -0,0 +1,8 @@
-- V16: Broaden the DashScope native-protocol purge (see V15).
DELETE FROM mate_model_config
WHERE provider = 'dashscope'
AND (model_name LIKE 'qwen1.%'
OR model_name LIKE 'qwen2.%'
OR model_name LIKE 'qwen3.%'
OR model_name LIKE 'qwen4.%'
OR model_name LIKE 'qwen5.%');