implemented the missing type: "message" field for user and developer input items in ResponsesApiClient.java, ensuring compliance with the Open Responses specification

This commit is contained in:
extio-de
2026-06-05 00:44:06 +02:00
parent 4453b7c2d1
commit 7969cba37b

View File

@@ -9,9 +9,9 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -27,13 +27,6 @@ import org.springframework.web.client.RestClientResponseException;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import tools.jackson.core.JacksonException;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import de.extio.lmlib.client.Chunk; import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Client; import de.extio.lmlib.client.Client;
import de.extio.lmlib.client.Completion; import de.extio.lmlib.client.Completion;
@@ -41,45 +34,51 @@ import de.extio.lmlib.client.CompletionFinishReason;
import de.extio.lmlib.client.CompletionStatistics; import de.extio.lmlib.client.CompletionStatistics;
import de.extio.lmlib.client.Conversation; import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.client.Conversation.Turn; import de.extio.lmlib.client.Conversation.Turn;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolCall; import de.extio.lmlib.client.ToolCall;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolDefinition; import de.extio.lmlib.client.ToolDefinition;
import de.extio.lmlib.profile.ModelCategory; import de.extio.lmlib.profile.ModelCategory;
import de.extio.lmlib.profile.ModelProfile; import de.extio.lmlib.profile.ModelProfile;
import de.extio.lmlib.profile.ModelProfile.ModelProvider; import de.extio.lmlib.profile.ModelProfile.ModelProvider;
import de.extio.lmlib.profile.ModelProfileService; import de.extio.lmlib.profile.ModelProfileService;
import de.extio.lmlib.token.TokenizerResolver; import de.extio.lmlib.token.TokenizerResolver;
import tools.jackson.core.JacksonException;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
public class ResponsesApiClient implements Client{ public class ResponsesApiClient implements Client {
private static final Logger LOGGER = LoggerFactory.getLogger(ResponsesApiClient.class); private static final Logger LOGGER = LoggerFactory.getLogger(ResponsesApiClient.class);
private static final String ASSISTANT_PHASE_COMMENTARY = "commentary"; private static final String ASSISTANT_PHASE_COMMENTARY = "commentary";
private static final String ASSISTANT_PHASE_FINAL_ANSWER = "final_answer"; private static final String ASSISTANT_PHASE_FINAL_ANSWER = "final_answer";
private static final int TOKEN_MARGIN = 15; private static final int TOKEN_MARGIN = 15;
private static final OpenAiResponsesApiDialect DEFAULT_DIALECT = new OpenAiResponsesApiDialect() { private static final OpenAiResponsesApiDialect DEFAULT_DIALECT = new OpenAiResponsesApiDialect() {
}; };
@Autowired @Autowired
@Qualifier("lmLibRestClientBuilder") @Qualifier("lmLibRestClientBuilder")
private RestClient.Builder restClientBuilder; private RestClient.Builder restClientBuilder;
@Autowired @Autowired
private TokenizerResolver tokenizerResolver; private TokenizerResolver tokenizerResolver;
@Autowired @Autowired
private ModelProfileService modelProfileService; private ModelProfileService modelProfileService;
@Autowired(required = false) @Autowired(required = false)
private OpenAiResponsesApiDialect openAiResponsesApiDialect; private OpenAiResponsesApiDialect openAiResponsesApiDialect;
private final Map<String, List<String>> resolvedModelNames = new ConcurrentHashMap<>(); private final Map<String, List<String>> resolvedModelNames = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public ResponsesApiClient() { public ResponsesApiClient() {
this.objectMapper = JsonMapper.builder() this.objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
@@ -103,24 +102,24 @@ public class ResponsesApiClient implements Client{
.enable(JsonReadFeature.ALLOW_UNQUOTED_PROPERTY_NAMES) .enable(JsonReadFeature.ALLOW_UNQUOTED_PROPERTY_NAMES)
.build(); .build();
} }
@Override @Override
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) { public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
return this.streamConversation(modelCategory, conversation, null, toolCallData, skipCache); return this.streamConversation(modelCategory, conversation, null, toolCallData, skipCache);
} }
@Override @Override
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) { public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
return this.streamConversation(modelProfile, conversation, null, toolCallData, skipCache); return this.streamConversation(modelProfile, conversation, null, toolCallData, skipCache);
} }
@Override @Override
public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) { public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
final var resolvedModelCategory = modelCategory == null ? ModelCategory.MEDIUM : modelCategory; final var resolvedModelCategory = modelCategory == null ? ModelCategory.MEDIUM : modelCategory;
final var modelProfile = this.modelProfileService.getModelProfile(resolvedModelCategory.getModelProfile(), resolvedModelCategory); final var modelProfile = this.modelProfileService.getModelProfile(resolvedModelCategory.getModelProfile(), resolvedModelCategory);
return this.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache); return this.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
} }
@Override @Override
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) { public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OAI_RESPONSES) { if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OAI_RESPONSES) {
@@ -128,7 +127,7 @@ public class ResponsesApiClient implements Client{
} }
return this.requestCompletion(conversation, modelProfile, chunkConsumer, toolCallData); return this.requestCompletion(conversation, modelProfile, chunkConsumer, toolCallData);
} }
@Override @Override
public List<String> getModelNames(final ModelProfile modelProfile, final boolean forceReload) { public List<String> getModelNames(final ModelProfile modelProfile, final boolean forceReload) {
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OAI_RESPONSES) { if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OAI_RESPONSES) {
@@ -137,7 +136,7 @@ public class ResponsesApiClient implements Client{
if (modelProfile.url() == null || modelProfile.url().isBlank()) { if (modelProfile.url() == null || modelProfile.url().isBlank()) {
return List.of(); return List.of();
} }
final var cacheKey = modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey(); final var cacheKey = modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey();
if (forceReload) { if (forceReload) {
final var modelNames = this.loadModelNames(modelProfile); final var modelNames = this.loadModelNames(modelProfile);
@@ -146,29 +145,29 @@ public class ResponsesApiClient implements Client{
} }
return this.resolvedModelNames.computeIfAbsent(cacheKey, key -> this.loadModelNames(modelProfile)); return this.resolvedModelNames.computeIfAbsent(cacheKey, key -> this.loadModelNames(modelProfile));
} }
@Override @Override
public ModelProvider getModelProvider() { public ModelProvider getModelProvider() {
return ModelProvider.OAI_RESPONSES; return ModelProvider.OAI_RESPONSES;
} }
@Override @Override
public boolean supportsToolCalling(final ModelCategory modelCategory) { public boolean supportsToolCalling(final ModelCategory modelCategory) {
final var resolvedModelCategory = modelCategory == null ? ModelCategory.MEDIUM : modelCategory; final var resolvedModelCategory = modelCategory == null ? ModelCategory.MEDIUM : modelCategory;
final var modelProfile = this.modelProfileService.getModelProfile(resolvedModelCategory.getModelProfile(), resolvedModelCategory); final var modelProfile = this.modelProfileService.getModelProfile(resolvedModelCategory.getModelProfile(), resolvedModelCategory);
return this.supportsToolCalling(modelProfile); return this.supportsToolCalling(modelProfile);
} }
@Override @Override
public boolean supportsToolCalling(final ModelProfile modelProfile) { public boolean supportsToolCalling(final ModelProfile modelProfile) {
return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_RESPONSES; return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_RESPONSES;
} }
private Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData) { private Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData) {
final var inputTextBuilder = new StringBuilder(); final var inputTextBuilder = new StringBuilder();
final var inputItems = this.createInputItems(conversation, modelProfile, inputTextBuilder); final var inputItems = this.createInputItems(conversation, modelProfile, inputTextBuilder);
final var request = this.createResponseRequest(inputItems, modelProfile, chunkConsumer != null, toolCallData); final var request = this.createResponseRequest(inputItems, modelProfile, chunkConsumer != null, toolCallData);
final String requestBody; final String requestBody;
try { try {
requestBody = this.objectMapper.writeValueAsString(request); requestBody = this.objectMapper.writeValueAsString(request);
@@ -176,7 +175,7 @@ public class ResponsesApiClient implements Client{
catch (final JacksonException e) { catch (final JacksonException e) {
throw new IllegalStateException("Cannot serialize request body", e); throw new IllegalStateException("Cannot serialize request body", e);
} }
LOGGER.debug("Requesting response at {}", modelProfile.url()); LOGGER.debug("Requesting response at {}", modelProfile.url());
final var start = LocalDateTime.now(); final var start = LocalDateTime.now();
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build() var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
@@ -186,7 +185,7 @@ public class ResponsesApiClient implements Client{
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) { if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey()); requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
} }
return requestSpec.body(requestBody).exchange((clientRequest, clientResponse) -> { return requestSpec.body(requestBody).exchange((clientRequest, clientResponse) -> {
if (clientResponse.getStatusCode().isError()) { if (clientResponse.getStatusCode().isError()) {
final var errorBody = new String(clientResponse.getBody().readAllBytes(), StandardCharsets.UTF_8); final var errorBody = new String(clientResponse.getBody().readAllBytes(), StandardCharsets.UTF_8);
@@ -194,14 +193,14 @@ public class ResponsesApiClient implements Client{
LOGGER.warn("Error response from server: {} {}", clientResponse.getStatusCode(), errorMessage); LOGGER.warn("Error response from server: {} {}", clientResponse.getStatusCode(), errorMessage);
throw new IllegalStateException("Error response from server: " + clientResponse.getStatusCode() + " " + errorMessage); throw new IllegalStateException("Error response from server: " + clientResponse.getStatusCode() + " " + errorMessage);
} }
if (chunkConsumer == null) { if (chunkConsumer == null) {
return this.parseCompletion(clientResponse.getBody(), modelProfile, start, inputTextBuilder.toString()); return this.parseCompletion(clientResponse.getBody(), modelProfile, start, inputTextBuilder.toString());
} }
return this.parseStreamingCompletion(clientResponse.getBody(), modelProfile, start, inputTextBuilder.toString(), chunkConsumer); return this.parseStreamingCompletion(clientResponse.getBody(), modelProfile, start, inputTextBuilder.toString(), chunkConsumer);
}); });
} }
private Map<String, Object> createResponseRequest(final List<Map<String, Object>> inputItems, final ModelProfile modelProfile, final boolean streaming, final ToolCallData toolCallData) { private Map<String, Object> createResponseRequest(final List<Map<String, Object>> inputItems, final ModelProfile modelProfile, final boolean streaming, final ToolCallData toolCallData) {
final var dialect = this.responsesDialect(); final var dialect = this.responsesDialect();
final var store = dialect.store(modelProfile); final var store = dialect.store(modelProfile);
@@ -279,12 +278,12 @@ public class ResponsesApiClient implements Client{
} }
return request; return request;
} }
private List<Map<String, Object>> createInputItems(final Conversation conversation, final ModelProfile modelProfile, final StringBuilder inputTextBuilder) { private List<Map<String, Object>> createInputItems(final Conversation conversation, final ModelProfile modelProfile, final StringBuilder inputTextBuilder) {
final var inputItems = new ArrayList<Map<String, Object>>(); final var inputItems = new ArrayList<Map<String, Object>>();
var tokens = 0; var tokens = 0;
final var maxLength = Math.max(0, modelProfile.maxContextLength() - modelProfile.maxTokens() - TOKEN_MARGIN); final var maxLength = Math.max(0, modelProfile.maxContextLength() - modelProfile.maxTokens() - TOKEN_MARGIN);
for (final Turn turn : conversation.getConversation()) { for (final Turn turn : conversation.getConversation()) {
switch (turn.type()) { switch (turn.type()) {
case SYSTEM -> { case SYSTEM -> {
@@ -355,10 +354,10 @@ public class ResponsesApiClient implements Client{
} }
} }
} }
return inputItems; return inputItems;
} }
private boolean addInputMessageItem(final String role, final String text, final String phase, final List<Map<String, Object>> inputItems, final StringBuilder inputTextBuilder, final ModelProfile modelProfile, final int maxLength, final int currentTokens) { private boolean addInputMessageItem(final String role, final String text, final String phase, final List<Map<String, Object>> inputItems, final StringBuilder inputTextBuilder, final ModelProfile modelProfile, final int maxLength, final int currentTokens) {
var normalizedText = text == null ? "" : text; var normalizedText = text == null ? "" : text;
if (maxLength > 0) { if (maxLength > 0) {
@@ -371,10 +370,19 @@ public class ResponsesApiClient implements Client{
} }
final Map<String, Object> item; final Map<String, Object> item;
if ("assistant".equals(role)) { if ("assistant".equals(role)) {
item = this.createAssistantMessageInputItem(normalizedText, phase); final var messageItem = new LinkedHashMap<String, Object>();
messageItem.put("type", "message");
messageItem.put("role", "assistant");
messageItem.put("status", "completed");
messageItem.put("content", List.of(Map.of("type", "output_text", "text", normalizedText)));
if (phase != null && !phase.isBlank()) {
messageItem.put("phase", phase);
}
item = messageItem;
} }
else { else {
final var easyInputMessage = new LinkedHashMap<String, Object>(); final var easyInputMessage = new LinkedHashMap<String, Object>();
easyInputMessage.put("type", "message");
easyInputMessage.put("role", role); easyInputMessage.put("role", role);
easyInputMessage.put("content", normalizedText); easyInputMessage.put("content", normalizedText);
item = easyInputMessage; item = easyInputMessage;
@@ -383,19 +391,7 @@ public class ResponsesApiClient implements Client{
this.appendInputText(inputTextBuilder, normalizedText); this.appendInputText(inputTextBuilder, normalizedText);
return this.countTokens(inputTextBuilder.toString(), modelProfile) > maxLength && maxLength > 0; return this.countTokens(inputTextBuilder.toString(), modelProfile) > maxLength && maxLength > 0;
} }
private Map<String, Object> createAssistantMessageInputItem(final String normalizedText, final String phase) {
final var messageItem = new LinkedHashMap<String, Object>();
messageItem.put("type", "message");
messageItem.put("role", "assistant");
messageItem.put("status", "completed");
messageItem.put("content", List.of(Map.of("type", "output_text", "text", normalizedText)));
if (phase != null && !phase.isBlank()) {
messageItem.put("phase", phase);
}
return Map.copyOf(messageItem);
}
private boolean addAssistantOutputItems(final List<Map<String, Object>> outputItems, final List<Map<String, Object>> inputItems, final StringBuilder inputTextBuilder, final ModelProfile modelProfile, final int maxLength) { private boolean addAssistantOutputItems(final List<Map<String, Object>> outputItems, final List<Map<String, Object>> inputItems, final StringBuilder inputTextBuilder, final ModelProfile modelProfile, final int maxLength) {
final var containsFunctionCalls = outputItems.stream().anyMatch(outputItem -> "function_call".equals(outputItem.get("type"))); final var containsFunctionCalls = outputItems.stream().anyMatch(outputItem -> "function_call".equals(outputItem.get("type")));
for (final var outputItem : outputItems) { for (final var outputItem : outputItems) {
@@ -410,7 +406,7 @@ public class ResponsesApiClient implements Client{
} }
return false; return false;
} }
private String trimToTokenBudget(final String text, final int remainingTokens, final ModelProfile modelProfile) { private String trimToTokenBudget(final String text, final int remainingTokens, final ModelProfile modelProfile) {
if (text == null || text.isEmpty()) { if (text == null || text.isEmpty()) {
return ""; return "";
@@ -419,14 +415,14 @@ public class ResponsesApiClient implements Client{
if (textTokens <= remainingTokens) { if (textTokens <= remainingTokens) {
return text; return text;
} }
var tokenized = this.tokenizerResolver.tokenize(text, modelProfile); final var tokenized = this.tokenizerResolver.tokenize(text, modelProfile);
if (remainingTokens <= 0 || tokenized.isEmpty()) { if (remainingTokens <= 0 || tokenized.isEmpty()) {
return ""; return "";
} }
final var endIndex = Math.max(1, Math.min(tokenized.size(), remainingTokens)); final var endIndex = Math.max(1, Math.min(tokenized.size(), remainingTokens));
return this.tokenizerResolver.detokenize(tokenized.subList(0, endIndex), modelProfile); return this.tokenizerResolver.detokenize(tokenized.subList(0, endIndex), modelProfile);
} }
private List<Map<String, Object>> createTools(final List<ToolDefinition> toolDefinitions) { private List<Map<String, Object>> createTools(final List<ToolDefinition> toolDefinitions) {
return toolDefinitions.stream().map(toolDefinition -> { return toolDefinitions.stream().map(toolDefinition -> {
final var tool = new LinkedHashMap<String, Object>(); final var tool = new LinkedHashMap<String, Object>();
@@ -440,7 +436,7 @@ public class ResponsesApiClient implements Client{
return Map.copyOf(tool); return Map.copyOf(tool);
}).toList(); }).toList();
} }
private Object createToolChoice(final ToolCallData toolCallData) { private Object createToolChoice(final ToolCallData toolCallData) {
if (toolCallData == null) { if (toolCallData == null) {
return null; return null;
@@ -456,7 +452,7 @@ public class ResponsesApiClient implements Client{
} }
return null; return null;
} }
private Completion parseCompletion(final InputStream body, final ModelProfile modelProfile, final LocalDateTime start, final String prompt) throws IOException { private Completion parseCompletion(final InputStream body, final ModelProfile modelProfile, final LocalDateTime start, final String prompt) throws IOException {
final var response = this.objectMapper.readTree(body); final var response = this.objectMapper.readTree(body);
final var output = response.path("output"); final var output = response.path("output");
@@ -468,13 +464,13 @@ public class ResponsesApiClient implements Client{
final var statistics = this.createCompletionStatistics(modelProfile, start, response.path("usage"), prompt, responseText, reasoningText); final var statistics = this.createCompletionStatistics(modelProfile, start, response.path("usage"), prompt, responseText, reasoningText);
return new Completion(responseText, reasoningText, finishReason, statistics, toolCalls, outputItems); return new Completion(responseText, reasoningText, finishReason, statistics, toolCalls, outputItems);
} }
private Completion parseStreamingCompletion(final InputStream body, final ModelProfile modelProfile, final LocalDateTime start, final String prompt, final Consumer<Chunk> chunkConsumer) throws IOException { private Completion parseStreamingCompletion(final InputStream body, final ModelProfile modelProfile, final LocalDateTime start, final String prompt, final Consumer<Chunk> chunkConsumer) throws IOException {
var response = this.objectMapper.createObjectNode(); var response = this.objectMapper.createObjectNode();
final var responseText = new StringBuilder(); final var responseText = new StringBuilder();
final var reasoningText = new StringBuilder(); final var reasoningText = new StringBuilder();
final var toolCalls = new ArrayList<StreamingToolCall>(); final var toolCalls = new ArrayList<StreamingToolCall>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8))) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
@@ -493,7 +489,7 @@ public class ResponsesApiClient implements Client{
if ("[DONE]".equals(line)) { if ("[DONE]".equals(line)) {
break; break;
} }
final JsonNode event; final JsonNode event;
try { try {
event = this.objectMapper.readTree(line); event = this.objectMapper.readTree(line);
@@ -502,7 +498,7 @@ public class ResponsesApiClient implements Client{
LOGGER.warn("Streamed chunk is not parseable: {}", line, e); LOGGER.warn("Streamed chunk is not parseable: {}", line, e);
continue; continue;
} }
final var eventType = this.text(event, "type"); final var eventType = this.text(event, "type");
switch (eventType) { switch (eventType) {
case "response.output_text.delta" -> { case "response.output_text.delta" -> {
@@ -536,11 +532,11 @@ public class ResponsesApiClient implements Client{
} }
} }
} }
if (!response.path("output").isArray()) { if (!response.path("output").isArray()) {
response.putArray("output"); response.putArray("output");
} }
final var completedText = responseText.isEmpty() ? this.extractAssistantMessageText(response.path("output")) : responseText.toString(); final var completedText = responseText.isEmpty() ? this.extractAssistantMessageText(response.path("output")) : responseText.toString();
final var completedReasoning = reasoningText.isEmpty() ? this.extractReasoningSummaryText(response.path("output")) : reasoningText.toString(); final var completedReasoning = reasoningText.isEmpty() ? this.extractReasoningSummaryText(response.path("output")) : reasoningText.toString();
final var completedToolCalls = this.extractToolCalls(response.path("output")); final var completedToolCalls = this.extractToolCalls(response.path("output"));
@@ -553,7 +549,7 @@ public class ResponsesApiClient implements Client{
final var statistics = this.createCompletionStatistics(modelProfile, start, response.path("usage"), prompt, completedText, completedReasoning); final var statistics = this.createCompletionStatistics(modelProfile, start, response.path("usage"), prompt, completedText, completedReasoning);
return new Completion(completedText, completedReasoning, finishReason, statistics, finalToolCalls, outputItems); return new Completion(completedText, completedReasoning, finishReason, statistics, finalToolCalls, outputItems);
} }
private void mergeFunctionCallItem(final List<StreamingToolCall> toolCalls, final JsonNode item, final int outputIndex) { private void mergeFunctionCallItem(final List<StreamingToolCall> toolCalls, final JsonNode item, final int outputIndex) {
if (!"function_call".equals(this.text(item, "type"))) { if (!"function_call".equals(this.text(item, "type"))) {
return; return;
@@ -573,7 +569,7 @@ public class ResponsesApiClient implements Client{
} }
toolCall.type = "function"; toolCall.type = "function";
} }
private void mergeFunctionCallArguments(final List<StreamingToolCall> toolCalls, final int outputIndex, final String delta) { private void mergeFunctionCallArguments(final List<StreamingToolCall> toolCalls, final int outputIndex, final String delta) {
if (delta == null || delta.isEmpty()) { if (delta == null || delta.isEmpty()) {
return; return;
@@ -581,14 +577,14 @@ public class ResponsesApiClient implements Client{
final var toolCall = this.ensureFunctionCall(toolCalls, outputIndex); final var toolCall = this.ensureFunctionCall(toolCalls, outputIndex);
toolCall.arguments = (toolCall.arguments == null ? "" : toolCall.arguments) + delta; toolCall.arguments = (toolCall.arguments == null ? "" : toolCall.arguments) + delta;
} }
private void setFunctionCallArguments(final List<StreamingToolCall> toolCalls, final int outputIndex, final String arguments) { private void setFunctionCallArguments(final List<StreamingToolCall> toolCalls, final int outputIndex, final String arguments) {
if (arguments == null || arguments.isEmpty()) { if (arguments == null || arguments.isEmpty()) {
return; return;
} }
this.ensureFunctionCall(toolCalls, outputIndex).arguments = arguments; this.ensureFunctionCall(toolCalls, outputIndex).arguments = arguments;
} }
private StreamingToolCall ensureFunctionCall(final List<StreamingToolCall> toolCalls, final int outputIndex) { private StreamingToolCall ensureFunctionCall(final List<StreamingToolCall> toolCalls, final int outputIndex) {
final var index = outputIndex < 0 ? toolCalls.size() : outputIndex; final var index = outputIndex < 0 ? toolCalls.size() : outputIndex;
while (toolCalls.size() <= index) { while (toolCalls.size() <= index) {
@@ -596,7 +592,7 @@ public class ResponsesApiClient implements Client{
} }
return toolCalls.get(index); return toolCalls.get(index);
} }
private List<ToolCall> extractToolCalls(final JsonNode output) { private List<ToolCall> extractToolCalls(final JsonNode output) {
if (!output.isArray()) { if (!output.isArray()) {
return List.of(); return List.of();
@@ -615,14 +611,14 @@ public class ResponsesApiClient implements Client{
} }
return List.copyOf(toolCalls); return List.copyOf(toolCalls);
} }
private List<ToolCall> toToolCalls(final List<StreamingToolCall> streamingToolCalls) { private List<ToolCall> toToolCalls(final List<StreamingToolCall> streamingToolCalls) {
return streamingToolCalls.stream() return streamingToolCalls.stream()
.filter(toolCall -> toolCall.callId != null && !toolCall.callId.isBlank()) .filter(toolCall -> toolCall.callId != null && !toolCall.callId.isBlank())
.map(toolCall -> new ToolCall(toolCall.callId, toolCall.type == null ? "function" : toolCall.type, toolCall.name, toolCall.arguments)) .map(toolCall -> new ToolCall(toolCall.callId, toolCall.type == null ? "function" : toolCall.type, toolCall.name, toolCall.arguments))
.toList(); .toList();
} }
private List<Map<String, Object>> extractOutputItems(final JsonNode output) { private List<Map<String, Object>> extractOutputItems(final JsonNode output) {
if (!output.isArray()) { if (!output.isArray()) {
return List.of(); return List.of();
@@ -636,7 +632,7 @@ public class ResponsesApiClient implements Client{
} }
return List.copyOf(outputItems); return List.copyOf(outputItems);
} }
private String extractAssistantMessageText(final JsonNode output) { private String extractAssistantMessageText(final JsonNode output) {
if (!output.isArray()) { if (!output.isArray()) {
return ""; return "";
@@ -655,7 +651,7 @@ public class ResponsesApiClient implements Client{
} }
return response.toString(); return response.toString();
} }
private String extractReasoningSummaryText(final JsonNode output) { private String extractReasoningSummaryText(final JsonNode output) {
if (!output.isArray()) { if (!output.isArray()) {
return ""; return "";
@@ -684,7 +680,7 @@ public class ResponsesApiClient implements Client{
} }
return reasoning.toString(); return reasoning.toString();
} }
private String extractReasoningDelta(final String eventType, final JsonNode event) { private String extractReasoningDelta(final String eventType, final JsonNode event) {
if (eventType == null || !eventType.contains("reasoning")) { if (eventType == null || !eventType.contains("reasoning")) {
return ""; return "";
@@ -695,7 +691,7 @@ public class ResponsesApiClient implements Client{
} }
return this.text(event, "text"); return this.text(event, "text");
} }
private CompletionFinishReason determineFinishReason(final JsonNode response, final List<ToolCall> toolCalls) { private CompletionFinishReason determineFinishReason(final JsonNode response, final List<ToolCall> toolCalls) {
if (!toolCalls.isEmpty()) { if (!toolCalls.isEmpty()) {
return CompletionFinishReason.TOOL_CALLS; return CompletionFinishReason.TOOL_CALLS;
@@ -722,7 +718,7 @@ public class ResponsesApiClient implements Client{
var cachedInTokens = 0; var cachedInTokens = 0;
var outTokens = 0; var outTokens = 0;
var reasoningOutTokens = 0; var reasoningOutTokens = 0;
if (usage != null && !usage.isMissingNode()) { if (usage != null && !usage.isMissingNode()) {
final var inputTokens = this.integerValue(usage.path("input_tokens"), 0); final var inputTokens = this.integerValue(usage.path("input_tokens"), 0);
cachedInTokens = this.integerValue(usage.path("input_tokens_details").path("cached_tokens"), 0); cachedInTokens = this.integerValue(usage.path("input_tokens_details").path("cached_tokens"), 0);
@@ -741,7 +737,7 @@ public class ResponsesApiClient implements Client{
if (outTokens <= 0 && response != null && !response.isBlank()) { if (outTokens <= 0 && response != null && !response.isBlank()) {
outTokens = this.countTokens(response, modelProfile); outTokens = this.countTokens(response, modelProfile);
} }
final var cost = this.getUsageCost(usage) final var cost = this.getUsageCost(usage)
.max(this.calculateTokenCost(modelProfile, inTokens, cachedInTokens, outTokens, reasoningOutTokens)); .max(this.calculateTokenCost(modelProfile, inTokens, cachedInTokens, outTokens, reasoningOutTokens));
return new CompletionStatistics( return new CompletionStatistics(
@@ -754,7 +750,7 @@ public class ResponsesApiClient implements Client{
cost, cost,
false); false);
} }
private BigDecimal getUsageCost(final JsonNode usage) { private BigDecimal getUsageCost(final JsonNode usage) {
if (usage == null || usage.isMissingNode()) { if (usage == null || usage.isMissingNode()) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -773,7 +769,7 @@ public class ResponsesApiClient implements Client{
cost = cost.add(this.decimalValue(usage.path("cost_details").path("upstream_inference_output_cost"))); cost = cost.add(this.decimalValue(usage.path("cost_details").path("upstream_inference_output_cost")));
return cost; return cost;
} }
private BigDecimal calculateTokenCost(final ModelProfile modelProfile, final int inTokens, final int cachedInTokens, final int outTokens, final int reasoningOutTokens) { private BigDecimal calculateTokenCost(final ModelProfile modelProfile, final int inTokens, final int cachedInTokens, final int outTokens, final int reasoningOutTokens) {
var cost = BigDecimal.ZERO; var cost = BigDecimal.ZERO;
cost = cost.add(new BigDecimal(inTokens).multiply(modelProfile.costPerInToken())); cost = cost.add(new BigDecimal(inTokens).multiply(modelProfile.costPerInToken()));
@@ -782,7 +778,7 @@ public class ResponsesApiClient implements Client{
cost = cost.add(new BigDecimal(reasoningOutTokens).multiply(modelProfile.costPerReasoningOutToken())); cost = cost.add(new BigDecimal(reasoningOutTokens).multiply(modelProfile.costPerReasoningOutToken()));
return cost; return cost;
} }
private List<String> loadModelNames(final ModelProfile modelProfile) { private List<String> loadModelNames(final ModelProfile modelProfile) {
try { try {
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build() var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
@@ -818,14 +814,14 @@ public class ResponsesApiClient implements Client{
return List.of(); return List.of();
} }
} }
private String getPrimaryModelName(final ModelProfile modelProfile) { private String getPrimaryModelName(final ModelProfile modelProfile) {
if (modelProfile.modelName() != null && !modelProfile.modelName().isBlank()) { if (modelProfile.modelName() != null && !modelProfile.modelName().isBlank()) {
return modelProfile.modelName(); return modelProfile.modelName();
} }
return this.getModelNames(modelProfile, false).stream().findFirst().orElse(""); return this.getModelNames(modelProfile, false).stream().findFirst().orElse("");
} }
private String getErrorMessage(final String errorBody) { private String getErrorMessage(final String errorBody) {
if (errorBody == null || errorBody.isBlank()) { if (errorBody == null || errorBody.isBlank()) {
return ""; return "";
@@ -840,7 +836,7 @@ public class ResponsesApiClient implements Client{
return errorBody; return errorBody;
} }
} }
private String getEventErrorMessage(final JsonNode event) { private String getEventErrorMessage(final JsonNode event) {
final var message = this.text(event.path("error"), "message"); final var message = this.text(event.path("error"), "message");
if (!message.isEmpty()) { if (!message.isEmpty()) {
@@ -852,11 +848,11 @@ public class ResponsesApiClient implements Client{
} }
return event.toString(); return event.toString();
} }
private OpenAiResponsesApiDialect responsesDialect() { private OpenAiResponsesApiDialect responsesDialect() {
return this.openAiResponsesApiDialect == null ? DEFAULT_DIALECT : this.openAiResponsesApiDialect; return this.openAiResponsesApiDialect == null ? DEFAULT_DIALECT : this.openAiResponsesApiDialect;
} }
private Map<String, Object> createReasoningRequest(final OpenAiResponsesApiDialect.ResponsesReasoning reasoning) { private Map<String, Object> createReasoningRequest(final OpenAiResponsesApiDialect.ResponsesReasoning reasoning) {
if (reasoning == null) { if (reasoning == null) {
return null; return null;
@@ -873,7 +869,7 @@ public class ResponsesApiClient implements Client{
} }
return request.isEmpty() ? null : request; return request.isEmpty() ? null : request;
} }
private List<String> createIncludeList(final ModelProfile modelProfile, final boolean store) { private List<String> createIncludeList(final ModelProfile modelProfile, final boolean store) {
final var include = new LinkedHashSet<>(this.responsesDialect().include(modelProfile)); final var include = new LinkedHashSet<>(this.responsesDialect().include(modelProfile));
if (!store && this.responsesDialect().includeEncryptedReasoningContent(modelProfile)) { if (!store && this.responsesDialect().includeEncryptedReasoningContent(modelProfile)) {
@@ -881,7 +877,7 @@ public class ResponsesApiClient implements Client{
} }
return List.copyOf(include); return List.copyOf(include);
} }
private Map<String, Object> createStreamOptionsRequest(final OpenAiResponsesApiDialect.ResponsesStreamOptions streamOptions) { private Map<String, Object> createStreamOptionsRequest(final OpenAiResponsesApiDialect.ResponsesStreamOptions streamOptions) {
if (streamOptions == null) { if (streamOptions == null) {
return null; return null;
@@ -892,7 +888,7 @@ public class ResponsesApiClient implements Client{
} }
return request.isEmpty() ? null : request; return request.isEmpty() ? null : request;
} }
private Map<String, Object> createTextRequest(final OpenAiResponsesApiDialect.ResponsesText text) { private Map<String, Object> createTextRequest(final OpenAiResponsesApiDialect.ResponsesText text) {
if (text == null) { if (text == null) {
return null; return null;
@@ -903,7 +899,7 @@ public class ResponsesApiClient implements Client{
} }
return request.isEmpty() ? null : request; return request.isEmpty() ? null : request;
} }
private List<Map<String, Object>> createFallbackOutputItems(final String assistantText, final List<ToolCall> toolCalls) { private List<Map<String, Object>> createFallbackOutputItems(final String assistantText, final List<ToolCall> toolCalls) {
final var outputItems = new ArrayList<Map<String, Object>>(); final var outputItems = new ArrayList<Map<String, Object>>();
if (assistantText != null && !assistantText.isBlank()) { if (assistantText != null && !assistantText.isBlank()) {
@@ -926,7 +922,7 @@ public class ResponsesApiClient implements Client{
} }
return List.copyOf(outputItems); return List.copyOf(outputItems);
} }
private void appendInputText(final StringBuilder inputTextBuilder, final String text) { private void appendInputText(final StringBuilder inputTextBuilder, final String text) {
if (text == null || text.isBlank()) { if (text == null || text.isBlank()) {
return; return;
@@ -936,7 +932,7 @@ public class ResponsesApiClient implements Client{
} }
inputTextBuilder.append(text); inputTextBuilder.append(text);
} }
private void appendInputItemText(final StringBuilder inputTextBuilder, final Map<String, Object> inputItem) { private void appendInputItemText(final StringBuilder inputTextBuilder, final Map<String, Object> inputItem) {
if (inputItem == null || inputItem.isEmpty()) { if (inputItem == null || inputItem.isEmpty()) {
return; return;
@@ -948,14 +944,14 @@ public class ResponsesApiClient implements Client{
this.appendInputText(inputTextBuilder, inputItem.toString()); this.appendInputText(inputTextBuilder, inputItem.toString());
} }
} }
private String resolveAssistantPhase(final String phase, final List<ToolCall> toolCalls) { private String resolveAssistantPhase(final String phase, final List<ToolCall> toolCalls) {
if (phase != null && !phase.isBlank()) { if (phase != null && !phase.isBlank()) {
return phase; return phase;
} }
return toolCalls != null && !toolCalls.isEmpty() ? ASSISTANT_PHASE_COMMENTARY : ASSISTANT_PHASE_FINAL_ANSWER; return toolCalls != null && !toolCalls.isEmpty() ? ASSISTANT_PHASE_COMMENTARY : ASSISTANT_PHASE_FINAL_ANSWER;
} }
private Map<String, Object> normalizeAssistantOutputItemPhase(final Map<String, Object> outputItem, final boolean containsFunctionCalls) { private Map<String, Object> normalizeAssistantOutputItemPhase(final Map<String, Object> outputItem, final boolean containsFunctionCalls) {
if (outputItem == null || outputItem.isEmpty()) { if (outputItem == null || outputItem.isEmpty()) {
return Map.of(); return Map.of();
@@ -967,25 +963,25 @@ public class ResponsesApiClient implements Client{
normalizedOutputItem.put("phase", containsFunctionCalls ? ASSISTANT_PHASE_COMMENTARY : ASSISTANT_PHASE_FINAL_ANSWER); normalizedOutputItem.put("phase", containsFunctionCalls ? ASSISTANT_PHASE_COMMENTARY : ASSISTANT_PHASE_FINAL_ANSWER);
return Map.copyOf(normalizedOutputItem); return Map.copyOf(normalizedOutputItem);
} }
private int countTokens(final String text, final ModelProfile modelProfile) { private int countTokens(final String text, final ModelProfile modelProfile) {
return text == null || text.isBlank() ? 0 : this.tokenizerResolver.count(text, modelProfile); return text == null || text.isBlank() ? 0 : this.tokenizerResolver.count(text, modelProfile);
} }
private String text(final JsonNode node, final String field) { private String text(final JsonNode node, final String field) {
if (node == null || node.isMissingNode() || node.isNull()) { if (node == null || node.isMissingNode() || node.isNull()) {
return ""; return "";
} }
return this.nodeText(node.path(field)); return this.nodeText(node.path(field));
} }
private int integerValue(final JsonNode node, final int defaultValue) { private int integerValue(final JsonNode node, final int defaultValue) {
if (node == null || node.isMissingNode() || node.isNull()) { if (node == null || node.isMissingNode() || node.isNull()) {
return defaultValue; return defaultValue;
} }
return node.canConvertToInt() ? node.intValue() : defaultValue; return node.canConvertToInt() ? node.intValue() : defaultValue;
} }
private BigDecimal decimalValue(final JsonNode node) { private BigDecimal decimalValue(final JsonNode node) {
if (node == null || node.isMissingNode() || node.isNull()) { if (node == null || node.isMissingNode() || node.isNull()) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -997,7 +993,7 @@ public class ResponsesApiClient implements Client{
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
} }
private String nodeText(final JsonNode node) { private String nodeText(final JsonNode node) {
if (node == null || node.isMissingNode() || node.isNull()) { if (node == null || node.isMissingNode() || node.isNull()) {
return ""; return "";
@@ -1016,16 +1012,16 @@ public class ResponsesApiClient implements Client{
return ""; return "";
} }
} }
private static final class StreamingToolCall { private static final class StreamingToolCall {
private String callId; private String callId;
private String type; private String type;
private String name; private String name;
private String arguments; private String arguments;
} }
} }