mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
fix(llm): classify DashScope 'url error' as client error + broaden dot-version purge
This commit is contained in:
parent
8a25d723f0
commit
adfe23cd2c
@ -152,6 +152,19 @@ public class NodeStreamingChatHelper {
|
|||||||
|| msg.contains("invalid_request_error") || msg.contains("unsupported")) {
|
|| msg.contains("invalid_request_error") || msg.contains("unsupported")) {
|
||||||
return ErrorType.CLIENT_ERROR;
|
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
|
// Server errors
|
||||||
if (msg.contains("500") || msg.contains("502") || msg.contains("503") || msg.contains("504")
|
if (msg.contains("500") || msg.contains("502") || msg.contains("503") || msg.contains("504")
|
||||||
|| msg.contains("APITimeoutError") || msg.contains("APIConnectionError")
|
|| msg.contains("APITimeoutError") || msg.contains("APIConnectionError")
|
||||||
@ -211,10 +224,15 @@ public class NodeStreamingChatHelper {
|
|||||||
if (lastResult.errorType() == ErrorType.THINKING_BLOCK_ERROR) {
|
if (lastResult.errorType() == ErrorType.THINKING_BLOCK_ERROR) {
|
||||||
return lastResult; // 已经重试过了
|
return lastResult; // 已经重试过了
|
||||||
}
|
}
|
||||||
// 成功或不可重试
|
// 成功
|
||||||
if (lastResult.errorMessage() == null || lastResult.errorType() == ErrorType.NONE) {
|
if (lastResult.errorMessage() == null || lastResult.errorType() == ErrorType.NONE) {
|
||||||
return lastResult;
|
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 表示需要重试
|
// lastResult == null 表示需要重试
|
||||||
}
|
}
|
||||||
@ -640,6 +658,13 @@ public class NodeStreamingChatHelper {
|
|||||||
private static String extractUserFriendlyError(Throwable error) {
|
private static String extractUserFriendlyError(Throwable error) {
|
||||||
String msg = error.getMessage();
|
String msg = error.getMessage();
|
||||||
if (msg == null) return error.getClass().getSimpleName();
|
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 反序列化错误,提取关键信息
|
// 对 Jackson 反序列化错误,提取关键信息
|
||||||
if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later";
|
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";
|
if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead";
|
||||||
|
|||||||
@ -51,6 +51,15 @@ public class ModelDiscoveryService {
|
|||||||
"qwen3.5-plus"
|
"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
|
* 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).
|
* protocol. An empty set means "no prefix filter" (we still apply DENY).
|
||||||
@ -120,15 +129,7 @@ public class ModelDiscoveryService {
|
|||||||
}
|
}
|
||||||
int before = discovered.size();
|
int before = discovered.size();
|
||||||
List<ModelInfoDTO> filtered = discovered.stream()
|
List<ModelInfoDTO> filtered = discovered.stream()
|
||||||
.filter(m -> {
|
.filter(m -> isDashScopeModelIdAcceptable(m.getId()))
|
||||||
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);
|
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
if (filtered.size() < before) {
|
if (filtered.size() < before) {
|
||||||
log.info("[ModelDiscovery] Filtered {} -> {} DashScope models for provider={} (allow/deny rules)",
|
log.info("[ModelDiscovery] Filtered {} -> {} DashScope models for provider={} (allow/deny rules)",
|
||||||
@ -137,6 +138,39 @@ public class ModelDiscoveryService {
|
|||||||
return filtered;
|
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
|
* Probe each discovered model in parallel (bounded concurrency) using the same
|
||||||
* protocol the runtime will use. Populates {@code probeOk}/{@code probeError}
|
* protocol the runtime will use. Populates {@code probeOk}/{@code probeError}
|
||||||
@ -253,10 +287,9 @@ public class ModelDiscoveryService {
|
|||||||
for (String modelId : modelIds) {
|
for (String modelId : modelIds) {
|
||||||
if (modelId == null || modelId.isBlank()) continue;
|
if (modelId == null || modelId.isBlank()) continue;
|
||||||
if (existingIds.contains(modelId)) continue;
|
if (existingIds.contains(modelId)) continue;
|
||||||
// Defense-in-depth: never add a DashScope model that is on the native protocol deny list
|
// Defense-in-depth: never add a DashScope model that fails the protocol-aware check
|
||||||
if (protocol == ModelProtocol.DASHSCOPE_NATIVE
|
if (protocol == ModelProtocol.DASHSCOPE_NATIVE && !isDashScopeModelIdAcceptable(modelId)) {
|
||||||
&& DASHSCOPE_NATIVE_DENY.contains(modelId.toLowerCase())) {
|
log.warn("[ModelDiscovery] Refusing to add {} — blocked by DashScope native protocol filter", modelId);
|
||||||
log.warn("[ModelDiscovery] Refusing to add {} — on DashScope native deny list", modelId);
|
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,15 @@ public class ModelProviderService {
|
|||||||
|
|
||||||
public ProviderInfoDTO addModel(String providerId, AddProviderModelRequest request) {
|
public ProviderInfoDTO addModel(String providerId, AddProviderModelRequest request) {
|
||||||
getProvider(providerId);
|
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));
|
return toProviderInfo(getProvider(providerId), modelConfigService.listModelsByProvider(providerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.%');
|
||||||
@ -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.%');
|
||||||
Loading…
Reference in New Issue
Block a user