fix(llm): skip chat-style probe for embedding-prefix models in DashScope discovery

This commit is contained in:
matevip 2026-05-20 10:14:25 +08:00
parent 58c53687e5
commit 3340885da3
2 changed files with 95 additions and 1 deletions

View File

@ -139,6 +139,17 @@ public class ModelDiscoveryService {
"qwen3-livetranslate-"
);
/**
* Model-id prefixes that mark an embedding model. These pass the chat-focused
* acceptable-id check (so the manual "Add model" form works) but must be
* excluded from chat-style runtime probes and from "new chat models" auto
* suggestions they only speak the embeddings protocol, not chat completion.
*/
private static final Set<String> EMBEDDING_MODEL_PREFIXES = Set.of(
"text-embedding-",
"embedding-"
);
/**
* Allow-list prefixes for DashScope models that are known to work on the native
* protocol (chat or embedding). An empty set means "no prefix filter" (we still
@ -183,10 +194,13 @@ public class ModelDiscoveryService {
.map(ModelConfigEntity::getModelName)
.collect(Collectors.toSet());
// Only propose models that passed the probe (or were not probed) as "new"
// Only propose models that passed the probe (or were not probed) as "new".
// Embedding models are catalog-visible but excluded from the chat-discovery
// suggestion bucket adding them as chat models would 400 at runtime.
List<ModelInfoDTO> newModels = discovered.stream()
.filter(m -> !existingIds.contains(m.getId()))
.filter(m -> !Boolean.FALSE.equals(m.getProbeOk()))
.filter(m -> !isEmbeddingModelId(m.getId()))
.toList();
return new DiscoverResult(discovered, newModels, discovered.size(), newModels.size());
@ -235,6 +249,18 @@ public class ModelDiscoveryService {
return DASHSCOPE_NATIVE_ALLOW_PREFIXES.stream().anyMatch(lower::startsWith);
}
/**
* Returns true when a model id looks like an embedding model (e.g.
* {@code text-embedding-v3}). Used to skip chat-style probes and to keep
* embedding entries out of the "new chat models to add" auto-suggestion.
* Package-private for unit tests.
*/
static boolean isEmbeddingModelId(String modelId) {
if (modelId == null) return false;
String lower = modelId.toLowerCase();
return EMBEDDING_MODEL_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
@ -270,6 +296,13 @@ public class ModelDiscoveryService {
Semaphore sem = new Semaphore(MAX_PROBE_CONCURRENCY);
List<CompletableFuture<Void>> futures = new ArrayList<>(discovered.size());
for (ModelInfoDTO dto : discovered) {
if (isEmbeddingModelId(dto.getId())) {
// Embedding models don't speak chat completion sendTestPrompt would
// 400 with InvalidParameter. Leave probeOk null (no warning badge)
// and surface a clear, non-error reason in probeError.
dto.setProbeError("Embedding model — not chat-probeable");
continue;
}
futures.add(CompletableFuture.runAsync(() -> {
try { sem.acquire(); }
catch (InterruptedException ie) { Thread.currentThread().interrupt(); return; }

View File

@ -0,0 +1,61 @@
package vip.mate.llm.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Coverage for {@link ModelDiscoveryService#isEmbeddingModelId(String)}
* the predicate that keeps embedding models out of the chat-style probe and
* out of the chat-discovery "new models" suggestion bucket.
*/
class ModelDiscoveryEmbeddingDetectionTest {
@Test
void dashscopeEmbeddingVariants_recognised() {
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-v1"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-v3"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-v4"));
}
@Test
void openAiEmbeddingVariants_recognised() {
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-ada-002"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-3-small"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("text-embedding-3-large"));
}
@Test
void genericEmbeddingPrefix_recognised() {
// Some providers ship just "embedding-..." without the "text-" prefix.
assertTrue(ModelDiscoveryService.isEmbeddingModelId("embedding-001"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("embedding-large"));
}
@Test
void chatModels_notRecognisedAsEmbedding() {
assertFalse(ModelDiscoveryService.isEmbeddingModelId("qwen-plus"));
assertFalse(ModelDiscoveryService.isEmbeddingModelId("gpt-4o"));
assertFalse(ModelDiscoveryService.isEmbeddingModelId("claude-sonnet-4-6"));
assertFalse(ModelDiscoveryService.isEmbeddingModelId("deepseek-r1"));
}
@Test
void detectionIsCaseInsensitive() {
assertTrue(ModelDiscoveryService.isEmbeddingModelId("Text-Embedding-V4"));
assertTrue(ModelDiscoveryService.isEmbeddingModelId("TEXT-EMBEDDING-3-SMALL"));
}
@Test
void nullAndBlank_notEmbedding() {
assertFalse(ModelDiscoveryService.isEmbeddingModelId(null));
assertFalse(ModelDiscoveryService.isEmbeddingModelId(""));
}
@Test
void stringContainingEmbeddingMidway_notDetected() {
// Only prefix-anchored matches count; arbitrary mentions don't.
assertFalse(ModelDiscoveryService.isEmbeddingModelId("qwen-embedding-experimental"));
}
}