diff --git a/README.md b/README.md index d98833c..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" ); @@ -250,8 +248,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 +256,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 +277,6 @@ url=http://localhost:5001 apiKey= cost1MInTokens=0 cost1MOutTokens=0 -reasoningEffort=medium -reasoningSummaryDetails=auto ``` Local text completion: @@ -322,6 +315,53 @@ 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 or switch request fields that a provider does not support while keeping `ModelProfile` provider-agnostic and runtime-driven. + +The default dialect behavior is: + +- `sendUsage(...) = false` +- `sendReasoning(...) = true` +- `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 legacyOpenAiProviderDialect() { + return new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return true; + } + + @Override + public boolean sendReasoning(final ModelProfile modelProfile) { + return false; + } + + @Override + public ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.MAX_TOKENS; + } + + @Override + public ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return ReasoningEffort.MEDIUM; + } + }; +} +``` + +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. 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..18930cf --- /dev/null +++ b/src/main/java/de/extio/lmlib/client/oai/OpenAiProviderDialect.java @@ -0,0 +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 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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 ca999cf..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 @@ -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,26 @@ 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 OpenAiProviderDialect.ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().chatTokenLimitParameterMode(modelProfile); + } + + protected OpenAiProviderDialect.ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return this.getOpenAiProviderDialect().reasoningEffort(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..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 @@ -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); + this.applyChatTokenLimitParameterMode(request, modelProfile); + 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) { + request.setReasoningEffort(reasoningEffort); + } + if (toolCallData != null && toolCallData.hasTools()) { + request.setTools(this.createTools(toolCallData.tools())); + request.setToolChoice(this.createToolChoice(toolCallData)); + request.setParallelToolCalls(toolCallData.parallelToolCalls()); + } + 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/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..f9edfd5 --- /dev/null +++ b/src/test/java/de/extio/lmlib/client/oai/completion/chat/ChatCompletionClientRequestTest.java @@ -0,0 +1,133 @@ +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.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 defaultsToOfficialRequestFieldsWhenNoDialectIsConfigured() { + final var client = new TestChatCompletionClient(); + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertNull(request.getUsage()); + assertEquals(OpenAiProviderDialect.ReasoningEffort.MEDIUM, request.getReasoningEffort()); + assertEquals(2048, request.getMaxCompletionTokens()); + assertNull(request.getMaxTokens()); + } + + @Test + void omitsDialectDisabledReasoningAndUsesLegacyMaxTokens() { + final var client = new TestChatCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public boolean sendReasoning(final ModelProfile modelProfile) { + return false; + } + + @Override + public ChatTokenLimitParameterMode chatTokenLimitParameterMode(final ModelProfile modelProfile) { + return ChatTokenLimitParameterMode.MAX_TOKENS; + } + }); + + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + assertNull(request.getUsage()); + assertNull(request.getReasoningEffort()); + assertNull(request.getMaxCompletionTokens()); + assertEquals(2048, request.getMaxTokens()); + assertFalse(request.isStream()); + } + + @Test + void usesDialectProvidedReasoningEffort() { + final var client = new TestChatCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public ReasoningEffort reasoningEffort(final ModelProfile modelProfile) { + return ReasoningEffort.HIGH; + } + }); + + final var request = client.createRequest( + List.of(new ChatMessage("user", "hello", null)), + this.createProfile(), + false, + null); + + 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() { + 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..3430078 --- /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 omitsUsageWhenNoDialectIsConfigured() { + final var client = new TestTextCompletionClient(); + final var request = client.createRequest("prompt", this.createProfile(), false); + + assertNull(request.getUsage()); + } + + @Test + void includesUsageWhenDialectEnablesIt() { + final var client = new TestTextCompletionClient(); + client.setDialect(new OpenAiProviderDialect() { + @Override + public boolean sendUsage(final ModelProfile modelProfile) { + return true; + } + }); + + final var request = client.createRequest("prompt", this.createProfile(), false); + + assertTrue(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