Skip to content

Commit 50009c3

Browse files
committed
Extract ResponseFormat to standalone class
- Extracts ResponseFormat from being a nested record in OpenAiApi to a dedicated class with builder pattern support. - Resolve the issue with constructor bindings for the Boog property binding. - Re-enables previously disabled response format integration tests. Resolves #1681
1 parent cbdb578 commit 50009c3

File tree

5 files changed

+267
-78
lines changed

5 files changed

+267
-78
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import org.springframework.ai.model.function.FunctionCallback;
3535
import org.springframework.ai.model.function.FunctionCallingOptions;
3636
import org.springframework.ai.openai.api.OpenAiApi;
37-
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
37+
import org.springframework.ai.openai.api.ResponseFormat;
3838
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.StreamOptions;
3939
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder;
4040
import org.springframework.util.Assert;

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -869,74 +869,6 @@ public static Object FUNCTION(String functionName) {
869869
}
870870
}
871871

872-
/**
873-
* An object specifying the format that the model must output.
874-
* @param type Must be one of 'text' or 'json_object'.
875-
* @param jsonSchema JSON schema object that describes the format of the JSON object.
876-
* Only applicable when type is 'json_schema'.
877-
*/
878-
@JsonInclude(Include.NON_NULL)
879-
public record ResponseFormat(
880-
@JsonProperty("type") Type type,
881-
@JsonProperty("json_schema") JsonSchema jsonSchema) {
882-
883-
public ResponseFormat(Type type) {
884-
this(type, (JsonSchema) null);
885-
}
886-
887-
public ResponseFormat(Type type, String schema) {
888-
this(type, "custom_schema", schema, true);
889-
}
890-
891-
public ResponseFormat(Type type, String name, String schema, Boolean strict) {
892-
this(type, StringUtils.hasText(schema) ? new JsonSchema(name, schema, strict) : null);
893-
}
894-
895-
public enum Type {
896-
/**
897-
* Generates a text response. (default)
898-
*/
899-
@JsonProperty("text")
900-
TEXT,
901-
902-
/**
903-
* Enables JSON mode, which guarantees the message
904-
* the model generates is valid JSON.
905-
*/
906-
@JsonProperty("json_object")
907-
JSON_OBJECT,
908-
909-
/**
910-
* Enables Structured Outputs which guarantees the model
911-
* will match your supplied JSON schema.
912-
*/
913-
@JsonProperty("json_schema")
914-
JSON_SCHEMA
915-
}
916-
917-
/**
918-
* JSON schema object that describes the format of the JSON object.
919-
* Applicable for the 'json_schema' type only.
920-
* @param name The name of the schema.
921-
* @param schema The JSON schema object that describes the format of the JSON object.
922-
* @param strict If true, the model will only generate outputs that match the schema.
923-
*/
924-
@JsonInclude(Include.NON_NULL)
925-
public record JsonSchema(
926-
@JsonProperty("name") String name,
927-
@JsonProperty("schema") Map<String, Object> schema,
928-
@JsonProperty("strict") Boolean strict) {
929-
930-
public JsonSchema(String name, String schema) {
931-
this(name, ModelOptionsUtils.jsonToMap(schema), true);
932-
}
933-
934-
public JsonSchema(String name, String schema, Boolean strict) {
935-
this(StringUtils.hasText(name) ? name : "custom_schema", ModelOptionsUtils.jsonToMap(schema), strict);
936-
}
937-
}
938-
939-
}
940872
/**
941873
* @param includeUsage If set, an additional chunk will be streamed
942874
* before the data: [DONE] message. The usage field on this chunk
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*
2+
* Copyright 2024 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.openai.api;
17+
18+
import java.util.Map;
19+
import java.util.Objects;
20+
21+
import com.fasterxml.jackson.annotation.JsonInclude;
22+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
25+
import org.springframework.ai.model.ModelOptionsUtils;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* An object specifying the format that the model must output.
30+
*
31+
* Setting the type to JSON_SCHEMA, enables Structured Outputs which ensures the model
32+
* will match your supplied JSON schema. Learn more in the
33+
* <a href="https://platform.openai.com/docs/guides/structured-outputs"> Structured
34+
* Outputs guide.</a <br/>
35+
*
36+
* References: <a href=
37+
* "https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format">OpenAi
38+
* API - ResponseFormat</a>,
39+
* <a href="https://platform.openai.com/docs/guides/structured-outputs#json-mode">JSON
40+
* Mode</a>, <a href=
41+
* "https://platform.openai.com/docs/guides/structured-outputs#structured-outputs-vs-json-mode">Structured
42+
* Outputs vs JSON mode</a>
43+
*
44+
* @author Christian Tzolov
45+
* @since 1.0.0
46+
*/
47+
48+
@JsonInclude(Include.NON_NULL)
49+
public class ResponseFormat {
50+
51+
/**
52+
* Type Must be one of 'text', 'json_object' or 'json_schema'.
53+
*/
54+
@JsonProperty("type")
55+
private Type type;
56+
57+
/**
58+
* JSON schema object that describes the format of the JSON object. Only applicable
59+
* when type is 'json_schema'.
60+
*/
61+
@JsonProperty("json_schema")
62+
private JsonSchema jsonSchema = null;
63+
64+
public Type getType() {
65+
return type;
66+
}
67+
68+
public void setType(Type type) {
69+
this.type = type;
70+
}
71+
72+
public JsonSchema getJsonSchema() {
73+
return jsonSchema;
74+
}
75+
76+
public void setJsonSchema(JsonSchema jsonSchema) {
77+
this.jsonSchema = jsonSchema;
78+
}
79+
80+
private ResponseFormat(Type type, JsonSchema jsonSchema) {
81+
this.type = type;
82+
this.jsonSchema = jsonSchema;
83+
}
84+
85+
public ResponseFormat(Type type, String schema) {
86+
this(type, StringUtils.hasText(schema) ? JsonSchema.builder().schema(schema).strict(true).build() : null);
87+
}
88+
89+
public static Builder builder() {
90+
return new Builder();
91+
}
92+
93+
public static class Builder {
94+
95+
private Type type;
96+
97+
private JsonSchema jsonSchema;
98+
99+
private Builder() {
100+
}
101+
102+
public Builder type(Type type) {
103+
this.type = type;
104+
return this;
105+
}
106+
107+
public Builder jsonSchema(JsonSchema jsonSchema) {
108+
this.jsonSchema = jsonSchema;
109+
return this;
110+
}
111+
112+
public Builder jsonSchema(String jsonSchema) {
113+
this.jsonSchema = JsonSchema.builder().schema(jsonSchema).build();
114+
return this;
115+
}
116+
117+
public ResponseFormat build() {
118+
return new ResponseFormat(this.type, this.jsonSchema);
119+
}
120+
121+
}
122+
123+
public enum Type {
124+
125+
/**
126+
* Generates a text response. (default)
127+
*/
128+
@JsonProperty("text")
129+
TEXT,
130+
131+
/**
132+
* Enables JSON mode, which guarantees the message the model generates is valid
133+
* JSON.
134+
*/
135+
@JsonProperty("json_object")
136+
JSON_OBJECT,
137+
138+
/**
139+
* Enables Structured Outputs which guarantees the model will match your supplied
140+
* JSON schema.
141+
*/
142+
@JsonProperty("json_schema")
143+
JSON_SCHEMA
144+
145+
}
146+
147+
/**
148+
* JSON schema object that describes the format of the JSON object. Applicable for the
149+
* 'json_schema' type only.
150+
*/
151+
@JsonInclude(Include.NON_NULL)
152+
public static class JsonSchema {
153+
154+
@JsonProperty("name")
155+
private final String name;
156+
157+
@JsonProperty("schema")
158+
private final Map<String, Object> schema;
159+
160+
@JsonProperty("strict")
161+
private final Boolean strict;
162+
163+
public String getName() {
164+
return this.name;
165+
}
166+
167+
public Map<String, Object> getSchema() {
168+
return this.schema;
169+
}
170+
171+
public Boolean getStrict() {
172+
return this.strict;
173+
}
174+
175+
private JsonSchema(String name, Map<String, Object> schema, Boolean strict) {
176+
this.name = name;
177+
this.schema = schema;
178+
this.strict = strict;
179+
}
180+
181+
public static Builder builder() {
182+
return new Builder();
183+
}
184+
185+
public static class Builder {
186+
187+
private String name = "custom_schema";
188+
189+
private Map<String, Object> schema;
190+
191+
private Boolean strict = true;
192+
193+
private Builder() {
194+
}
195+
196+
public Builder name(String name) {
197+
this.name = name;
198+
return this;
199+
}
200+
201+
public Builder schema(Map<String, Object> schema) {
202+
this.schema = schema;
203+
return this;
204+
}
205+
206+
public Builder schema(String schema) {
207+
this.schema = ModelOptionsUtils.jsonToMap(schema);
208+
return this;
209+
}
210+
211+
public Builder strict(Boolean strict) {
212+
this.strict = strict;
213+
return this;
214+
}
215+
216+
public JsonSchema build() {
217+
return new JsonSchema(name, schema, strict);
218+
}
219+
220+
}
221+
222+
@Override
223+
public int hashCode() {
224+
return Objects.hash(name, schema, strict);
225+
}
226+
227+
@Override
228+
public boolean equals(Object o) {
229+
if (this == o)
230+
return true;
231+
if (o == null || getClass() != o.getClass())
232+
return false;
233+
JsonSchema that = (JsonSchema) o;
234+
return Objects.equals(name, that.name) && Objects.equals(schema, that.schema)
235+
&& Objects.equals(strict, that.strict);
236+
}
237+
238+
}
239+
240+
@Override
241+
public boolean equals(Object o) {
242+
if (this == o)
243+
return true;
244+
if (o == null || getClass() != o.getClass())
245+
return false;
246+
ResponseFormat that = (ResponseFormat) o;
247+
return type == that.type && Objects.equals(jsonSchema, that.jsonSchema);
248+
}
249+
250+
@Override
251+
public int hashCode() {
252+
return Objects.hash(type, jsonSchema);
253+
}
254+
255+
@Override
256+
public String toString() {
257+
return "ResponseFormat{" + "type=" + type + ", jsonSchema=" + jsonSchema + '}';
258+
}
259+
260+
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import org.springframework.ai.openai.OpenAiChatModel;
3434
import org.springframework.ai.openai.OpenAiChatOptions;
3535
import org.springframework.ai.openai.api.OpenAiApi;
36-
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
36+
import org.springframework.ai.openai.api.ResponseFormat;
3737
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
3838
import org.springframework.beans.factory.annotation.Autowired;
3939
import org.springframework.boot.SpringBootConfiguration;
@@ -80,7 +80,7 @@ void jsonObject() throws JsonMappingException, JsonProcessingException {
8080

8181
Prompt prompt = new Prompt("List 8 planets. Use JSON response",
8282
OpenAiChatOptions.builder()
83-
.withResponseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT))
83+
.withResponseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build())
8484
.build());
8585

8686
ChatResponse response = this.openAiChatModel.call(prompt);

0 commit comments

Comments
 (0)