-
Notifications
You must be signed in to change notification settings - Fork 34
Refactor message extraction to support reasoning content #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package ee.carlrobert.llm.client.openai.completion; | ||
|
||
import org.jetbrains.annotations.Nullable; | ||
|
||
class CompletionDeltaModel { | ||
@Nullable String reasoningContent; | ||
@Nullable String content; | ||
|
||
public CompletionDeltaModel(@Nullable String reasoningContent, @Nullable String content) { | ||
this.reasoningContent = reasoningContent; | ||
this.content = content; | ||
} | ||
|
||
@Nullable | ||
public String getReasoningContent() { | ||
return reasoningContent; | ||
} | ||
|
||
@Nullable | ||
public String getContent() { | ||
return content; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,91 @@ | ||
package ee.carlrobert.llm.client.openai.completion; | ||
|
||
import static ee.carlrobert.llm.client.DeserializationUtil.OBJECT_MAPPER; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; | ||
import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponseChoice; | ||
import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponseChoiceDelta; | ||
import ee.carlrobert.llm.completion.CompletionEventListener; | ||
import ee.carlrobert.llm.completion.CompletionEventSourceListener; | ||
|
||
import java.util.Objects; | ||
import java.util.stream.Stream; | ||
|
||
import static ee.carlrobert.llm.client.DeserializationUtil.OBJECT_MAPPER; | ||
|
||
public class OpenAIChatCompletionEventSourceListener extends CompletionEventSourceListener<String> { | ||
|
||
private CompletionDeltaModel prevCompletionDeltaModel = null; | ||
|
||
public OpenAIChatCompletionEventSourceListener(CompletionEventListener<String> listener) { | ||
super(listener); | ||
} | ||
|
||
/** | ||
* Content of the first choice. | ||
* Returns the first valid message content extracted from the OpenAI Chat Completion response. | ||
* | ||
* <p>This method implements the following new logic: | ||
* <ul> | ||
* <li>Search all choices which are not null</li> | ||
* <li>Search all deltas which are not null</li> | ||
* <li>Use first content which is not null or blank (whitespace)</li> | ||
* <li>Otherwise use "" (empty string) if no match can be found</li> | ||
* <li>Deserializes the provided JSON string into an {@code OpenAIChatCompletionResponse}.</li> | ||
* <li>Retrieves the list of choices and filters out any null elements.</li> | ||
* <li>For each valid choice, obtains the delta and inspects its fields: | ||
* <ul> | ||
* <li>If {@code reasoningContent} is available, wraps it into a delta model with only the reasoning set.</li> | ||
* <li>Otherwise, wraps the {@code content} field into a delta model.</li> | ||
* </ul> | ||
* </li> | ||
* <li>Chooses the first delta model with a non-null, non-blank content; if none is found, defaults to an empty string.</li> | ||
* <li>Formats the output message: | ||
* <ul> | ||
* <li>Prepends an opening {@code <think>} tag when initiating reasoning content.</li> | ||
* <li>Appends a closing {@code </think>} tag and new line characters if transitioning from reasoning to normal content.</li> | ||
* </ul> | ||
* </li> | ||
* </ul> | ||
* | ||
* @return First non-blank content which can be found, otherwise {@code ""} | ||
* @param data the JSON string received from the OpenAI Chat Completion API. | ||
* @return the formatted message extracted from the first valid delta model or an empty string if no matching content is found. | ||
* @throws JsonProcessingException if an error occurs during JSON processing. | ||
*/ | ||
protected String getMessage(String data) throws JsonProcessingException { | ||
var choices = OBJECT_MAPPER | ||
.readValue(data, OpenAIChatCompletionResponse.class) | ||
.getChoices(); | ||
return (choices == null ? Stream.<OpenAIChatCompletionResponseChoice>empty() : choices.stream()) | ||
var response = OBJECT_MAPPER.readValue(data, OpenAIChatCompletionResponse.class); | ||
var choices = response.getChoices(); | ||
|
||
CompletionDeltaModel completionDeltaModel = choices == null ? new CompletionDeltaModel(null, "") : choices.stream() | ||
.filter(Objects::nonNull) | ||
.map(OpenAIChatCompletionResponseChoice::getDelta) | ||
.filter(Objects::nonNull) | ||
.map(OpenAIChatCompletionResponseChoiceDelta::getContent) | ||
.filter(Objects::nonNull) | ||
.map(delta -> | ||
delta.getReasoningContent() != null | ||
? new CompletionDeltaModel(delta.getReasoningContent(), null) | ||
: new CompletionDeltaModel(null, delta.getContent()) | ||
) | ||
.findFirst() | ||
.orElse(""); | ||
.orElse(new CompletionDeltaModel(null, "")); | ||
|
||
return getMessageFromDeltaModel(completionDeltaModel); | ||
} | ||
|
||
@Override | ||
protected ErrorDetails getErrorDetails(String data) throws JsonProcessingException { | ||
return OBJECT_MAPPER.readValue(data, ApiResponseError.class).getError(); | ||
} | ||
|
||
private String getMessageFromDeltaModel(CompletionDeltaModel completionDeltaModel) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like an ugly hack to make it compliant with how ProxyAI parses the thinking output. Perhaps we can change the logic of how and where the respected thinking output is taken. For example, similar support (new param) was just recently added for Google Gemini requests as well, but as far as I know, it's not accepted in the UI logic yet.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to do this by replacing the return type from a string to a class with two fields (message, reasoningContent). but it turned out that a lot of changes had to be made on the plug-in side, because the OpenAI format is used not only by CustomOpenAI, but also by many other providers. it didn't seem advisable without prior refactoring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking ahead, I think there will be a lot of changes when mcp integration begins, which will require rewriting the integration of the llm client library. I'm waiting for a resolution, leave it that way, or still make the transition to the class in the OpenAI response. |
||
StringBuilder sb = new StringBuilder(); | ||
String reasoning = completionDeltaModel.getReasoningContent(); | ||
String content = completionDeltaModel.getContent(); | ||
|
||
if (reasoning != null) { | ||
if (prevCompletionDeltaModel == null || prevCompletionDeltaModel.getReasoningContent() == null) { | ||
sb.append("<think>"); | ||
} | ||
sb.append(reasoning); | ||
} else if (content != null) { | ||
if (prevCompletionDeltaModel != null && prevCompletionDeltaModel.getReasoningContent() != null) { | ||
sb.append("</think>\n\n"); | ||
} | ||
sb.append(content); | ||
} | ||
|
||
prevCompletionDeltaModel = completionDeltaModel; | ||
return sb.toString(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this? I usually increment it when I modify the changelog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure