From 244aa2574b463d2caed2ffbdbad2c62ae46f75ed Mon Sep 17 00:00:00 2001 From: Stephan Birkl Date: Thu, 7 May 2026 17:57:59 +0200 Subject: [PATCH 1/4] feat: Introduce OpenAiProviderDialect for flexible request shaping and remove reasoning properties from ModelProfile - Added OpenAiProviderDialect to manage request features for OpenAI-compatible providers. - Refactored ChatCompletionClient and TextCompletionClient to utilize the new dialect for handling reasoning and usage settings. - Removed reasoningEffort and reasoningSummaryDetails from ModelProfile and related services. - Updated README to reflect changes in request shaping and dialect usage. --- README.md | 51 +++++++- .../client/oai/OpenAiProviderDialect.java | 26 ++++ .../completion/AbstractCompletionClient.java | 31 +++++ .../completion/chat/ChatCompletionClient.java | 87 +++++++------ .../completion/text/TextCompletionClient.java | 31 +++-- .../de/extio/lmlib/profile/ModelProfile.java | 2 - .../lmlib/profile/ModelProfileService.java | 8 +- .../chat/ChatCompletionClientRequestTest.java | 121 ++++++++++++++++++ .../text/TextCompletionClientRequestTest.java | 65 ++++++++++ .../lmlib/token/TokenizerResolverTest.java | 2 - .../resources/local-chatcompletion.properties | 4 +- 11 files changed, 357 insertions(+), 71 deletions(-) create mode 100644 src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java create mode 100644 src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java create mode 100644 src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java diff --git a/README.md b/README.md index d98833c..d505d79 100644 --- a/README.md +++ b/README.md @@ -250,8 +250,6 @@ Each profile file is a `.properties` resource on the classpath. | `cost1MCachedInTokens` | Double | No | Cost per 1 million cached input tokens. Defaults to `cost1MInTokens`. | `0.35`, `0` | | `cost1MOutTokens` | Double | Yes | Cost per 1 million output tokens. | `0.71`, `15.00`, `0` | | `cost1MReasoningOutTokens` | Double | No | Cost per 1 million reasoning output tokens. Defaults to `0`. | `0`, `60.00` | -| `reasoningEffort` | String | No | Reasoning effort for reasoning-capable models. | `low`, `medium`, `high` | -| `reasoningSummaryDetails` | String | No | How reasoning content should be summarized. | `auto`, `concise`, `detailed` | Notes: @@ -260,7 +258,6 @@ Notes: - Cost properties are only used for statistics and accounting. - `cost1MCachedInTokens` defaults to the normal input token cost when not provided. - `cost1MReasoningOutTokens` defaults to `0` when not provided. -- Reasoning properties only apply to models that expose separate reasoning output. - `tokenEncoding` is primarily relevant for tokenizer implementations that support multiple encodings, such as `jTokkit`. ### Example Profile Files @@ -282,8 +279,6 @@ url=http://localhost:5001 apiKey= cost1MInTokens=0 cost1MOutTokens=0 -reasoningEffort=medium -reasoningSummaryDetails=auto ``` Local text completion: @@ -322,6 +317,52 @@ Tool calling is currently supported only for chat completion models. `TextComple These built-in providers are OpenAI-compatible today, but that is an implementation detail of the current client set, not a hard architectural limit of the library. Additional provider integrations can be added in the library itself, and applications can plug in their own client implementations where needed. +If you need to adapt request shaping for a specific OpenAI-compatible provider, you can optionally register an `OpenAiProviderDialect` bean. This allows applications to suppress request fields that a provider does not support while keeping `ModelProfile` provider-agnostic and runtime-driven. + +The default dialect behavior is: + +- `sendUsage(...) = true` +- `sendReasoning(...) = true` +- `sendMaxCompletionTokens(...) = true` +- `reasoningEffort(...) = "medium"` +- `reasoningSummaryDetails(...) = "concise"` + +Example: + +```java +@Bean +OpenAiProviderDialect azureOpenAiProviderDialect() { + return new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return false; + } + + @Override + public boolean sendReasoning(final ModelProfile modelProfile) { + return false; + } + + @Override + public boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { + return false; + } + + @Override + public String reasoningEffort(final ModelProfile modelProfile) { + return "medium"; + } + + @Override + public String reasoningSummaryDetails(final ModelProfile modelProfile) { + return "concise"; + } + }; +} +``` + +If no `OpenAiProviderDialect` bean is present, all OpenAI-compatible request features remain enabled by default. + For text completions, select a prompt strategy with the `prompts` property. lm-lib ships built-in strategies for common model families including Llama, GPT-OSS, Gemma, Mistral, Phi, Qwen, Vicuna, ChatML, Alpaca, and a no-formatting strategy. Example: diff --git a/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java b/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java new file mode 100644 index 0000000..16e042e --- /dev/null +++ b/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java @@ -0,0 +1,26 @@ +package de.extio.lmlib.client.oai; + +import de.extio.lmlib.profile.ModelProfile; + +public interface OpenAiProviderDialect { + + default boolean sendUsage(final ModelProfile modelProfile) { + return true; + } + + default boolean sendReasoning(final ModelProfile modelProfile) { + return true; + } + + default boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { + return true; + } + + default String reasoningEffort(final ModelProfile modelProfile) { + return "medium"; + } + + default String reasoningSummaryDetails(final ModelProfile modelProfile) { + return "concise"; + } +} diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java b/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java index ca999cf..51935bd 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java @@ -35,6 +35,7 @@ import de.extio.lmlib.client.Conversation; import de.extio.lmlib.client.ToolCallData; import de.extio.lmlib.client.oai.Model; import de.extio.lmlib.client.oai.ModelsResponse; +import de.extio.lmlib.client.oai.OpenAiProviderDialect; import de.extio.lmlib.profile.ModelCategory; import de.extio.lmlib.profile.ModelProfile; import de.extio.lmlib.profile.ModelProfile.ModelProvider; @@ -44,6 +45,9 @@ import de.extio.lmlib.token.TokenizerResolver; public abstract class AbstractCompletionClient implements Client, DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCompletionClient.class); + + private static final OpenAiProviderDialect DEFAULT_OPENAI_PROVIDER_DIALECT = new OpenAiProviderDialect() { + }; @Autowired @Qualifier("lmLibRestClientBuilder") @@ -54,6 +58,9 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean @Autowired protected ModelProfileService modelProfileService; + + @Autowired(required = false) + protected OpenAiProviderDialect openAiProviderDialect; @Value("${client.collectStatistics:false}") protected boolean collectStatistics; @@ -137,6 +144,30 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean protected abstract Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer chunkConsumer, final ToolCallData toolCallData); + protected OpenAiProviderDialect getOpenAiProviderDialect() { + return this.openAiProviderDialect == null ? DEFAULT_OPENAI_PROVIDER_DIALECT : this.openAiProviderDialect; + } + + protected boolean sendUsage(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().sendUsage(modelProfile); + } + + protected boolean sendReasoning(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().sendReasoning(modelProfile); + } + + protected boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().sendMaxCompletionTokens(modelProfile); + } + + protected String reasoningEffort(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().reasoningEffort(modelProfile); + } + + protected String reasoningSummaryDetails(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().reasoningSummaryDetails(modelProfile); + } + protected String getPrimaryModelName(final ModelProfile modelProfile) { return this.getModelNames(modelProfile, false).stream().findFirst().orElse(""); } diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java index ff9eba5..8bcbda7 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java @@ -48,45 +48,7 @@ public class ChatCompletionClient extends AbstractCompletionClient { @Override protected Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer chunkConsumer, final ToolCallData toolCallData) { final var chat = createChats(conversation, modelProfile); - - final var request = new ChatCompletionRequest(); - request.setModel(this.getPrimaryModelName(modelProfile)); - request.setMessages(chat); - request.setMaxTokens(modelProfile.maxTokens()); - request.setMaxCompletionTokens(modelProfile.maxTokens()); - request.setTemperature(modelProfile.temperature()); - request.setTopP(modelProfile.topP()); - request.setStream(chunkConsumer != null); - if (chunkConsumer != null) { - this.configureStreamOptions(true, request::setStreamOptions); - } else { - request.setUsage(true); - } - if (modelProfile.reasoningEffort() != null && !modelProfile.reasoningEffort().isBlank()) { - request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); - try { - request.getReasoning().setEffort(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.valueOf(modelProfile.reasoningEffort())); - } - catch (final IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid reasoning effort in the model profile: " + modelProfile.reasoningEffort(), e); - } - } - if (modelProfile.reasoningSummaryDetails() != null && !modelProfile.reasoningSummaryDetails().isBlank()) { - if (request.getReasoning() == null) { - request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); - } - try { - request.getReasoning().setSummary(ChatCompletionRequest.ChatCompletionsRequestReasoningSummaryDetails.valueOf(modelProfile.reasoningSummaryDetails()).name()); - } - catch (final IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid reasoning summary details in the model profile: " + modelProfile.reasoningSummaryDetails(), e); - } - } - if (toolCallData != null && toolCallData.hasTools()) { - request.setTools(this.createTools(toolCallData.tools())); - request.setToolChoice(this.createToolChoice(toolCallData)); - request.setParallelToolCalls(toolCallData.parallelToolCalls()); - } + final var request = this.createRequest(chat, modelProfile, chunkConsumer != null, toolCallData); String requestBody; try { @@ -203,6 +165,53 @@ public class ChatCompletionClient extends AbstractCompletionClient { return new Completion(content, reasoning, finishReason, statistics, toolCalls); } + ChatCompletionRequest createRequest(final List chat, final ModelProfile modelProfile, final boolean streaming, final ToolCallData toolCallData) { + final var request = new ChatCompletionRequest(); + request.setModel(this.getPrimaryModelName(modelProfile)); + request.setMessages(chat); + request.setMaxTokens(modelProfile.maxTokens()); + if (this.sendMaxCompletionTokens(modelProfile)) { + request.setMaxCompletionTokens(modelProfile.maxTokens()); + } + request.setTemperature(modelProfile.temperature()); + request.setTopP(modelProfile.topP()); + request.setStream(streaming); + if (streaming) { + this.configureStreamOptions(true, request::setStreamOptions); + } + else if (this.sendUsage(modelProfile)) { + request.setUsage(true); + } + final var reasoningEffort = this.reasoningEffort(modelProfile); + if (this.sendReasoning(modelProfile) && reasoningEffort != null && !reasoningEffort.isBlank()) { + request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); + try { + request.getReasoning().setEffort(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.valueOf(reasoningEffort)); + } + catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid reasoning effort for the current OAI provider dialect: " + reasoningEffort, e); + } + } + final var reasoningSummaryDetails = this.reasoningSummaryDetails(modelProfile); + if (this.sendReasoning(modelProfile) && reasoningSummaryDetails != null && !reasoningSummaryDetails.isBlank()) { + if (request.getReasoning() == null) { + request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); + } + try { + request.getReasoning().setSummary(ChatCompletionRequest.ChatCompletionsRequestReasoningSummaryDetails.valueOf(reasoningSummaryDetails).name()); + } + catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid reasoning summary details for the current OAI provider dialect: " + reasoningSummaryDetails, e); + } + } + if (toolCallData != null && toolCallData.hasTools()) { + request.setTools(this.createTools(toolCallData.tools())); + request.setToolChoice(this.createToolChoice(toolCallData)); + request.setParallelToolCalls(toolCallData.parallelToolCalls()); + } + return request; + } + private List createTools(final List toolDefinitions) { return toolDefinitions.stream().map(toolDefinition -> { final var function = new ChatCompletionRequest.ChatCompletionFunction(); diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClient.java b/src/main/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClient.java index 44ed82a..4e18b02 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClient.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClient.java @@ -53,19 +53,7 @@ public class TextCompletionClient extends AbstractCompletionClient { } final var prompt = this.createPrompt(conversation, modelProfile, promptStrategy); - - final var request = new CompletionRequest(); - request.setModel(this.getPrimaryModelName(modelProfile)); - request.setPrompt(prompt); - request.setMaxTokens(modelProfile.maxTokens()); - request.setTemperature(modelProfile.temperature()); - request.setTopP(modelProfile.topP()); - request.setStream(chunkConsumer != null); - if (chunkConsumer != null) { - this.configureStreamOptions(true, request::setStreamOptions); - } else { - request.setUsage(true); - } + final var request = this.createRequest(prompt, modelProfile, chunkConsumer != null); String requestBody; try { @@ -196,6 +184,23 @@ public class TextCompletionClient extends AbstractCompletionClient { final var statistics = createCompletionStatistics(modelProfile, start, response.getUsage(), response.getTimings(), prompt, content, reasoning); return new Completion(content, reasoning, finishReason, statistics); } + + CompletionRequest createRequest(final String prompt, final ModelProfile modelProfile, final boolean streaming) { + final var request = new CompletionRequest(); + request.setModel(this.getPrimaryModelName(modelProfile)); + request.setPrompt(prompt); + request.setMaxTokens(modelProfile.maxTokens()); + request.setTemperature(modelProfile.temperature()); + request.setTopP(modelProfile.topP()); + request.setStream(streaming); + if (streaming) { + this.configureStreamOptions(true, request::setStreamOptions); + } + else if (this.sendUsage(modelProfile)) { + request.setUsage(true); + } + return request; + } private String createPrompt(final Conversation conversation, final ModelProfile modelProfile, final PromptStrategy promptStrategy) { StringBuilder prompt = this.formatConversation(conversation, promptStrategy); diff --git a/src/main/java/de/extio/lmlib/profile/ModelProfile.java b/src/main/java/de/extio/lmlib/profile/ModelProfile.java index ea31fc8..27b37e5 100755 --- a/src/main/java/de/extio/lmlib/profile/ModelProfile.java +++ b/src/main/java/de/extio/lmlib/profile/ModelProfile.java @@ -18,8 +18,6 @@ public record ModelProfile( BigDecimal costPerCachedInToken, BigDecimal costPerOutToken, BigDecimal costPerReasoningOutToken, - String reasoningEffort, - String reasoningSummaryDetails, String category) { public ModelProfile { diff --git a/src/main/java/de/extio/lmlib/profile/ModelProfileService.java b/src/main/java/de/extio/lmlib/profile/ModelProfileService.java index d44b2ad..d8027fa 100755 --- a/src/main/java/de/extio/lmlib/profile/ModelProfileService.java +++ b/src/main/java/de/extio/lmlib/profile/ModelProfileService.java @@ -90,12 +90,6 @@ public class ModelProfileService { .map(Object::toString) .map(value -> new BigDecimal(value).divide(new BigDecimal(1000000.0))) .orElse(BigDecimal.ZERO); - final var reasoningEffort = Optional.ofNullable(resource.getProperty("reasoningEffort")) - .map(Object::toString) - .orElse(null); - final var reasoningSummaryDetails = Optional.ofNullable(resource.getProperty("reasoningSummaryDetails")) - .map(Object::toString) - .orElse(null); final var category = Optional .ofNullable(modelCategory) .map(ModelCategory::getShortName) @@ -112,7 +106,7 @@ public class ModelProfileService { .orElse(null); } - return new ModelProfile(promptTemplate, tokenizer, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costPerInToken, costPerCachedInToken, costPerOutToken, costPerReasoningOutToken, reasoningEffort, reasoningSummaryDetails, category); + return new ModelProfile(promptTemplate, tokenizer, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costPerInToken, costPerCachedInToken, costPerOutToken, costPerReasoningOutToken, category); } catch (final IOException e) { LOGGER.error("Error while reading model profile", e); diff --git a/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java b/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java new file mode 100644 index 0000000..0f19b19 --- /dev/null +++ b/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java @@ -0,0 +1,121 @@ +package de.extio.lmlib.client.oai.completion.chat; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import de.extio.lmlib.client.oai.OpenAiProviderDialect; +import de.extio.lmlib.profile.ModelProfile; +import de.extio.lmlib.profile.ModelProfile.ModelProvider; + +class ChatCompletionClientRequestTest { + + @Test + void defaultsToAllRequestFieldsWhenNoDialectIsConfigured() { + final var client = new TestChatCompletionClient(); + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertTrue(request.getUsage()); + assertNotNull(request.getReasoning()); + assertEquals(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.medium, request.getReasoning().getEffort()); + assertEquals("concise", request.getReasoning().getSummary()); + assertEquals(2048, request.getMaxCompletionTokens()); + } + + @Test + void omitsDialectDisabledRequestFields() { + final var client = new TestChatCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return false; + } + + @Override + public boolean sendReasoning(final ModelProfile modelProfile) { + return false; + } + + @Override + public boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { + return false; + } + }); + + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertNull(request.getUsage()); + assertNull(request.getReasoning()); + assertNull(request.getMaxCompletionTokens()); + assertEquals(2048, request.getMaxTokens()); + assertFalse(request.isStream()); + } + + @Test + void usesDialectProvidedReasoningDefaults() { + final var client = new TestChatCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public String reasoningEffort(final ModelProfile modelProfile) { + return "high"; + } + + @Override + public String reasoningSummaryDetails(final ModelProfile modelProfile) { + return "detailed"; + } + }); + + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertNotNull(request.getReasoning()); + assertEquals(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.high, request.getReasoning().getEffort()); + assertEquals("detailed", request.getReasoning().getSummary()); + } + + private ModelProfile createProfile() { + return new ModelProfile( + "", + "fallback", + "cl100k_base", + 2048, + 8192, + 0.4, + 1.0, + ModelProvider.OAI_CHAT_COMPLETION, + "test-model", + "http://localhost:5001", + "", + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + "TEST"); + } + + private static final class TestChatCompletionClient extends ChatCompletionClient { + + private void setDialect(final OpenAiProviderDialect openAiProviderDialect) { + this.openAiProviderDialect = openAiProviderDialect; + } + } +} diff --git a/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java b/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java new file mode 100644 index 0000000..cdf8299 --- /dev/null +++ b/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java @@ -0,0 +1,65 @@ +package de.extio.lmlib.client.oai.completion.text; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import de.extio.lmlib.client.oai.OpenAiProviderDialect; +import de.extio.lmlib.profile.ModelProfile; +import de.extio.lmlib.profile.ModelProfile.ModelProvider; + +class TextCompletionClientRequestTest { + + @Test + void defaultsToUsageWhenNoDialectIsConfigured() { + final var client = new TestTextCompletionClient(); + final var request = client.createRequest("prompt", this.createProfile(), false); + + assertTrue(request.getUsage()); + } + + @Test + void omitsUsageWhenDialectDisablesIt() { + final var client = new TestTextCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return false; + } + }); + + final var request = client.createRequest("prompt", this.createProfile(), false); + + assertNull(request.getUsage()); + } + + private ModelProfile createProfile() { + return new ModelProfile( + "gpt-oss", + "fallback", + "cl100k_base", + 2048, + 8192, + 0.4, + 1.0, + ModelProvider.OAI_TEXT_COMPLETION, + "test-model", + "http://localhost:5001", + "", + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + "TEST"); + } + + private static final class TestTextCompletionClient extends TextCompletionClient { + + private void setDialect(final OpenAiProviderDialect openAiProviderDialect) { + this.openAiProviderDialect = openAiProviderDialect; + } + } +} diff --git a/src/test/java/de/extio/lmlib/token/TokenizerResolverTest.java b/src/test/java/de/extio/lmlib/token/TokenizerResolverTest.java index a72c933..469923f 100644 --- a/src/test/java/de/extio/lmlib/token/TokenizerResolverTest.java +++ b/src/test/java/de/extio/lmlib/token/TokenizerResolverTest.java @@ -55,8 +55,6 @@ public class TokenizerResolverTest { BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, - null, - null, "TEST"); } diff --git a/src/test/resources/local-chatcompletion.properties b/src/test/resources/local-chatcompletion.properties index 608ecfc..2ada5d7 100755 --- a/src/test/resources/local-chatcompletion.properties +++ b/src/test/resources/local-chatcompletion.properties @@ -13,6 +13,4 @@ apiKey= cost1MInTokens=0 cost1MCachedInTokens=0 cost1MOutTokens=0 -cost1MReasoningOutTokens=0 -reasoningEffort=medium -reasoningSummaryDetails=auto \ No newline at end of file +cost1MReasoningOutTokens=0 \ No newline at end of file From 33f76defa5fe7ccfaa9ae4f7c84022a7dbbe729e Mon Sep 17 00:00:00 2001 From: Stephan Birkl Date: Thu, 7 May 2026 18:22:12 +0200 Subject: [PATCH 2/4] feat: Enhance OpenAiProviderDialect for improved request handling and update related classes - Refactored OpenAiProviderDialect to include methods for managing chat token limits and reasoning effort. - Updated ChatCompletionClient and AbstractCompletionClient to utilize the new dialect methods for request shaping. - Modified ChatCompletionRequest to replace reasoning properties with a single reasoningEffort field. - Adjusted tests to reflect changes in request handling and dialect behavior. - Updated README to clarify the new request shaping capabilities and defaults. --- README.md | 37 ++++---- .../client/oai/OpenAiProviderDialect.java | 89 +++++++++++++++++-- .../completion/AbstractCompletionClient.java | 10 +-- .../completion/chat/ChatCompletionClient.java | 48 +++++----- .../chat/ChatCompletionRequest.java | 47 ++-------- .../chat/ChatCompletionClientRequestTest.java | 64 +++++++------ .../text/TextCompletionClientRequestTest.java | 10 +-- 7 files changed, 177 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index d505d79..1ebcd98 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,6 @@ final var modelProfile = new ModelProfile( new BigDecimal("0.125").divide(new BigDecimal(1000000)), new BigDecimal("10.0").divide(new BigDecimal(1000000)), BigDecimal.ZERO, - null, - null, "DYNAMIC" ); @@ -317,25 +315,31 @@ Tool calling is currently supported only for chat completion models. `TextComple These built-in providers are OpenAI-compatible today, but that is an implementation detail of the current client set, not a hard architectural limit of the library. Additional provider integrations can be added in the library itself, and applications can plug in their own client implementations where needed. -If you need to adapt request shaping for a specific OpenAI-compatible provider, you can optionally register an `OpenAiProviderDialect` bean. This allows applications to suppress request fields that a provider does not support while keeping `ModelProfile` provider-agnostic and runtime-driven. +If you need to adapt request shaping for a specific OpenAI-compatible provider, you can optionally register an `OpenAiProviderDialect` bean. This allows applications to suppress or switch request fields that a provider does not support while keeping `ModelProfile` provider-agnostic and runtime-driven. The default dialect behavior is: -- `sendUsage(...) = true` +- `sendUsage(...) = false` - `sendReasoning(...) = true` -- `sendMaxCompletionTokens(...) = true` -- `reasoningEffort(...) = "medium"` -- `reasoningSummaryDetails(...) = "concise"` +- `chatTokenLimitParameterMode(...) = ChatTokenLimitParameterMode.MAX_COMPLETION_TOKENS` +- `reasoningEffort(...) = ReasoningEffort.MEDIUM` + +Notes: + +- `usage` is a llama-server style extension and is therefore disabled unless a dialect explicitly enables it. +- `reasoning_effort` is sent as a top-level request field when reasoning is enabled. +- `max_completion_tokens` is the default because it matches the current OpenAI recommendation. +- Some older OpenAI-compatible servers still require `max_tokens`, which can be selected through the dialect. Example: ```java @Bean -OpenAiProviderDialect azureOpenAiProviderDialect() { +OpenAiProviderDialect legacyOpenAiProviderDialect() { return new OpenAiProviderDialect() { @Override public boolean sendUsage(final ModelProfile modelProfile) { - return false; + return true; } @Override @@ -344,24 +348,19 @@ OpenAiProviderDialect azureOpenAiProviderDialect() { } @Override - public boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { - return false; + public ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.MAX_TOKENS; } @Override - public String reasoningEffort(final ModelProfile modelProfile) { - return "medium"; - } - - @Override - public String reasoningSummaryDetails(final ModelProfile modelProfile) { - return "concise"; + public ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return ReasoningEffort.MEDIUM; } }; } ``` -If no `OpenAiProviderDialect` bean is present, all OpenAI-compatible request features remain enabled by default. +If no `OpenAiProviderDialect` bean is present, lm-lib follows the official-oriented defaults above, with `usage` left disabled unless you opt in. For text completions, select a prompt strategy with the `prompts` property. lm-lib ships built-in strategies for common model families including Llama, GPT-OSS, Gemma, Mistral, Phi, Qwen, Vicuna, ChatML, Alpaca, and a no-formatting strategy. diff --git a/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java b/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java index 16e042e..18930cf 100644 --- a/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java +++ b/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java @@ -1,26 +1,101 @@ package de.extio.lmlib.client.oai; +import com.fasterxml.jackson.annotation.JsonValue; + import de.extio.lmlib.profile.ModelProfile; +/** + * Optional dialect hook for OpenAI-compatible clients. + *

