feat: Introduce reasoning capabilities in chat and text completion

- Added AbstractCompletionRequest and AbstractCompletionResponse classes to standardize request and response structures.
- Implemented StreamOptions for streaming responses in chat completions.
- Enhanced ChatCompletionClient and TextCompletionClient to support reasoning content in responses.
- Updated ModelProfile to include reasoning effort and summary details.
- Created GptOssPromptStrategy to handle specific prompt formatting and reasoning extraction.
- Modified existing tests to validate reasoning content in completions.
- Updated configuration properties for reasoning settings in local properties files.
This commit is contained in:
extio-de
2025-10-01 19:05:15 +02:00
parent 34c96f0147
commit 514dad1c7f
32 changed files with 582 additions and 392 deletions

View File

@@ -29,7 +29,7 @@ import de.extio.lmlib.profile.ModelCategory;
import io.netty.util.internal.ThreadLocalRandom;
/**
* These tests have been verified with Llama 3.1 8B and Gemma2 27B models
* These tests have been verified with Llama 3.1 8B, Gemma2 27B, Gemma3 27B and GPT-OSS models
*/
@Disabled("This test requires a running Llama server or an Azure subscription (setup azure key in model profile)")
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@@ -137,11 +137,14 @@ public class AgentTest {
null,
null,
null,
context -> {
if ("summary".equals(context.getStringValue(TextAgentResponseHandler.UPDATE_KEY))) { // UPDATE_KEY helps you to identify the streamed chunk
System.out.print(context.getStringValue("summary_chunk")); // Single chunks can be accessed via _chunk suffix, depends on the StreamedAgentResponseHandler implementation
lastStreamedSummary.set(context.getStringValue("summary")); // The current key value can also be accessed (e.g. concatenated, depends on the StreamedAgentResponseHandler implementation)
}
(context, chunk) -> {
if (chunk.content() != null) { // Since chunk can contain both content and reasoning content, we check for null
context.setStringValue("summary_chunk", chunk.content());
}
if (chunk.reasoningContent() != null) {
// System.out.print(chunk.reasoningContent()); // The reasoning can also be accessed if available
}
lastStreamedSummary.set(context.getStringValue("summary")); // The current key value can also be accessed (e.g. concatenated, depends on the StreamedAgentResponseHandler implementation)
}
),
null,
@@ -157,6 +160,7 @@ public class AgentTest {
final var resultContexts = this.agentExecutor.walk(agents.get("CodeSummarizer"), context, contextUpdate -> {
// This is an optional consumer that can be used to receive updates from the agent context
System.out.print(contextUpdate.getStringValue("summary_chunk"));
});
assertEquals(1, resultContexts.size());

View File

@@ -1,6 +1,7 @@
package de.extio.lmlib.client.oai;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
@@ -45,10 +46,11 @@ public class CompletionClientTest {
@Test
void completion() throws Exception {
final var completion = this.textCompletionClient.completion(
null,
ModelCategory.SMALL,
"You are a helpful assistant",
"Say the color is green");
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
assertTrue(Grader.assessScoreBinary("Does the text mention the color green?", completion.response(), this.chatCompletionclient));
@@ -61,6 +63,7 @@ public class CompletionClientTest {
conversation.addTurn(new Turn(TurnType.USER, "You choose!"));
final var completion = this.textCompletionClient.conversation(ModelCategory.SMALL, conversation);
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
assertTrue(Grader.assessScoreBinary("Does the text mention a color?", completion.response(), this.chatCompletionclient));
@@ -73,11 +76,24 @@ public class CompletionClientTest {
conversation.addTurn(new Turn(TurnType.USER, "You choose! But I need you to explain the character attributes to me."));
final StringBuilder sb = new StringBuilder();
final var completion = this.textCompletionClient.streamConversation(ModelCategory.SMALL, conversation, chunk -> {
System.out.print(chunk);
sb.append(chunk);
if (chunk.reasoningContent() != null) {
System.out.print(chunk.reasoningContent());
}
if (chunk.content() != null) {
System.out.print(chunk.content());
sb.append(chunk.content());
}
});
assertEquals(completion.response(), sb.toString());
if (completion.reasoning() != null) {
// Prompt template does not separate reasoning and response from the stream at the moment
assertTrue(sb.toString().contains(completion.reasoning()));
assertTrue(sb.toString().contains(completion.response()));
assertFalse(completion.response().contains(completion.reasoning()));
}
else {
assertEquals(completion.response(), sb.toString());
}
assertTrue(Grader.assessScoreBinary("Does the text mention a role-playing game character?", completion.response(), this.chatCompletionclient));
}
@@ -88,6 +104,7 @@ public class CompletionClientTest {
conversation.addTurn(new Turn(TurnType.USER, "You choose!"));
final var completion = this.chatCompletionclient.conversation(ModelCategory.MEDIUM, conversation);
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
assertTrue(Grader.assessScoreBinary("Does the text mention a color?", completion.response(), this.chatCompletionclient));
@@ -100,8 +117,13 @@ public class CompletionClientTest {
conversation.addTurn(new Turn(TurnType.USER, "You choose! But I need you to explain the opening to me."));
final StringBuilder sb = new StringBuilder();
final var completion = this.chatCompletionclient.streamConversation(ModelCategory.MEDIUM, conversation, chunk -> {
System.out.print(chunk);
sb.append(chunk);
if (chunk.reasoningContent() != null) {
System.out.print(chunk.reasoningContent());
}
if (chunk.content() != null) {
System.out.print(chunk.content());
sb.append(chunk.content());
}
});
assertEquals(completion.response(), sb.toString());

View File

@@ -1,7 +1,7 @@
prompts=
tokenEncoding=cl100k_base
maxTokens=2000
maxContextLength=8000
maxTokens=2500
maxContextLength=16000
temperature=0.4
topP=1.0
modelProvider=OAI_CHAT_COMPLETION
@@ -10,3 +10,5 @@ url=http://localhost:5001
apiKey=
cost1MInTokens=0
cost1MOutTokens=0
reasoningEffort=medium
reasoningSummaryDetails=auto

View File

@@ -1,7 +1,7 @@
prompts=gemma
prompts=gpt-oss
tokenEncoding=cl100k_base
maxTokens=2000
maxContextLength=8000
maxTokens=2500
maxContextLength=16000
temperature=0.4
topP=1.0
modelProvider=OAI_TEXT_COMPLETION