From 9e8682d133e290cfc9fb74ea9721472cc70943e2 Mon Sep 17 00:00:00 2001 From: Asad Date: Sat, 11 Jul 2026 20:12:25 +0530 Subject: [PATCH 1/3] bugfix: allow optional embedding dimensions in OpenAITextEmbedding to support open-source models --- .../embedding/openai/OpenAITextEmbedding.java | 67 ++++++++++++++++--- .../openai/OpenAITextEmbeddingEmbedTest.java | 49 ++++++++++++++ .../openai/OpenAITextEmbeddingTest.java | 64 ++++++++++++++++++ 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java index 35290f27a0..8ff901aeb2 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java @@ -47,9 +47,12 @@ public class OpenAITextEmbedding implements EmbeddingModel { private static final Logger log = LoggerFactory.getLogger(OpenAITextEmbedding.class); + /** Default dimension when not explicitly configured, per EmbeddingModel interface contract. */ + private static final int DEFAULT_DIMENSIONS = 1024; + private final String apiKey; private final String modelName; - private final int dimensions; + private final Integer dimensions; private final ExecutionConfig defaultExecutionConfig; private final String baseUrl; @@ -57,6 +60,9 @@ public class OpenAITextEmbedding implements EmbeddingModel { /** * Creates a new OpenAI text embedding model instance. * + *

This constructor is kept for binary compatibility with existing compiled code. + * It delegates to {@link #OpenAITextEmbedding(String, String, Integer, ExecutionConfig, String)}. + * * @param apiKey the API key for OpenAI authentication * @param modelName the model name (e.g., "text-embedding-3-small") * @param dimensions the dimension of embedding vectors @@ -69,6 +75,32 @@ public OpenAITextEmbedding( int dimensions, ExecutionConfig defaultExecutionConfig, String baseUrl) { + this(apiKey, modelName, Integer.valueOf(dimensions), defaultExecutionConfig, baseUrl); + } + + /** + * Creates a new OpenAI text embedding model instance with optional dimensions. + * + *

When {@code dimensions} is null, the dimensions parameter will not be sent + * to the API, allowing open-source models that don't support matryoshka + * representation (e.g., BAAI/bge-large-zh-v1.5) to work correctly. + * + * @param apiKey the API key for OpenAI authentication + * @param modelName the model name (e.g., "text-embedding-3-small") + * @param dimensions the dimension of embedding vectors (null to omit from API request) + * @param defaultExecutionConfig default execution configuration for timeout and retry + * @param baseUrl custom base URL for OpenAI API (null for default) + * @throws IllegalArgumentException if dimensions is non-null and not positive + */ + public OpenAITextEmbedding( + String apiKey, + String modelName, + Integer dimensions, + ExecutionConfig defaultExecutionConfig, + String baseUrl) { + if (dimensions != null && dimensions <= 0) { + throw new IllegalArgumentException("dimensions must be positive, got: " + dimensions); + } this.apiKey = apiKey; this.modelName = modelName; this.dimensions = dimensions; @@ -132,15 +164,19 @@ public Mono embed(ContentBlock block) { OpenAIClient client = clientBuilder.build(); - EmbeddingCreateParams createParams = + EmbeddingCreateParams.Builder paramsBuilder = EmbeddingCreateParams.builder() .model(modelName) - .dimensions(dimensions) .encodingFormat( EmbeddingCreateParams.EncodingFormat .FLOAT) - .inputOfArrayOfStrings(List.of(text)) - .build(); + .inputOfArrayOfStrings(List.of(text)); + + if (dimensions != null) { + paramsBuilder.dimensions(dimensions); + } + + EmbeddingCreateParams createParams = paramsBuilder.build(); log.debug( "OpenAI embedding call: model={}," @@ -181,8 +217,9 @@ public Mono embed(ContentBlock block) { EmbeddingUtils.convertFloatListToDoubleArray( embeddingValues); - // Validate dimension - if (embeddingArray.length != dimensions) { + // Validate dimension only when explicitly configured + if (dimensions != null + && embeddingArray.length != dimensions) { log.warn( "Embedding dimension mismatch: expected={}," + " actual={}", @@ -225,7 +262,7 @@ public String getModelName() { @Override public int getDimensions() { - return dimensions; + return dimensions != null ? dimensions : DEFAULT_DIMENSIONS; } /** @@ -234,7 +271,7 @@ public int getDimensions() { public static class Builder { private String apiKey; private String modelName; - private int dimensions = 1536; + private Integer dimensions; private ExecutionConfig defaultExecutionConfig; private String baseUrl; @@ -261,7 +298,15 @@ public Builder modelName(String modelName) { } /** - * Sets the dimension of embedding vectors. + * Sets the dimension of embedding vectors. This parameter is optional. + * + *

When set, the dimensions parameter will be included in the API request. + * This is only supported by OpenAI official embedding models (e.g., + * text-embedding-3-small, text-embedding-3-large) that support matryoshka + * representation. + * + *

For open-source models (e.g., BAAI/bge-large-zh-v1.5), do NOT set this + * parameter as those models do not support it and will return a 400 error. * * @param dimensions the dimension * @return this builder instance @@ -311,7 +356,7 @@ public OpenAITextEmbedding build() { throw new IllegalStateException( "modelName is required and cannot be null or empty"); } - if (dimensions <= 0) { + if (dimensions != null && dimensions <= 0) { throw new IllegalStateException("dimensions must be positive, got: " + dimensions); } diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java index d8185974d0..6623ab6a92 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java @@ -314,4 +314,53 @@ void testEmptyEmbeddingListInResponse() { .verify(); } } + + @Test + @DisplayName("Should successfully embed without dimensions parameter (open-source models)") + void testEmbedWithoutDimensions() { + // Create model without setting dimensions - simulates open-source model usage + OpenAITextEmbedding modelWithoutDimensions = + OpenAITextEmbedding.builder() + .apiKey("mock_api_key") + .modelName("BAAI/bge-large-zh-v1.5") + .executionConfig(ExecutionConfig.builder().maxAttempts(1).build()) + .build(); + + // Prepare mock response + Embedding mockEmbedding = mock(Embedding.class); + when(mockEmbedding.embedding()).thenReturn(Arrays.asList(0.5f, 0.6f, 0.7f, 0.8f)); + + CreateEmbeddingResponse mockResponse = mock(CreateEmbeddingResponse.class); + when(mockResponse.data()).thenReturn(Arrays.asList(mockEmbedding)); + + try (MockedStatic mockedClient = + Mockito.mockStatic(OpenAIOkHttpClient.class)) { + + OpenAIOkHttpClient.Builder mockBuilder = mock(OpenAIOkHttpClient.Builder.class); + OpenAIClient mockOpenAIClient = mock(OpenAIClient.class); + EmbeddingService mockEmbeddings = mock(EmbeddingService.class); + + when(mockBuilder.build()).thenReturn(mockOpenAIClient); + when(mockBuilder.apiKey(any())).thenReturn(mockBuilder); + when(mockBuilder.baseUrl(any(String.class))).thenReturn(mockBuilder); + when(mockBuilder.putHeader(any(), any())).thenReturn(mockBuilder); + + mockedClient.when(OpenAIOkHttpClient::builder).thenReturn(mockBuilder); + + when(mockOpenAIClient.embeddings()).thenReturn(mockEmbeddings); + when(mockEmbeddings.create(any(EmbeddingCreateParams.class))).thenReturn(mockResponse); + + TextBlock textBlock = TextBlock.builder().text("Test open-source model").build(); + + StepVerifier.create(modelWithoutDimensions.embed(textBlock)) + .assertNext( + embedding -> { + assertNotNull(embedding); + assertEquals(4, embedding.length); + assertEquals(0.5, embedding[0], 0.001); + assertEquals(0.8, embedding[3], 0.001); + }) + .verifyComplete(); + } + } } diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java index 07dfd6ae6f..9b0b9f0de4 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java @@ -177,6 +177,34 @@ void testMinimalBuilder() { assertNotNull(model); } + @Test + @DisplayName("Should build successfully without dimensions for open-source models") + void testBuilderWithoutDimensions() { + OpenAITextEmbedding model = + OpenAITextEmbedding.builder() + .apiKey(TEST_API_KEY) + .modelName("BAAI/bge-large-zh-v1.5") + .build(); + + assertNotNull(model); + assertEquals("BAAI/bge-large-zh-v1.5", model.getModelName()); + } + + @Test + @DisplayName("Should return default dimensions (1024) when not explicitly set") + void testGetDimensionsReturnsDefaultWhenNotSet() { + OpenAITextEmbedding model = + OpenAITextEmbedding.builder() + .apiKey(TEST_API_KEY) + .modelName(TEST_MODEL_NAME) + .build(); + + assertEquals( + 1024, + model.getDimensions(), + "getDimensions() should return default 1024 per EmbeddingModel interface"); + } + @Test @DisplayName("Should apply timeout configuration") void testTimeoutConfiguration() { @@ -259,5 +287,41 @@ void testDimensionsValidation() { .modelName(TEST_MODEL_NAME) .dimensions(-1) .build()); + + // Building without calling dimensions() should NOT throw + OpenAITextEmbedding model = + OpenAITextEmbedding.builder() + .apiKey(TEST_API_KEY) + .modelName(TEST_MODEL_NAME) + .build(); + assertNotNull(model); + } + + @Test + @DisplayName("Should throw exception when constructed directly with non-positive dimensions") + void testDirectConstructorRejectsNonPositiveDimensions() { + assertThrows( + IllegalArgumentException.class, + () -> + new OpenAITextEmbedding( + TEST_API_KEY, TEST_MODEL_NAME, Integer.valueOf(0), null, null)); + + assertThrows( + IllegalArgumentException.class, + () -> + new OpenAITextEmbedding( + TEST_API_KEY, TEST_MODEL_NAME, Integer.valueOf(-1), null, null)); + } + + @Test + @DisplayName( + "Should support the deprecated int-dimensions constructor for binary compatibility") + void testDeprecatedIntConstructor() { + OpenAITextEmbedding model = + new OpenAITextEmbedding(TEST_API_KEY, TEST_MODEL_NAME, TEST_DIMENSIONS, null, null); + + assertNotNull(model); + assertEquals(TEST_MODEL_NAME, model.getModelName()); + assertEquals(TEST_DIMENSIONS, model.getDimensions()); } } From e7576206c73893fc2a4b12090ca2a584eba97c3f Mon Sep 17 00:00:00 2001 From: Asad Date: Sat, 11 Jul 2026 20:55:33 +0530 Subject: [PATCH 2/3] test: add unit test for OpenAI embedding with matching dimensions verification --- .../openai/OpenAITextEmbeddingEmbedTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java index 6623ab6a92..63f44821ac 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java @@ -363,4 +363,50 @@ void testEmbedWithoutDimensions() { .verifyComplete(); } } + + @Test + @DisplayName("Should not warn when returned embedding length matches configured dimensions") + void testEmbedWithMatchingDimensions() { + OpenAITextEmbedding modelWithMatchingDimensions = + OpenAITextEmbedding.builder() + .apiKey("mock_api_key") + .modelName(TEST_MODEL_NAME) + .dimensions(3) + .executionConfig(ExecutionConfig.builder().maxAttempts(1).build()) + .build(); + + Embedding mockEmbedding = mock(Embedding.class); + when(mockEmbedding.embedding()).thenReturn(Arrays.asList(0.1f, 0.2f, 0.3f)); + + CreateEmbeddingResponse mockResponse = mock(CreateEmbeddingResponse.class); + when(mockResponse.data()).thenReturn(Arrays.asList(mockEmbedding)); + + try (MockedStatic mockedClient = + Mockito.mockStatic(OpenAIOkHttpClient.class)) { + + OpenAIOkHttpClient.Builder mockBuilder = mock(OpenAIOkHttpClient.Builder.class); + OpenAIClient mockOpenAIClient = mock(OpenAIClient.class); + EmbeddingService mockEmbeddings = mock(EmbeddingService.class); + + when(mockBuilder.build()).thenReturn(mockOpenAIClient); + when(mockBuilder.apiKey(any())).thenReturn(mockBuilder); + when(mockBuilder.baseUrl(any(String.class))).thenReturn(mockBuilder); + when(mockBuilder.putHeader(any(), any())).thenReturn(mockBuilder); + + mockedClient.when(OpenAIOkHttpClient::builder).thenReturn(mockBuilder); + + when(mockOpenAIClient.embeddings()).thenReturn(mockEmbeddings); + when(mockEmbeddings.create(any(EmbeddingCreateParams.class))).thenReturn(mockResponse); + + TextBlock textBlock = TextBlock.builder().text("Matching dimensions").build(); + + StepVerifier.create(modelWithMatchingDimensions.embed(textBlock)) + .assertNext( + embedding -> { + assertNotNull(embedding); + assertEquals(3, embedding.length); + }) + .verifyComplete(); + } + } } From 4b80b8204cf33334542ab39e3efe1911df3c96f4 Mon Sep 17 00:00:00 2001 From: Asad Date: Sun, 12 Jul 2026 10:04:57 +0530 Subject: [PATCH 3/3] feat: update OpenAITextEmbedding to default dimensions to 1536 and allow explicit null to omit parameter --- .../embedding/openai/OpenAITextEmbedding.java | 28 +++++++++++++------ .../openai/OpenAITextEmbeddingEmbedTest.java | 3 +- .../openai/OpenAITextEmbeddingTest.java | 24 ++++++++++++++-- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java index 8ff901aeb2..f1c8339cf7 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/main/java/io/agentscope/core/embedding/openai/OpenAITextEmbedding.java @@ -260,6 +260,17 @@ public String getModelName() { return modelName; } + /** + * {@inheritDoc} + * + *

Returns the explicitly configured dimensions, or {@value #DEFAULT_DIMENSIONS} if {@code + * dimensions} was never configured (per the {@link EmbeddingModel#getDimensions()} contract, + * which documents 1024 as the default when unspecified). This fallback is a placeholder only: + * for open-source models where dimensions is intentionally left unset, the true embedding + * vector length returned by {@link #embed} may differ from this value. Callers needing the + * exact size should inspect the length of the array returned by {@link #embed}, not this + * getter. + */ @Override public int getDimensions() { return dimensions != null ? dimensions : DEFAULT_DIMENSIONS; @@ -271,7 +282,7 @@ public int getDimensions() { public static class Builder { private String apiKey; private String modelName; - private Integer dimensions; + private Integer dimensions = 1536; private ExecutionConfig defaultExecutionConfig; private String baseUrl; @@ -298,20 +309,21 @@ public Builder modelName(String modelName) { } /** - * Sets the dimension of embedding vectors. This parameter is optional. + * Sets the dimension of embedding vectors. Defaults to 1536. * - *

When set, the dimensions parameter will be included in the API request. - * This is only supported by OpenAI official embedding models (e.g., + *

When set to a positive value, the dimensions parameter will be included in the API + * request. This is only supported by OpenAI official embedding models (e.g., * text-embedding-3-small, text-embedding-3-large) that support matryoshka * representation. * - *

For open-source models (e.g., BAAI/bge-large-zh-v1.5), do NOT set this - * parameter as those models do not support it and will return a 400 error. + *

For open-source models (e.g., BAAI/bge-large-zh-v1.5) that do not support this + * parameter and will return a 400 error if it is sent, pass {@code null} to omit the + * parameter from the request entirely. * - * @param dimensions the dimension + * @param dimensions the dimension, or null to omit the parameter from the API request * @return this builder instance */ - public Builder dimensions(int dimensions) { + public Builder dimensions(Integer dimensions) { this.dimensions = dimensions; return this; } diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java index 63f44821ac..6f8da795c7 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingEmbedTest.java @@ -318,11 +318,12 @@ void testEmptyEmbeddingListInResponse() { @Test @DisplayName("Should successfully embed without dimensions parameter (open-source models)") void testEmbedWithoutDimensions() { - // Create model without setting dimensions - simulates open-source model usage + // Explicitly omit dimensions - simulates open-source model usage OpenAITextEmbedding modelWithoutDimensions = OpenAITextEmbedding.builder() .apiKey("mock_api_key") .modelName("BAAI/bge-large-zh-v1.5") + .dimensions(null) .executionConfig(ExecutionConfig.builder().maxAttempts(1).build()) .build(); diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java index 9b0b9f0de4..4571c23e1f 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-simple/src/test/java/io/agentscope/core/embedding/openai/OpenAITextEmbeddingTest.java @@ -178,12 +178,14 @@ void testMinimalBuilder() { } @Test - @DisplayName("Should build successfully without dimensions for open-source models") + @DisplayName( + "Should build successfully with dimensions explicitly omitted for open-source models") void testBuilderWithoutDimensions() { OpenAITextEmbedding model = OpenAITextEmbedding.builder() .apiKey(TEST_API_KEY) .modelName("BAAI/bge-large-zh-v1.5") + .dimensions(null) .build(); assertNotNull(model); @@ -191,12 +193,28 @@ void testBuilderWithoutDimensions() { } @Test - @DisplayName("Should return default dimensions (1024) when not explicitly set") - void testGetDimensionsReturnsDefaultWhenNotSet() { + @DisplayName("Should default dimensions to 1536 when not explicitly set") + void testBuilderDefaultsDimensionsTo1536() { + OpenAITextEmbedding model = + OpenAITextEmbedding.builder() + .apiKey(TEST_API_KEY) + .modelName(TEST_MODEL_NAME) + .build(); + + assertEquals( + 1536, + model.getDimensions(), + "Builder should default dimensions to 1536 for backward compatibility"); + } + + @Test + @DisplayName("Should return fallback dimensions (1024) when explicitly set to null") + void testGetDimensionsReturnsDefaultWhenExplicitlyNull() { OpenAITextEmbedding model = OpenAITextEmbedding.builder() .apiKey(TEST_API_KEY) .modelName(TEST_MODEL_NAME) + .dimensions(null) .build(); assertEquals(