+ * Applications can register this as a Spring bean to adapt request shaping for + * providers that are not fully aligned with the current OpenAI Chat + * Completions API surface. + *

+ * If no bean is present, lm-lib uses the defaults defined on this interface. + */ public interface OpenAiProviderDialect { + /** + * Whether lm-lib should send the top-level {@code usage} request flag. + *

+ * This is disabled by default because it is not part of the official OpenAI + * Chat Completions request schema and is mainly useful for llama-server style + * extensions. + * + * @param modelProfile the resolved model profile + * @return {@code true} if the request should include the top-level + * {@code usage} flag + */ default boolean sendUsage(final ModelProfile modelProfile) { - return true; + return false; } + /** + * Whether lm-lib should send a top-level {@code reasoning_effort} request + * field. + * + * @param modelProfile the resolved model profile + * @return {@code true} if {@code reasoning_effort} should be sent + */ default boolean sendReasoning(final ModelProfile modelProfile) { return true; } - default boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { - return true; + /** + * Selects which token limit request parameter should be sent for chat + * completions. + *

+ * {@code max_completion_tokens} is the current OpenAI recommendation, while + * older llama-server style implementations may still require + * {@code max_tokens}. + * + * @param modelProfile the resolved model profile + * @return the token limit parameter mode to use + */ + default ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.MAX_COMPLETION_TOKENS; } - default String reasoningEffort(final ModelProfile modelProfile) { - return "medium"; + /** + * Returns the reasoning effort to send for chat completions when reasoning is + * enabled. + * + * @param modelProfile the resolved model profile + * @return the reasoning effort value to send + */ + default ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return ReasoningEffort.MEDIUM; } - default String reasoningSummaryDetails(final ModelProfile modelProfile) { - return "concise"; + /** + * Controls which chat token limit field lm-lib should send. + */ + enum ChatTokenLimitParameterMode { + MAX_COMPLETION_TOKENS, + MAX_TOKENS, + BOTH + } + + /** + * Official OpenAI reasoning effort values for chat completions. + */ + enum ReasoningEffort { + NONE("none"), + MINIMAL("minimal"), + LOW("low"), + MEDIUM("medium"), + HIGH("high"), + XHIGH("xhigh"); + + private final String wireValue; + + ReasoningEffort(final String wireValue) { + this.wireValue = wireValue; + } + + @JsonValue + public String getWireValue() { + return this.wireValue; + } } } diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java b/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java index 51935bd..d2f7765 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/AbstractCompletionClient.java @@ -156,18 +156,14 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean return this.getOpenAiProviderDialect().sendReasoning(modelProfile); } - protected boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { - return this.getOpenAiProviderDialect().sendMaxCompletionTokens(modelProfile); + protected OpenAiProviderDialect.ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().chatTokenLimitParameterMode(modelProfile); } - protected String reasoningEffort(final ModelProfile modelProfile) { + protected OpenAiProviderDialect.ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { return this.getOpenAiProviderDialect().reasoningEffort(modelProfile); } - protected String reasoningSummaryDetails(final ModelProfile modelProfile) { - return this.getOpenAiProviderDialect().reasoningSummaryDetails(modelProfile); - } - protected String getPrimaryModelName(final ModelProfile modelProfile) { return this.getModelNames(modelProfile, false).stream().findFirst().orElse(""); } diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java index 8bcbda7..8295814 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClient.java @@ -169,10 +169,7 @@ public class ChatCompletionClient extends AbstractCompletionClient { final var request = new ChatCompletionRequest(); request.setModel(this.getPrimaryModelName(modelProfile)); request.setMessages(chat); - request.setMaxTokens(modelProfile.maxTokens()); - if (this.sendMaxCompletionTokens(modelProfile)) { - request.setMaxCompletionTokens(modelProfile.maxTokens()); - } + this.applyChatTokenLimitParameterMode(request, modelProfile); request.setTemperature(modelProfile.temperature()); request.setTopP(modelProfile.topP()); request.setStream(streaming); @@ -183,26 +180,8 @@ public class ChatCompletionClient extends AbstractCompletionClient { request.setUsage(true); } final var reasoningEffort = this.reasoningEffort(modelProfile); - if (this.sendReasoning(modelProfile) && reasoningEffort != null && !reasoningEffort.isBlank()) { - request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); - try { - request.getReasoning().setEffort(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.valueOf(reasoningEffort)); - } - catch (final IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid reasoning effort for the current OAI provider dialect: " + reasoningEffort, e); - } - } - final var reasoningSummaryDetails = this.reasoningSummaryDetails(modelProfile); - if (this.sendReasoning(modelProfile) && reasoningSummaryDetails != null && !reasoningSummaryDetails.isBlank()) { - if (request.getReasoning() == null) { - request.setReasoning(new ChatCompletionRequest.ChatCompletionsRequestReasoning()); - } - try { - request.getReasoning().setSummary(ChatCompletionRequest.ChatCompletionsRequestReasoningSummaryDetails.valueOf(reasoningSummaryDetails).name()); - } - catch (final IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid reasoning summary details for the current OAI provider dialect: " + reasoningSummaryDetails, e); - } + if (this.sendReasoning(modelProfile) && reasoningEffort != null) { + request.setReasoningEffort(reasoningEffort); } if (toolCallData != null && toolCallData.hasTools()) { request.setTools(this.createTools(toolCallData.tools())); @@ -212,6 +191,27 @@ public class ChatCompletionClient extends AbstractCompletionClient { return request; } + private void applyChatTokenLimitParameterMode(final ChatCompletionRequest request, final ModelProfile modelProfile) { + final var mode = this.chatTokenLimitParameterMode(modelProfile); + if (mode == null) { + throw new IllegalArgumentException("Chat token limit parameter mode must not be null"); + } + switch (mode) { + case MAX_COMPLETION_TOKENS: + request.setMaxCompletionTokens(modelProfile.maxTokens()); + break; + case MAX_TOKENS: + request.setMaxTokens(modelProfile.maxTokens()); + break; + case BOTH: + request.setMaxCompletionTokens(modelProfile.maxTokens()); + request.setMaxTokens(modelProfile.maxTokens()); + break; + default: + throw new IllegalArgumentException("Unsupported chat token limit parameter mode: " + mode); + } + } + private List createTools(final List toolDefinitions) { return toolDefinitions.stream().map(toolDefinition -> { final var function = new ChatCompletionRequest.ChatCompletionFunction(); diff --git a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionRequest.java b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionRequest.java index aa0437e..6187097 100755 --- a/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionRequest.java +++ b/src/main/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionRequest.java @@ -5,6 +5,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.*; +import de.extio.lmlib.client.oai.OpenAiProviderDialect.ReasoningEffort; import de.extio.lmlib.client.oai.completion.AbstractCompletionRequest; final class ChatCompletionRequest extends AbstractCompletionRequest { @@ -13,7 +14,7 @@ final class ChatCompletionRequest extends AbstractCompletionRequest { private Integer maxCompletionTokens; - private ChatCompletionsRequestReasoning reasoning; + private ReasoningEffort reasoningEffort; private List tools; @@ -39,13 +40,13 @@ final class ChatCompletionRequest extends AbstractCompletionRequest { this.maxCompletionTokens = maxCompletionTokens; } - @JsonProperty("reasoning") - public ChatCompletionsRequestReasoning getReasoning() { - return this.reasoning; + @JsonProperty("reasoning_effort") + public ReasoningEffort getReasoningEffort() { + return this.reasoningEffort; } - public void setReasoning(final ChatCompletionsRequestReasoning reasoning) { - this.reasoning = reasoning; + public void setReasoningEffort(final ReasoningEffort reasoningEffort) { + this.reasoningEffort = reasoningEffort; } @JsonProperty("tools") @@ -75,40 +76,6 @@ final class ChatCompletionRequest extends AbstractCompletionRequest { this.parallelToolCalls = parallelToolCalls; } - final static class ChatCompletionsRequestReasoning { - - private ChatCompletionsRequestReasoningEffort effort; - - private String summary; - - @JsonProperty("effort") - public ChatCompletionsRequestReasoningEffort getEffort() { - return this.effort; - } - - public void setEffort(final ChatCompletionsRequestReasoningEffort effort) { - this.effort = effort; - } - - @JsonProperty("summary") - public String getSummary() { - return this.summary; - } - - public void setSummary(final String summary) { - this.summary = summary; - } - - } - - static enum ChatCompletionsRequestReasoningEffort { - minimal, low, medium, high - } - - static enum ChatCompletionsRequestReasoningSummaryDetails { - auto, concise, detailed - } - static final class ChatCompletionTool { private String type; diff --git a/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java b/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java index 0f19b19..f9edfd5 100644 --- a/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java +++ b/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java @@ -2,7 +2,6 @@ package de.extio.lmlib.client.oai.completion.chat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -18,7 +17,7 @@ import de.extio.lmlib.profile.ModelProfile.ModelProvider; class ChatCompletionClientRequestTest { @Test - void defaultsToAllRequestFieldsWhenNoDialectIsConfigured() { + void defaultsToOfficialRequestFieldsWhenNoDialectIsConfigured() { final var client = new TestChatCompletionClient(); final var request = client.createRequest( List.of(new ChatMessage("user", "hello", null)), @@ -26,30 +25,24 @@ class ChatCompletionClientRequestTest { false, null); - assertTrue(request.getUsage()); - assertNotNull(request.getReasoning()); - assertEquals(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.medium, request.getReasoning().getEffort()); - assertEquals("concise", request.getReasoning().getSummary()); + assertNull(request.getUsage()); + assertEquals(OpenAiProviderDialect.ReasoningEffort.MEDIUM, request.getReasoningEffort()); assertEquals(2048, request.getMaxCompletionTokens()); + assertNull(request.getMaxTokens()); } @Test - void omitsDialectDisabledRequestFields() { + void omitsDialectDisabledReasoningAndUsesLegacyMaxTokens() { final var client = new TestChatCompletionClient(); client.setDialect(new OpenAiProviderDialect() { - @Override - public boolean sendUsage(final ModelProfile modelProfile) { - return false; - } - @Override public boolean sendReasoning(final ModelProfile modelProfile) { return false; } @Override - public boolean sendMaxCompletionTokens(final ModelProfile modelProfile) { - return false; + public ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.MAX_TOKENS; } }); @@ -60,24 +53,19 @@ class ChatCompletionClientRequestTest { null); assertNull(request.getUsage()); - assertNull(request.getReasoning()); + assertNull(request.getReasoningEffort()); assertNull(request.getMaxCompletionTokens()); assertEquals(2048, request.getMaxTokens()); assertFalse(request.isStream()); } @Test - void usesDialectProvidedReasoningDefaults() { + void usesDialectProvidedReasoningEffort() { final var client = new TestChatCompletionClient(); client.setDialect(new OpenAiProviderDialect() { @Override - public String reasoningEffort(final ModelProfile modelProfile) { - return "high"; - } - - @Override - public String reasoningSummaryDetails(final ModelProfile modelProfile) { - return "detailed"; + public ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return ReasoningEffort.HIGH; } }); @@ -87,9 +75,33 @@ class ChatCompletionClientRequestTest { false, null); - assertNotNull(request.getReasoning()); - assertEquals(ChatCompletionRequest.ChatCompletionsRequestReasoningEffort.high, request.getReasoning().getEffort()); - assertEquals("detailed", request.getReasoning().getSummary()); + assertEquals(OpenAiProviderDialect.ReasoningEffort.HIGH, request.getReasoningEffort()); + } + + @Test + void canEnableUsageAndSendBothTokenLimitFields() { + final var client = new TestChatCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return true; + } + + @Override + public ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.BOTH; + } + }); + + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertTrue(request.getUsage()); + assertEquals(2048, request.getMaxCompletionTokens()); + assertEquals(2048, request.getMaxTokens()); } private ModelProfile createProfile() { diff --git a/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java b/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java index cdf8299..3430078 100644 --- a/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java +++ b/src/test/java/de/extio/lmlib/client/oai/completion/text/TextCompletionClientRequestTest.java @@ -14,26 +14,26 @@ import de.extio.lmlib.profile.ModelProfile.ModelProvider; class TextCompletionClientRequestTest { @Test - void defaultsToUsageWhenNoDialectIsConfigured() { + void omitsUsageWhenNoDialectIsConfigured() { final var client = new TestTextCompletionClient(); final var request = client.createRequest("prompt", this.createProfile(), false); - assertTrue(request.getUsage()); + assertNull(request.getUsage()); } @Test - void omitsUsageWhenDialectDisablesIt() { + void includesUsageWhenDialectEnablesIt() { final var client = new TestTextCompletionClient(); client.setDialect(new OpenAiProviderDialect() { @Override public boolean sendUsage(final ModelProfile modelProfile) { - return false; + return true; } }); final var request = client.createRequest("prompt", this.createProfile(), false); - assertNull(request.getUsage()); + assertTrue(request.getUsage()); } private ModelProfile createProfile() { From 12b9d2aabaf15b4d32a961f5c5c86ab2e594aee1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:35:46 +0000 Subject: [PATCH 3/4] chore: release 2.7.0 --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ebcd98..ba6590b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The published artifacts are hosted on Maven Central, so no additional repository de.extio lm-lib - 2.6.0 + 2.7.0 ``` diff --git a/pom.xml b/pom.xml index aa97a6c..7b822e6 100755 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ lm-lib Modular Java library for LLM-powered applications, model clients, and agentic orchestration. https://github.com/extio-de/lm-lib - 2.6.0 + 2.7.0 From b741b5f016bfcdb0ef3d6362d16a28a7fdd8e24e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:42:08 +0000 Subject: [PATCH 4/4] chore: release 2.8.0 --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba6590b..482230c 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The published artifacts are hosted on Maven Central, so no additional repository de.extio lm-lib - 2.7.0 + 2.8.0 ``` diff --git a/pom.xml b/pom.xml index 7b822e6..e161e03 100755 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ lm-lib Modular Java library for LLM-powered applications, model clients, and agentic orchestration. https://github.com/extio-de/lm-lib - 2.7.0 + 2.8.0