From 8bcb3658e887a82fe72e600e8cd81717f431dd4e Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sun, 23 Nov 2025 07:15:09 +0000 Subject: [PATCH 1/2] Add JSON schema support to Azure OpenAI models - Add supportedCapabilities() method to both AzureOpenAiChatModel and AzureOpenAiStreamingChatModel - Return RESPONSE_FORMAT_JSON_SCHEMA capability when responseFormat type is JSON - Support request-level ResponseFormat override from ChatRequest - Enables structured JSON output with schema validation for Azure OpenAI models (e.g., gpt-4o-2024-08-06) Fixes #1953 --- .../azure/openai/AzureOpenAiChatModel.java | 26 +++++++++++++++++-- .../openai/AzureOpenAiStreamingChatModel.java | 24 ++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java index 820991ef6..f42453ce6 100644 --- a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java +++ b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java @@ -3,6 +3,7 @@ import static dev.langchain4j.internal.RetryUtils.withRetry; import static dev.langchain4j.internal.Utils.getOrDefault; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.chat.Capability.RESPONSE_FORMAT_JSON_SCHEMA; import static dev.langchain4j.model.openai.internal.OpenAiUtils.aiMessageFrom; import static dev.langchain4j.model.openai.internal.OpenAiUtils.finishReasonFrom; import static dev.langchain4j.model.openai.internal.OpenAiUtils.toFunctions; @@ -13,9 +14,11 @@ import java.net.Proxy; import java.time.Duration; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; @@ -24,6 +27,7 @@ import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.TokenCountEstimator; +import dev.langchain4j.model.chat.Capability; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.listener.ChatModelErrorContext; import dev.langchain4j.model.chat.listener.ChatModelListener; @@ -74,6 +78,7 @@ public class AzureOpenAiChatModel implements ChatModel { private final TokenCountEstimator tokenizer; private final ResponseFormat responseFormat; private final List listeners; + private final Set supportedCapabilities; public AzureOpenAiChatModel(String endpoint, String apiVersion, @@ -128,10 +133,22 @@ public AzureOpenAiChatModel(String endpoint, : ResponseFormat.builder() .type(ResponseFormatType.valueOf(responseFormat.toUpperCase(Locale.ROOT))) .build(); + + // Azure OpenAI supports JSON schema for models like gpt-4o-2024-08-06+ + this.supportedCapabilities = new HashSet<>(); + if (this.responseFormat != null && ResponseFormatType.JSON.equals(this.responseFormat.type())) { + this.supportedCapabilities.add(RESPONSE_FORMAT_JSON_SCHEMA); + } } - @Override public ChatResponse doChat(ChatRequest chatRequest) { + ResponseFormat requestResponseFormat = this.responseFormat; + + // Handle ChatRequest-level ResponseFormat if provided + if (chatRequest.responseFormat() != null) { + requestResponseFormat = chatRequest.responseFormat(); + } + List messages = chatRequest.messages(); List toolSpecifications = chatRequest.toolSpecifications(); @@ -143,7 +160,7 @@ public ChatResponse doChat(ChatRequest chatRequest) { .maxTokens(maxTokens) .presencePenalty(presencePenalty) .frequencyPenalty(frequencyPenalty) - .responseFormat(responseFormat); + .responseFormat(requestResponseFormat); if (toolSpecifications != null && !toolSpecifications.isEmpty()) { requestBuilder.functions(toFunctions(toolSpecifications)); @@ -239,6 +256,11 @@ private ChatResponse createModelListenerResponse(String responseId, .build(); } + @Override + public Set supportedCapabilities() { + return supportedCapabilities; + } + public static Builder builder() { return new Builder(); } diff --git a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java index b803cd907..dfdc332dc 100644 --- a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java +++ b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java @@ -4,6 +4,7 @@ import static dev.langchain4j.internal.Utils.isNullOrBlank; import static dev.langchain4j.internal.Utils.isNullOrEmpty; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.chat.Capability.RESPONSE_FORMAT_JSON_SCHEMA; import static dev.langchain4j.model.openai.internal.OpenAiUtils.toFunctions; import static dev.langchain4j.model.openai.internal.OpenAiUtils.toOpenAiMessages; import static io.quarkiverse.langchain4j.azure.openai.Consts.DEFAULT_USER_AGENT; @@ -12,9 +13,11 @@ import java.net.Proxy; import java.time.Duration; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; @@ -25,6 +28,7 @@ import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.StreamingResponseHandler; import dev.langchain4j.model.TokenCountEstimator; +import dev.langchain4j.model.chat.Capability; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.listener.ChatModelErrorContext; import dev.langchain4j.model.chat.listener.ChatModelListener; @@ -78,6 +82,7 @@ public class AzureOpenAiStreamingChatModel implements StreamingChatModel { private final TokenCountEstimator tokenizer; private final ResponseFormat responseFormat; private final List listeners; + private final Set supportedCapabilities; public AzureOpenAiStreamingChatModel(String endpoint, String apiVersion, @@ -124,10 +129,22 @@ public AzureOpenAiStreamingChatModel(String endpoint, : ResponseFormat.builder() .type(ResponseFormatType.valueOf(responseFormat.toUpperCase(Locale.ROOT))) .build(); + + // Azure OpenAI supports JSON schema for models like gpt-4o-2024-08-06+ + this.supportedCapabilities = new HashSet<>(); + if (this.responseFormat != null && ResponseFormatType.JSON.equals(this.responseFormat.type())) { + this.supportedCapabilities.add(RESPONSE_FORMAT_JSON_SCHEMA); + } } @Override public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { + ResponseFormat requestResponseFormat = this.responseFormat; + + // Handle ChatRequest-level ResponseFormat if provided + if (chatRequest.responseFormat() != null) { + requestResponseFormat = chatRequest.responseFormat(); + } List messages = chatRequest.messages(); List toolSpecifications = chatRequest.toolSpecifications(); @@ -140,7 +157,7 @@ public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler .maxTokens(maxTokens) .presencePenalty(presencePenalty) .frequencyPenalty(frequencyPenalty) - .responseFormat(responseFormat); + .responseFormat(requestResponseFormat); Integer inputTokenCount = tokenizer == null ? null : tokenizer.estimateTokenCountInMessages(messages); @@ -272,6 +289,11 @@ private ChatResponse createModelListenerResponse(String responseId, .build(); } + @Override + public Set supportedCapabilities() { + return supportedCapabilities; + } + public static Builder builder() { return new Builder(); } From c4ef78dd637e82cc300ab09555845db38e3684e7 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 25 Nov 2025 07:04:01 +0000 Subject: [PATCH 2/2] Fix JSON schema capability check and remove broken type conversion --- .../azure/openai/AzureOpenAiChatModel.java | 11 ++--------- .../azure/openai/AzureOpenAiStreamingChatModel.java | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java index f42453ce6..5f8792a68 100644 --- a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java +++ b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiChatModel.java @@ -136,19 +136,12 @@ public AzureOpenAiChatModel(String endpoint, // Azure OpenAI supports JSON schema for models like gpt-4o-2024-08-06+ this.supportedCapabilities = new HashSet<>(); - if (this.responseFormat != null && ResponseFormatType.JSON.equals(this.responseFormat.type())) { + if (this.responseFormat != null && ResponseFormatType.JSON_SCHEMA.equals(this.responseFormat.type())) { this.supportedCapabilities.add(RESPONSE_FORMAT_JSON_SCHEMA); } } public ChatResponse doChat(ChatRequest chatRequest) { - ResponseFormat requestResponseFormat = this.responseFormat; - - // Handle ChatRequest-level ResponseFormat if provided - if (chatRequest.responseFormat() != null) { - requestResponseFormat = chatRequest.responseFormat(); - } - List messages = chatRequest.messages(); List toolSpecifications = chatRequest.toolSpecifications(); @@ -160,7 +153,7 @@ public ChatResponse doChat(ChatRequest chatRequest) { .maxTokens(maxTokens) .presencePenalty(presencePenalty) .frequencyPenalty(frequencyPenalty) - .responseFormat(requestResponseFormat); + .responseFormat(this.responseFormat); if (toolSpecifications != null && !toolSpecifications.isEmpty()) { requestBuilder.functions(toFunctions(toolSpecifications)); diff --git a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java index dfdc332dc..0cd09253e 100644 --- a/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java +++ b/model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/AzureOpenAiStreamingChatModel.java @@ -132,20 +132,13 @@ public AzureOpenAiStreamingChatModel(String endpoint, // Azure OpenAI supports JSON schema for models like gpt-4o-2024-08-06+ this.supportedCapabilities = new HashSet<>(); - if (this.responseFormat != null && ResponseFormatType.JSON.equals(this.responseFormat.type())) { + if (this.responseFormat != null && ResponseFormatType.JSON_SCHEMA.equals(this.responseFormat.type())) { this.supportedCapabilities.add(RESPONSE_FORMAT_JSON_SCHEMA); } } @Override public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { - ResponseFormat requestResponseFormat = this.responseFormat; - - // Handle ChatRequest-level ResponseFormat if provided - if (chatRequest.responseFormat() != null) { - requestResponseFormat = chatRequest.responseFormat(); - } - List messages = chatRequest.messages(); List toolSpecifications = chatRequest.toolSpecifications(); @@ -157,7 +150,7 @@ public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler .maxTokens(maxTokens) .presencePenalty(presencePenalty) .frequencyPenalty(frequencyPenalty) - .responseFormat(requestResponseFormat); + .responseFormat(this.responseFormat); Integer inputTokenCount = tokenizer == null ? null : tokenizer.estimateTokenCountInMessages(messages);