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.
This commit is contained in:
Stephan Birkl
2026-05-07 17:57:59 +02:00
parent 6235c49358
commit 244aa2574b
11 changed files with 357 additions and 71 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -55,8 +55,6 @@ public class TokenizerResolverTest {
BigDecimal.ZERO,
BigDecimal.ZERO,
BigDecimal.ZERO,
null,
null,
"TEST");
}

View File

@@ -13,6 +13,4 @@ apiKey=
cost1MInTokens=0
cost1MCachedInTokens=0
cost1MOutTokens=0
cost1MReasoningOutTokens=0
reasoningEffort=medium
reasoningSummaryDetails=auto
cost1MReasoningOutTokens=0