mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(llm): purge unavailable DashScope models + protocol-aware discovery probe
This commit is contained in:
parent
b3c6b5a654
commit
ba086d75f2
@ -10,4 +10,20 @@ import lombok.NoArgsConstructor;
|
||||
public class ModelInfoDTO {
|
||||
private String id;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Discovery probe result. true = passed runtime-protocol ping test,
|
||||
* false = ping failed (listed by provider but unusable at runtime,
|
||||
* e.g. DashScope compatible-mode may list models the native SDK rejects).
|
||||
* null = not probed (probe disabled or still pending).
|
||||
*/
|
||||
private Boolean probeOk;
|
||||
|
||||
/** Reason text when probeOk=false (short, suitable for UI badge tooltip) */
|
||||
private String probeError;
|
||||
|
||||
public ModelInfoDTO(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,12 @@ import vip.mate.llm.model.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@ -27,6 +33,42 @@ public class ModelDiscoveryService {
|
||||
|
||||
private static final Duration TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
// Virtual-thread executor for parallel model probing (lightweight, short-lived)
|
||||
private static final ExecutorService PROBE_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
// Probe concurrency cap — avoid flooding the provider with concurrent ping requests
|
||||
private static final int MAX_PROBE_CONCURRENCY = 5;
|
||||
|
||||
// Per-model probe timeout (short; we only need to know "yes/no usable")
|
||||
private static final long PROBE_TIMEOUT_SECONDS = 12;
|
||||
|
||||
/**
|
||||
* Explicit deny list: model ids listed by DashScope compatible-mode that are known
|
||||
* to fail on the native protocol. Updated as we observe new failures.
|
||||
*/
|
||||
private static final Set<String> DASHSCOPE_NATIVE_DENY = Set.of(
|
||||
"qwen3.5-max",
|
||||
"qwen3.5-plus"
|
||||
);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Extend conservatively as we verify additional families.
|
||||
*/
|
||||
private static final Set<String> DASHSCOPE_NATIVE_ALLOW_PREFIXES = Set.of(
|
||||
"qwen-", // qwen-max / qwen-plus / qwen-turbo / qwen-coder-* / qwen-long
|
||||
"qwen2-", // qwen2 series
|
||||
"qwen3-", // qwen3-max / qwen3-plus / qwen3-coder / qwen3-235b-*
|
||||
"qwen-vl-", // vision-language
|
||||
"qwen-audio-",
|
||||
"qwen-omni-",
|
||||
"deepseek-", // deepseek-v3.x
|
||||
"baichuan",
|
||||
"yi-",
|
||||
"llama"
|
||||
);
|
||||
|
||||
// ==================== 模型发现 ====================
|
||||
|
||||
public DiscoverResult discoverModels(String providerId) {
|
||||
@ -38,17 +80,118 @@ public class ModelDiscoveryService {
|
||||
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
|
||||
List<ModelInfoDTO> discovered = fetchRemoteModels(provider, protocol);
|
||||
|
||||
// 去重:对比已有模型
|
||||
// Layer 2: Protocol-aware allow/deny filtering. The listing endpoint
|
||||
// (compatible-mode /v1/models for DashScope) often returns models that
|
||||
// the native SDK does not accept — filter them out before the user sees
|
||||
// them.
|
||||
discovered = applyProtocolFilter(discovered, protocol, providerId);
|
||||
|
||||
// Layer 3: Probe each remaining model with a real runtime-protocol call.
|
||||
// This catches any model the allow-list let through but the provider
|
||||
// actually rejects at request time. Failed probes are kept in the list
|
||||
// but marked probeOk=false so the UI can show a warning badge.
|
||||
discovered = probeInParallel(discovered, provider, protocol);
|
||||
|
||||
// De-dupe against already-configured models for the "new" bucket
|
||||
Set<String> existingIds = modelConfigService.listModelsByProvider(providerId).stream()
|
||||
.map(ModelConfigEntity::getModelName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Only propose models that passed the probe (or were not probed) as "new"
|
||||
List<ModelInfoDTO> newModels = discovered.stream()
|
||||
.filter(m -> !existingIds.contains(m.getId()))
|
||||
.filter(m -> !Boolean.FALSE.equals(m.getProbeOk()))
|
||||
.toList();
|
||||
|
||||
return new DiscoverResult(discovered, newModels, discovered.size(), newModels.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply protocol-aware allow/deny filtering to the raw discovery list.
|
||||
* <p>
|
||||
* Currently only DashScope is filtered: the compatible-mode listing includes
|
||||
* many models the native SDK rejects. Other providers pass through unchanged.
|
||||
*/
|
||||
private List<ModelInfoDTO> applyProtocolFilter(List<ModelInfoDTO> discovered,
|
||||
ModelProtocol protocol,
|
||||
String providerId) {
|
||||
if (protocol != ModelProtocol.DASHSCOPE_NATIVE) {
|
||||
return discovered;
|
||||
}
|
||||
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);
|
||||
})
|
||||
.toList();
|
||||
if (filtered.size() < before) {
|
||||
log.info("[ModelDiscovery] Filtered {} -> {} DashScope models for provider={} (allow/deny rules)",
|
||||
before, filtered.size(), providerId);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe each discovered model in parallel (bounded concurrency) using the same
|
||||
* protocol the runtime will use. Populates {@code probeOk}/{@code probeError}
|
||||
* on each DTO; does not remove failed entries so the UI can surface the reason.
|
||||
*/
|
||||
private List<ModelInfoDTO> probeInParallel(List<ModelInfoDTO> discovered,
|
||||
ModelProviderEntity provider,
|
||||
ModelProtocol protocol) {
|
||||
if (discovered.isEmpty()) return discovered;
|
||||
|
||||
// OpenAI ChatGPT has no model-level test, skip probe for it
|
||||
if (protocol == ModelProtocol.OPENAI_CHATGPT) return discovered;
|
||||
|
||||
Semaphore sem = new Semaphore(MAX_PROBE_CONCURRENCY);
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(discovered.size());
|
||||
for (ModelInfoDTO dto : discovered) {
|
||||
futures.add(CompletableFuture.runAsync(() -> {
|
||||
try { sem.acquire(); }
|
||||
catch (InterruptedException ie) { Thread.currentThread().interrupt(); return; }
|
||||
try {
|
||||
sendTestPrompt(provider, protocol, dto.getId());
|
||||
dto.setProbeOk(true);
|
||||
} catch (Exception e) {
|
||||
dto.setProbeOk(false);
|
||||
dto.setProbeError(shortError(e));
|
||||
log.info("[ModelDiscovery] Probe failed for model={}: {}", dto.getId(), dto.getProbeError());
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
}, PROBE_EXECUTOR));
|
||||
}
|
||||
try {
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.get(PROBE_TIMEOUT_SECONDS * Math.max(1, discovered.size() / MAX_PROBE_CONCURRENCY + 1),
|
||||
TimeUnit.SECONDS);
|
||||
} catch (TimeoutException te) {
|
||||
log.warn("[ModelDiscovery] Probe batch timeout; {} models may be marked unknown",
|
||||
futures.stream().filter(f -> !f.isDone()).count());
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelDiscovery] Probe batch wait failed: {}", e.getMessage());
|
||||
}
|
||||
long passed = discovered.stream().filter(m -> Boolean.TRUE.equals(m.getProbeOk())).count();
|
||||
long failed = discovered.stream().filter(m -> Boolean.FALSE.equals(m.getProbeOk())).count();
|
||||
log.info("[ModelDiscovery] Probe results: {} passed, {} failed, {} unknown (of {})",
|
||||
passed, failed, discovered.size() - passed - failed, discovered.size());
|
||||
return discovered;
|
||||
}
|
||||
|
||||
private String shortError(Exception e) {
|
||||
String msg = extractErrorMessage(e);
|
||||
if (msg == null) return "unknown error";
|
||||
// Clip to ~120 chars so the UI tooltip stays usable
|
||||
return msg.length() > 120 ? msg.substring(0, 120) + "..." : msg;
|
||||
}
|
||||
|
||||
// ==================== 连接测试 ====================
|
||||
|
||||
public TestResult testConnection(String providerId) {
|
||||
@ -99,17 +242,29 @@ public class ModelDiscoveryService {
|
||||
// ==================== 批量添加发现的模型 ====================
|
||||
|
||||
public int batchAddModels(String providerId, List<String> modelIds) {
|
||||
modelProviderService.getProviderConfig(providerId);
|
||||
ModelProviderEntity provider = modelProviderService.getProviderConfig(providerId);
|
||||
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
|
||||
Set<String> existingIds = modelConfigService.listModelsByProvider(providerId).stream()
|
||||
.map(ModelConfigEntity::getModelName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
int added = 0;
|
||||
int skipped = 0;
|
||||
for (String modelId : modelIds) {
|
||||
if (!existingIds.contains(modelId)) {
|
||||
modelConfigService.addModelToProvider(providerId, modelId, modelId, false);
|
||||
added++;
|
||||
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);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
modelConfigService.addModelToProvider(providerId, modelId, modelId, false);
|
||||
added++;
|
||||
}
|
||||
if (skipped > 0) {
|
||||
log.info("[ModelDiscovery] batchAddModels: added={}, skipped(deny)={}", added, skipped);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
@ -252,30 +407,65 @@ public class ModelDiscoveryService {
|
||||
return extractOpenAiChatContent(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a DashScope model using the **native** endpoint
|
||||
* ({@code /api/v1/services/aigc/text-generation/generation}).
|
||||
* <p>
|
||||
* This matches the protocol Spring AI Alibaba's {@code DashScopeChatModel} uses
|
||||
* at runtime. Using compatible-mode for testing (as the previous implementation
|
||||
* did) was the root cause of "test passed but chat fails" — compatible-mode
|
||||
* accepts a broader set of model names than the native API does.
|
||||
*/
|
||||
private String sendDashScopeTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||
String apiKey = provider.getApiKey();
|
||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
||||
throw new MateClawException("err.llm.dashscope_key_missing", "DashScope API Key 未配置");
|
||||
}
|
||||
|
||||
// DashScope native request shape: input.messages + parameters
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", modelId,
|
||||
"messages", List.of(Map.of("role", "user", "content", "请回复:连接正常")),
|
||||
"max_tokens", 10,
|
||||
"temperature", 0
|
||||
"input", Map.of(
|
||||
"messages", List.of(Map.of("role", "user", "content", "ping"))
|
||||
),
|
||||
"parameters", Map.of(
|
||||
"max_tokens", 1,
|
||||
"temperature", 0,
|
||||
"result_format", "message"
|
||||
)
|
||||
);
|
||||
|
||||
String body = RestClient.builder()
|
||||
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode")
|
||||
.baseUrl("https://dashscope.aliyuncs.com")
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.trim())
|
||||
.build()
|
||||
.post()
|
||||
.uri("/v1/chat/completions")
|
||||
.uri("/api/v1/services/aigc/text-generation/generation")
|
||||
.body(requestBody)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
return extractOpenAiChatContent(body);
|
||||
return extractDashScopeNativeContent(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content from DashScope native response:
|
||||
* {@code { "output": { "choices": [ { "message": { "content": "..." } } ] } } }
|
||||
* Falls back to the raw body preview if the shape differs.
|
||||
*/
|
||||
private String extractDashScopeNativeContent(String body) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
JsonNode choices = root.path("output").path("choices");
|
||||
if (choices.isArray() && choices.size() > 0) {
|
||||
String content = choices.get(0).path("message").path("content").asText("");
|
||||
if (!content.isBlank()) return content;
|
||||
}
|
||||
// Older shape: output.text
|
||||
String legacyText = root.path("output").path("text").asText("");
|
||||
if (!legacyText.isBlank()) return legacyText;
|
||||
} catch (Exception ignored) {}
|
||||
return body == null ? "" : (body.length() > 200 ? body.substring(0, 200) : body);
|
||||
}
|
||||
|
||||
private String sendGeminiTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||
|
||||
@ -159,8 +159,7 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
|
||||
(1000000101, 'Qwen3 Max', 'dashscope', 'qwen3-max', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000102, 'Qwen3 235B A22B Thinking', 'dashscope', 'qwen3-235b-a22b-thinking-2507', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000103, 'DeepSeek-V3.2', 'dashscope', 'deepseek-v3.2', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000170, 'Qwen3.5 Plus', 'dashscope', 'qwen3.5-plus', 'Qwen 3.5 series latest balanced model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000171, 'Qwen3.5 Max', 'dashscope', 'qwen3.5-max', 'Qwen 3.5 series strongest model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
-- Removed: qwen3.5-plus / qwen3.5-max — unavailable on DashScope native protocol (returns 400 InvalidParameter)
|
||||
(1000000172, 'Qwen3 Plus', 'dashscope', 'qwen3-plus', 'Qwen3 balanced model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000173, 'Qwen Long', 'dashscope', 'qwen-long', 'Long-context model with extended context support', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000104, 'Qwen3.5-122B-A10B', 'modelscope', 'Qwen/Qwen3.5-122B-A10B', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
|
||||
@ -160,8 +160,7 @@ VALUES
|
||||
(1000000101, 'Qwen3 Max', 'dashscope', 'qwen3-max', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000102, 'Qwen3 235B A22B Thinking', 'dashscope', 'qwen3-235b-a22b-thinking-2507', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000103, 'DeepSeek-V3.2', 'dashscope', 'deepseek-v3.2', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000170, 'Qwen3.5 Plus', 'dashscope', 'qwen3.5-plus', 'Qwen 3.5 series latest balanced model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000171, 'Qwen3.5 Max', 'dashscope', 'qwen3.5-max', 'Qwen 3.5 series strongest model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
-- Removed: qwen3.5-plus / qwen3.5-max — unavailable on DashScope native protocol (returns 400 InvalidParameter)
|
||||
(1000000172, 'Qwen3 Plus', 'dashscope', 'qwen3-plus', 'Qwen3 balanced model', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000173, 'Qwen Long', 'dashscope', 'qwen-long', 'Long-context model with extended context support', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000104, 'Qwen3.5-122B-A10B', 'modelscope', 'Qwen/Qwen3.5-122B-A10B', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
|
||||
@ -160,8 +160,7 @@ VALUES
|
||||
(1000000101, 'Qwen3 Max', 'dashscope', 'qwen3-max', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000102, 'Qwen3 235B A22B Thinking', 'dashscope', 'qwen3-235b-a22b-thinking-2507', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000103, 'DeepSeek-V3.2', 'dashscope', 'deepseek-v3.2', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000170, 'Qwen3.5 Plus', 'dashscope', 'qwen3.5-plus', 'Qwen 3.5 系列最新均衡模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000171, 'Qwen3.5 Max', 'dashscope', 'qwen3.5-max', 'Qwen 3.5 系列最强模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
-- Removed: qwen3.5-plus / qwen3.5-max — unavailable on DashScope native protocol (returns 400 InvalidParameter)
|
||||
(1000000172, 'Qwen3 Plus', 'dashscope', 'qwen3-plus', 'Qwen3 均衡模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000173, 'Qwen Long', 'dashscope', 'qwen-long', '长文本模型,支持超长上下文', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000104, 'Qwen3.5-122B-A10B', 'modelscope', 'Qwen/Qwen3.5-122B-A10B', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
|
||||
@ -165,8 +165,7 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
|
||||
(1000000101, 'Qwen3 Max', 'dashscope', 'qwen3-max', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000102, 'Qwen3 235B A22B Thinking', 'dashscope', 'qwen3-235b-a22b-thinking-2507', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000103, 'DeepSeek-V3.2', 'dashscope', 'deepseek-v3.2', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000170, 'Qwen3.5 Plus', 'dashscope', 'qwen3.5-plus', 'Qwen 3.5 系列最新均衡模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000171, 'Qwen3.5 Max', 'dashscope', 'qwen3.5-max', 'Qwen 3.5 系列最强模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
-- Removed: qwen3.5-plus / qwen3.5-max — unavailable on DashScope native protocol (returns 400 InvalidParameter)
|
||||
(1000000172, 'Qwen3 Plus', 'dashscope', 'qwen3-plus', 'Qwen3 均衡模型', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000173, 'Qwen Long', 'dashscope', 'qwen-long', '长文本模型,支持超长上下文', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
(1000000104, 'Qwen3.5-122B-A10B', 'modelscope', 'Qwen/Qwen3.5-122B-A10B', '', 0.7, 4096, 0.8, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
-- V15: Purge DashScope model seed rows that are unavailable on the native protocol
|
||||
-- These model names are listed by DashScope compatible-mode /v1/models but will fail
|
||||
-- with 400 InvalidParameter or "url error, please check url" on the native endpoint
|
||||
-- (/api/v1/services/aigc/text-generation/generation), which is what Spring AI Alibaba
|
||||
-- DashScopeChatModel actually uses at runtime. Keeping them around only leads to user
|
||||
-- confusion when conversations silently fail.
|
||||
DELETE FROM mate_model_config
|
||||
WHERE id IN (1000000170, 1000000171)
|
||||
AND provider = 'dashscope'
|
||||
AND builtin = TRUE;
|
||||
-- 1000000170 = qwen3.5-plus
|
||||
-- 1000000171 = qwen3.5-max
|
||||
@ -0,0 +1,7 @@
|
||||
-- V15: Purge DashScope model seed rows that are unavailable on the native protocol
|
||||
DELETE FROM mate_model_config
|
||||
WHERE id IN (1000000170, 1000000171)
|
||||
AND provider = 'dashscope'
|
||||
AND builtin = TRUE;
|
||||
-- 1000000170 = qwen3.5-plus
|
||||
-- 1000000171 = qwen3.5-max
|
||||
@ -574,6 +574,10 @@ export interface SystemSettings {
|
||||
export interface ProviderModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
/** Discovery probe result (backend `probeOk` field). True = verified reachable, false = probe failed, undefined = not probed */
|
||||
probeOk?: boolean
|
||||
/** Short error message when probeOk=false */
|
||||
probeError?: string
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
|
||||
@ -51,6 +51,9 @@
|
||||
<input type="checkbox" :value="model.id" :checked="selectedNewModelIds.includes(model.id)" @change="$emit('toggleModel', model.id)" />
|
||||
<span class="discover-model-name">{{ model.name }}</span>
|
||||
<span class="discover-model-id">{{ model.id }}</span>
|
||||
<span v-if="model.probeOk === true" class="probe-badge probe-ok" title="Verified reachable">
|
||||
✓ verified
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="discover-actions">
|
||||
@ -63,6 +66,17 @@
|
||||
({{ selectedNewModelIds.length }})
|
||||
</button>
|
||||
</div>
|
||||
<!-- Show models that were discovered but failed the probe, so users see
|
||||
why they are not offered in the "add selected" list -->
|
||||
<div v-if="discoveredUnavailable.length > 0" class="discover-unavailable">
|
||||
<div class="discover-unavailable-title">
|
||||
⚠ {{ discoveredUnavailable.length }} discovered model(s) failed the reachability probe and were not listed above:
|
||||
</div>
|
||||
<div v-for="model in discoveredUnavailable" :key="model.id" class="discover-unavailable-item">
|
||||
<span class="discover-model-id">{{ model.id }}</span>
|
||||
<span class="discover-unavailable-reason">{{ model.probeError || 'not reachable' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -136,12 +150,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { DiscoverResult, ProviderInfo, ProviderModelInfo, TestResult } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
provider: ProviderInfo | null
|
||||
modelForm: { id: string; name: string }
|
||||
@ -158,6 +173,13 @@ defineProps<{
|
||||
onIconError: (e: Event) => void
|
||||
}>()
|
||||
|
||||
// Models from discovery that failed the probe — shown in a warning block
|
||||
// so users understand why they are not in the "add selected" list
|
||||
const discoveredUnavailable = computed(() => {
|
||||
const all = props.discoverResult?.discoveredModels || []
|
||||
return all.filter(m => m && m.probeOk === false)
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
discover: []
|
||||
@ -244,6 +266,12 @@ defineEmits<{
|
||||
.discover-checkbox:hover { background: var(--mc-bg-sunken); }
|
||||
.discover-model-name { font-weight: 500; color: var(--mc-text-primary); }
|
||||
.discover-model-id { color: var(--mc-text-tertiary); margin-left: auto; font-size: 12px; }
|
||||
.probe-badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; font-weight: 600; margin-left: 6px; }
|
||||
.probe-ok { background: rgba(34, 197, 94, 0.12); color: rgb(21, 128, 61); }
|
||||
.discover-unavailable { margin-top: 12px; padding: 10px 12px; border-radius: 8px; background: rgba(234, 179, 8, 0.08); border: 1px solid rgba(234, 179, 8, 0.3); }
|
||||
.discover-unavailable-title { font-size: 12px; font-weight: 600; color: rgb(161, 98, 7); margin-bottom: 6px; }
|
||||
.discover-unavailable-item { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; font-size: 12px; }
|
||||
.discover-unavailable-reason { color: var(--mc-text-tertiary); margin-left: 10px; max-width: 60%; text-align: right; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }
|
||||
.discover-actions { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
|
||||
.model-list { display: grid; gap: 12px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user