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.
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* {@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
@@ -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<ChatCompletionRequest.ChatCompletionTool> createTools(final List<ToolDefinition> toolDefinitions) {
|
||||
return toolDefinitions.stream().map(toolDefinition -> {
|
||||
final var function = new ChatCompletionRequest.ChatCompletionFunction();
|
||||
|
||||
@@ -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<ChatCompletionTool> 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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user