Skip to content

Commit f2ac921

Browse files
Implement Azure OpenAI response_format option
Signed-off-by: jonghoon park <[email protected]>
1 parent 53a7af5 commit f2ac921

File tree

5 files changed

+493
-51
lines changed

5 files changed

+493
-51
lines changed

models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,30 +28,7 @@
2828
import com.azure.ai.openai.OpenAIClient;
2929
import com.azure.ai.openai.OpenAIClientBuilder;
3030
import com.azure.ai.openai.implementation.accesshelpers.ChatCompletionsOptionsAccessHelper;
31-
import com.azure.ai.openai.models.ChatChoice;
32-
import com.azure.ai.openai.models.ChatCompletions;
33-
import com.azure.ai.openai.models.ChatCompletionsFunctionToolCall;
34-
import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinition;
35-
import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinitionFunction;
36-
import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat;
37-
import com.azure.ai.openai.models.ChatCompletionsOptions;
38-
import com.azure.ai.openai.models.ChatCompletionsResponseFormat;
39-
import com.azure.ai.openai.models.ChatCompletionsTextResponseFormat;
40-
import com.azure.ai.openai.models.ChatCompletionsToolCall;
41-
import com.azure.ai.openai.models.ChatCompletionsToolDefinition;
42-
import com.azure.ai.openai.models.ChatMessageContentItem;
43-
import com.azure.ai.openai.models.ChatMessageImageContentItem;
44-
import com.azure.ai.openai.models.ChatMessageImageUrl;
45-
import com.azure.ai.openai.models.ChatMessageTextContentItem;
46-
import com.azure.ai.openai.models.ChatRequestAssistantMessage;
47-
import com.azure.ai.openai.models.ChatRequestMessage;
48-
import com.azure.ai.openai.models.ChatRequestSystemMessage;
49-
import com.azure.ai.openai.models.ChatRequestToolMessage;
50-
import com.azure.ai.openai.models.ChatRequestUserMessage;
51-
import com.azure.ai.openai.models.CompletionsFinishReason;
52-
import com.azure.ai.openai.models.CompletionsUsage;
53-
import com.azure.ai.openai.models.ContentFilterResultsForPrompt;
54-
import com.azure.ai.openai.models.FunctionCall;
31+
import com.azure.ai.openai.models.*;
5532
import com.azure.core.util.BinaryData;
5633
import io.micrometer.observation.Observation;
5734
import io.micrometer.observation.ObservationRegistry;
@@ -901,7 +878,14 @@ private ChatCompletionsOptions copy(ChatCompletionsOptions fromOptions) {
901878
* @return Azure response format
902879
*/
903880
private ChatCompletionsResponseFormat toAzureResponseFormat(AzureOpenAiResponseFormat responseFormat) {
904-
if (responseFormat == AzureOpenAiResponseFormat.JSON) {
881+
if (responseFormat.getType() == AzureOpenAiResponseFormat.Type.JSON_SCHEMA) {
882+
ChatCompletionsJsonSchemaResponseFormatJsonSchema jsonSchema = new ChatCompletionsJsonSchemaResponseFormatJsonSchema(
883+
responseFormat.getJsonSchema().getName());
884+
jsonSchema.setSchema(BinaryData.fromObject(responseFormat.getJsonSchema().getSchema()));
885+
jsonSchema.setStrict(responseFormat.getJsonSchema().getStrict());
886+
return new ChatCompletionsJsonSchemaResponseFormat(jsonSchema);
887+
}
888+
else if (responseFormat.getType() == AzureOpenAiResponseFormat.Type.JSON_OBJECT) {
905889
return new ChatCompletionsJsonResponseFormat();
906890
}
907891
return new ChatCompletionsTextResponseFormat();
Lines changed: 225 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,27 +16,233 @@
1616

1717
package org.springframework.ai.azure.openai;
1818

19+
import com.fasterxml.jackson.annotation.JsonInclude;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import org.springframework.ai.model.ModelOptionsUtils;
22+
23+
import java.util.Map;
24+
import java.util.Objects;
25+
1926
/**
2027
* Utility enumeration for representing the response format that may be requested from the
21-
* Azure OpenAI model. Please check <a href=
22-
* "https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format">OpenAI
23-
* API documentation</a> for more details.
28+
* Azure OpenAI model. Please check
29+
* <a href= "https://learn.microsoft.com/en-us/azure/ai-services/openai/reference"> Azure
30+
* OpenAI API documentation</a> for more details.
31+
*
32+
* @author Jonghoon Park
2433
*/
25-
public enum AzureOpenAiResponseFormat {
26-
27-
// default value used by OpenAI
28-
TEXT,
29-
/*
30-
* From the OpenAI API documentation: Compatability: Compatible with GPT-4 Turbo and
31-
* all GPT-3.5 Turbo models newer than gpt-3.5-turbo-1106. Caveats: This enables JSON
32-
* mode, which guarantees the message the model generates is valid JSON. Important:
33-
* when using JSON mode, you must also instruct the model to produce JSON yourself via
34-
* a system or user message. Without this, the model may generate an unending stream
35-
* of whitespace until the generation reaches the token limit, resulting in a
36-
* long-running and seemingly "stuck" request. Also note that the message content may
37-
* be partially cut off if finish_reason="length", which indicates the generation
38-
* exceeded max_tokens or the conversation exceeded the max context length.
34+
@JsonInclude(JsonInclude.Include.NON_NULL)
35+
public class AzureOpenAiResponseFormat {
36+
37+
/**
38+
* Type Must be one of 'text', 'json_object' or 'json_schema'.
39+
*/
40+
@JsonProperty("type")
41+
private Type type;
42+
43+
public AzureOpenAiResponseFormat() {
44+
45+
}
46+
47+
/**
48+
* JSON schema object that describes the format of the JSON object. Only applicable
49+
* when type is 'json_schema'.
50+
*/
51+
@JsonProperty("json_schema")
52+
private JsonSchema jsonSchema;
53+
54+
public Type getType() {
55+
return this.type;
56+
}
57+
58+
public JsonSchema getJsonSchema() {
59+
return this.jsonSchema;
60+
}
61+
62+
public static Builder builder() {
63+
return new Builder();
64+
}
65+
66+
private String schema;
67+
68+
private AzureOpenAiResponseFormat(Type type, JsonSchema jsonSchema) {
69+
this.type = type;
70+
this.jsonSchema = jsonSchema;
71+
}
72+
73+
@Override
74+
public boolean equals(Object o) {
75+
if (this == o) {
76+
return true;
77+
}
78+
if (o == null || getClass() != o.getClass()) {
79+
return false;
80+
}
81+
AzureOpenAiResponseFormat that = (AzureOpenAiResponseFormat) o;
82+
return this.type == that.type && Objects.equals(this.jsonSchema, that.jsonSchema);
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(this.type, this.jsonSchema);
88+
}
89+
90+
@Override
91+
public String toString() {
92+
return "AzureOpenAiResponseFormat{" + "type=" + this.type + ", jsonSchema=" + this.jsonSchema + '}';
93+
}
94+
95+
public static final class Builder {
96+
97+
private Type type;
98+
99+
private JsonSchema jsonSchema;
100+
101+
private Builder() {
102+
}
103+
104+
public Builder type(Type type) {
105+
this.type = type;
106+
return this;
107+
}
108+
109+
public Builder jsonSchema(JsonSchema jsonSchema) {
110+
this.jsonSchema = jsonSchema;
111+
return this;
112+
}
113+
114+
public Builder jsonSchema(String jsonSchema) {
115+
this.jsonSchema = JsonSchema.builder().schema(jsonSchema).build();
116+
return this;
117+
}
118+
119+
public AzureOpenAiResponseFormat build() {
120+
return new AzureOpenAiResponseFormat(this.type, this.jsonSchema);
121+
}
122+
123+
}
124+
125+
public enum Type {
126+
127+
/**
128+
* Generates a text response. (default)
129+
*/
130+
@JsonProperty("text")
131+
TEXT,
132+
133+
/**
134+
* Enables JSON mode, which guarantees the message the model generates is valid
135+
* JSON.
136+
*/
137+
@JsonProperty("json_object")
138+
JSON_OBJECT,
139+
140+
/**
141+
* Enables Structured Outputs which guarantees the model will match your supplied
142+
* JSON schema.
143+
*/
144+
@JsonProperty("json_schema")
145+
JSON_SCHEMA
146+
147+
}
148+
149+
/**
150+
* JSON schema object that describes the format of the JSON object. Applicable for the
151+
* 'json_schema' type only.
39152
*/
40-
JSON
153+
@JsonInclude(JsonInclude.Include.NON_NULL)
154+
public static class JsonSchema {
155+
156+
@JsonProperty("name")
157+
private String name;
158+
159+
@JsonProperty("schema")
160+
private Map<String, Object> schema;
161+
162+
@JsonProperty("strict")
163+
private Boolean strict;
164+
165+
public JsonSchema() {
166+
167+
}
168+
169+
public String getName() {
170+
return this.name;
171+
}
172+
173+
public Map<String, Object> getSchema() {
174+
return this.schema;
175+
}
176+
177+
public Boolean getStrict() {
178+
return this.strict;
179+
}
180+
181+
private JsonSchema(String name, Map<String, Object> schema, Boolean strict) {
182+
this.name = name;
183+
this.schema = schema;
184+
this.strict = strict;
185+
}
186+
187+
public static Builder builder() {
188+
return new Builder();
189+
}
190+
191+
@Override
192+
public int hashCode() {
193+
return Objects.hash(this.name, this.schema, this.strict);
194+
}
195+
196+
@Override
197+
public boolean equals(Object o) {
198+
if (this == o) {
199+
return true;
200+
}
201+
if (o == null || getClass() != o.getClass()) {
202+
return false;
203+
}
204+
JsonSchema that = (JsonSchema) o;
205+
return Objects.equals(this.name, that.name) && Objects.equals(this.schema, that.schema)
206+
&& Objects.equals(this.strict, that.strict);
207+
}
208+
209+
public static final class Builder {
210+
211+
private String name = "custom_schema";
212+
213+
private Map<String, Object> schema;
214+
215+
private Boolean strict = true;
216+
217+
private Builder() {
218+
}
219+
220+
public Builder name(String name) {
221+
this.name = name;
222+
return this;
223+
}
224+
225+
public Builder schema(Map<String, Object> schema) {
226+
this.schema = schema;
227+
return this;
228+
}
229+
230+
public Builder schema(String schema) {
231+
this.schema = ModelOptionsUtils.jsonToMap(schema);
232+
return this;
233+
}
234+
235+
public Builder strict(Boolean strict) {
236+
this.strict = strict;
237+
return this;
238+
}
239+
240+
public JsonSchema build() {
241+
return new JsonSchema(this.name, this.schema, this.strict);
242+
}
243+
244+
}
245+
246+
}
41247

42248
}

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -68,7 +68,7 @@ public void createRequestWithChatOptions() {
6868
.logprobs(true)
6969
.topLogprobs(5)
7070
.enhancements(mockAzureChatEnhancementConfiguration)
71-
.responseFormat(AzureOpenAiResponseFormat.TEXT)
71+
.responseFormat(AzureOpenAiResponseFormat.builder().type(AzureOpenAiResponseFormat.Type.TEXT).build())
7272
.build();
7373

7474
var client = AzureOpenAiChatModel.builder()
@@ -114,7 +114,8 @@ public void createRequestWithChatOptions() {
114114
.logprobs(true)
115115
.topLogprobs(4)
116116
.enhancements(anotherMockAzureChatEnhancementConfiguration)
117-
.responseFormat(AzureOpenAiResponseFormat.JSON)
117+
.responseFormat(
118+
AzureOpenAiResponseFormat.builder().type(AzureOpenAiResponseFormat.Type.JSON_OBJECT).build())
118119
.build();
119120

120121
requestOptions = client.toAzureChatCompletionsOptions(new Prompt("Test message content", runtimeOptions));

0 commit comments

Comments
 (0)