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