From a6b5e3b515824b508754824c4246b8ee9dacf609 Mon Sep 17 00:00:00 2001 From: matevip Date: Wed, 20 May 2026 10:14:12 +0800 Subject: [PATCH] refactor(llm): extract testable embedding protocol routing + drop dead fromProviderId --- .../llm/embedding/EmbeddingModelFactory.java | 28 ++++++--- .../vip/mate/llm/model/EmbeddingProtocol.java | 25 ++------ .../EmbeddingModelFactoryRoutingTest.java | 62 +++++++++++++++++++ 3 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 mateclaw-server/src/test/java/vip/mate/llm/embedding/EmbeddingModelFactoryRoutingTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/llm/embedding/EmbeddingModelFactory.java b/mateclaw-server/src/main/java/vip/mate/llm/embedding/EmbeddingModelFactory.java index 1b60c2b6..dc5884d8 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/embedding/EmbeddingModelFactory.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/embedding/EmbeddingModelFactory.java @@ -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. + *

+ * 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); diff --git a/mateclaw-server/src/main/java/vip/mate/llm/model/EmbeddingProtocol.java b/mateclaw-server/src/main/java/vip/mate/llm/model/EmbeddingProtocol.java index a6530f88..62660697 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/model/EmbeddingProtocol.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/model/EmbeddingProtocol.java @@ -6,13 +6,14 @@ package vip.mate.llm.model; * 与 {@link ModelProtocol}(Chat 协议)分离——chat 和 embedding 在同一个 Provider 下 * 可能走不同的请求格式: *

*

- * 注意:{@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 协议。 - *

- */ - 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; - } } diff --git a/mateclaw-server/src/test/java/vip/mate/llm/embedding/EmbeddingModelFactoryRoutingTest.java b/mateclaw-server/src/test/java/vip/mate/llm/embedding/EmbeddingModelFactoryRoutingTest.java new file mode 100644 index 00000000..eded92b1 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/llm/embedding/EmbeddingModelFactoryRoutingTest.java @@ -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")); + } +}