2 Commits
v1.0 ... main

3 changed files with 126 additions and 20 deletions

View File

@@ -21,6 +21,11 @@
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,20 +1,7 @@
/*
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.easyagents.embedding.openai;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.core.document.Document;
import com.easyagents.core.model.client.HttpClient;
import com.easyagents.core.model.embedding.BaseEmbeddingModel;
@@ -24,10 +11,9 @@ import com.easyagents.core.store.VectorData;
import com.easyagents.core.util.JSONUtil;
import com.easyagents.core.util.Maps;
import com.easyagents.core.util.StringUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConfig> {
@@ -69,19 +55,63 @@ public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConf
VectorData vectorData = new VectorData();
double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
if (embedding == null || embedding.length == 0) {
throw new ModelException(buildMissingEmbeddingMessage());
}
vectorData.setVector(embedding);
return vectorData;
}
/**
* Builds the embeddings request payload for OpenAI-compatible providers.
*
* @param text document to embed
* @param options embedding request options
* @param config model configuration
* @return JSON payload for the embeddings endpoint
*/
public static String promptToEmbeddingsPayload(Document text, EmbeddingOptions options, OpenAIEmbeddingConfig config) {
// https://platform.openai.com/docs/api-reference/making-requests
return Maps.of("model", options.getModelOrDefault(config.getModel()))
String model = options.getModelOrDefault(config.getModel());
return Maps.of("model", model)
.set("encoding_format", options.getEncodingFormatOrDefault("float"))
.set("input", text.getContent())
.setIfNotEmpty("user", options.getUser())
.setIfNotEmpty("dimensions", options.getDimensions())
.setIf(
supportsDimensionsParameter(model) && options.getDimensions() != null,
"dimensions",
options.getDimensions()
)
.toJSON();
}
/**
* Determines whether the upstream embeddings endpoint supports dynamic dimensions.
*
* @param model model name sent to the provider
* @return true when dimensions should be sent
*/
static boolean supportsDimensionsParameter(String model) {
if (StringUtil.noText(model)) {
return false;
}
String normalizedModel = model.toLowerCase(Locale.ROOT);
return normalizedModel.contains("qwen3-embedding")
|| normalizedModel.contains("qwen");
}
/**
* Builds a safe diagnostic message for malformed embeddings responses.
*
* @return diagnostic message without secrets or raw response content
*/
private String buildMissingEmbeddingMessage() {
return "Embedding response does not contain data[0].embedding."
+ " Please check provider, model, request path, dimensions, or response format."
+ " provider=" + config.getProvider()
+ ", model=" + config.getModel()
+ ", endpoint=" + config.getEndpoint()
+ ", requestPath=" + config.getRequestPath();
}
}

View File

@@ -0,0 +1,71 @@
package com.easyagents.embedding.openai;
import com.easyagents.core.document.Document;
import com.easyagents.core.model.client.HttpClient;
import com.easyagents.core.model.embedding.EmbeddingOptions;
import com.easyagents.core.model.exception.ModelException;
import org.junit.Assert;
import org.junit.Test;
import java.util.Map;
/**
* Tests for OpenAI-compatible embeddings request payload generation.
*/
public class OpenAIEmbeddingModelTest {
/**
* Verifies that fixed-dimension embeddings models do not receive dimensions.
*/
@Test
public void shouldNotSendDimensionsForBgeModel() {
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
config.setModel("BAAI/bge-m3");
EmbeddingOptions options = new EmbeddingOptions();
options.setDimensions(1024);
String payload = OpenAIEmbeddingModel.promptToEmbeddingsPayload(Document.of("hello"), options, config);
Assert.assertFalse(payload.contains("\"dimensions\""));
}
/**
* Verifies that Qwen3 embedding models keep the dynamic dimensions parameter.
*/
@Test
public void shouldSendDimensionsForQwen3EmbeddingModel() {
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
config.setModel("Qwen/Qwen3-Embedding-8B");
EmbeddingOptions options = new EmbeddingOptions();
options.setDimensions(1024);
String payload = OpenAIEmbeddingModel.promptToEmbeddingsPayload(Document.of("hello"), options, config);
Assert.assertTrue(payload.contains("\"dimensions\":1024"));
}
/**
* Verifies that malformed embeddings responses fail with a clear model exception.
*/
@Test
public void shouldThrowModelExceptionWhenEmbeddingMissing() {
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
config.setProvider("test-provider");
config.setModel("BAAI/bge-m3");
config.setApiKey("test-key");
OpenAIEmbeddingModel model = new OpenAIEmbeddingModel(config);
model.setHttpClient(new HttpClient() {
@Override
public String post(String url, Map<String, String> headers, String payload) {
return "{\"data\":[{}]}";
}
});
ModelException exception = Assert.assertThrows(
ModelException.class,
() -> model.embed(Document.of("hello"))
);
Assert.assertTrue(exception.getMessage().contains("data[0].embedding"));
}
}