refactor(llm): extract testable embedding protocol routing + drop dead fromProviderId

This commit is contained in:
matevip 2026-05-20 10:14:12 +08:00
parent 70a599e403
commit a6b5e3b515
3 changed files with 87 additions and 28 deletions

View File

@ -105,6 +105,25 @@ public class EmbeddingModelFactory {
cache.clear();
}
/**
* Pick the embedding protocol from the provider's {@code chatModel} column.
* Package-private for unit testing.
* <p>
* dashscope-compat carries 'dashscope' in its providerId but is wired with
* OpenAIChatModel + compatible-mode base URL, so substring-matching the
* providerId routed it to the native DashScope embedding endpoint and 404'd.
* Using the {@code chatModel} column matches {@link ModelProtocol#fromChatModel}
* for the chat path same signal, same case-insensitive trim semantics.
*/
static EmbeddingProtocol resolveEmbeddingProtocol(String chatModel) {
if (chatModel == null || chatModel.isBlank()) {
return EmbeddingProtocol.OPENAI_EMBEDDING;
}
return "DashScopeChatModel".equalsIgnoreCase(chatModel.trim())
? EmbeddingProtocol.DASHSCOPE_EMBEDDING
: EmbeddingProtocol.OPENAI_EMBEDDING;
}
// ==================== 内部实现 ====================
private EmbeddingModel doBuild(ModelConfigEntity modelConfig) {
@ -114,14 +133,7 @@ public class EmbeddingModelFactory {
"Embedding provider '" + modelConfig.getProvider() + "' not found in mate_model_provider");
}
// Use chatModel column (same signal as ModelProtocol.fromChatModel) rather than
// providerId substring matching. dashscope-compat has "dashscope" in its id but
// uses OpenAIChatModel + compatible-mode URL routing it to DASHSCOPE_EMBEDDING
// causes DashScopeApi to construct a native path that returns 404 against the
// compat base URL.
EmbeddingProtocol protocol = "DashScopeChatModel".equals(provider.getChatModel())
? EmbeddingProtocol.DASHSCOPE_EMBEDDING
: EmbeddingProtocol.OPENAI_EMBEDDING;
EmbeddingProtocol protocol = resolveEmbeddingProtocol(provider.getChatModel());
log.info("[EmbeddingFactory] Building embedding model: provider={}, chatModel={}, model={}, protocol={}",
provider.getProviderId(), provider.getChatModel(), modelConfig.getModelName(), protocol);

View File

@ -6,13 +6,14 @@ package vip.mate.llm.model;
* {@link ModelProtocol}Chat 协议分离chat embedding 在同一个 Provider
* 可能走不同的请求格式
* <ul>
* <li>DashScope embedding endpoint 是专用 path/api/v1/services/embeddings/text-embedding/text-embedding</li>
* <li>DashScope embedding endpoint 是专用 path
* /api/v1/services/embeddings/text-embedding/text-embedding</li>
* <li>OpenAI 兼容协议的 embedding 统一走 /v1/embeddings</li>
* </ul>
* <p>
* <b>注意</b>{@link EmbeddingModelFactory} 现在优先通过 {@code chatModel} 列判断协议
* {@link ModelProtocol#fromChatModel} 保持一致{@link #fromProviderId} 已不再使用
* 保留该方法仅作参考不要在新代码中调用它
* Routing happens in {@code EmbeddingModelFactory.resolveEmbeddingProtocol}
* via the {@code chatModel} column on {@link ModelProviderEntity}, matching
* {@link ModelProtocol#fromChatModel}.
*
* @author MateClaw Team
*/
@ -30,20 +31,4 @@ public enum EmbeddingProtocol {
public String getId() {
return id;
}
/**
* providerId 推断 embedding 协议
* <ul>
* <li>dashscope / 任何包含 "dashscope" / "qwen" / "aliyun" DASHSCOPE_EMBEDDING</li>
* <li>其他openai / deepseek / kimi / zhipu / moonshot / 任何 OpenAI 兼容 OPENAI_EMBEDDING</li>
* </ul>
*/
public static EmbeddingProtocol fromProviderId(String providerId) {
if (providerId == null) return OPENAI_EMBEDDING;
String p = providerId.toLowerCase().trim();
if (p.contains("dashscope") || p.contains("qwen") || p.contains("aliyun")) {
return DASHSCOPE_EMBEDDING;
}
return OPENAI_EMBEDDING;
}
}

View File

@ -0,0 +1,62 @@
package vip.mate.llm.embedding;
import org.junit.jupiter.api.Test;
import vip.mate.llm.model.EmbeddingProtocol;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Routing tests for {@link EmbeddingModelFactory#resolveEmbeddingProtocol(String)}.
* The dashscope-compat regression (#166) lives in the OpenAIChatModel branch.
*/
class EmbeddingModelFactoryRoutingTest {
@Test
void dashscopeChatModel_routesToNativeEmbedding() {
assertEquals(EmbeddingProtocol.DASHSCOPE_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol("DashScopeChatModel"));
}
@Test
void openaiChatModel_routesToOpenAiCompat() {
// dashscope-compat carries chatModel='OpenAIChatModel' must NOT take the native path
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol("OpenAIChatModel"));
}
@Test
void anthropicChatModel_routesToOpenAiCompat() {
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol("AnthropicChatModel"));
}
@Test
void nullChatModel_routesToOpenAiCompat() {
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol(null));
}
@Test
void blankChatModel_routesToOpenAiCompat() {
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol(""));
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol(" "));
}
@Test
void chatModelMatchIsCaseInsensitiveAndTrimmed() {
// Mirror ModelProtocol.fromChatModel's normalization: equalsIgnoreCase + trim.
assertEquals(EmbeddingProtocol.DASHSCOPE_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol("dashscopechatmodel"));
assertEquals(EmbeddingProtocol.DASHSCOPE_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol(" DashScopeChatModel "));
}
@Test
void unknownChatModel_fallsBackToOpenAiCompat() {
// Future / custom chatModel strings default to OpenAI-compatible (safe default).
assertEquals(EmbeddingProtocol.OPENAI_EMBEDDING,
EmbeddingModelFactory.resolveEmbeddingProtocol("CustomGatewayChatModel"));
}
}