Skip to content

Commit 9c06344

Browse files
authored
[Responses API] Structured output (#1586)
Built on top of #1576. Based on https://platform.openai.com/docs/guides/structured-outputs Works both with and without streaming. ## Non-stream **Run** ```bash pnpm run example structured_output ``` (which core logic is:) ```js (...) const response = await openai.responses.parse({ model: "Qwen/Qwen2.5-VL-72B-Instruct", provider: "nebius", input: [ { role: "system", content: "You are a helpful math tutor. Guide the user through the solution step by step.", }, { role: "user", content: "how can I solve 8x + 7 = -23" }, ], text: { format: zodTextFormat(MathReasoning, "math_reasoning"), }, }); (...) ``` **Output:** ```js { steps: [ { explanation: 'To solve for x, we need to isolate it on one side of the equation. We start by subtracting 7 from both sides of the equation.', output: '8x + 7 - 7 = -23 - 7' }, { explanation: 'Simplify the equation after performing the subtraction.', output: '8x = -30' }, { explanation: 'Now that we have isolated the term with x, we divide both sides by 8 to get x by itself.', output: '8x / 8 = -30 / 8' }, { explanation: 'Perform the division to find the value of x.', output: 'x = -30 / 8' }, { explanation: 'Simplify the fraction if possible.', output: 'x = -15 / 4' } ], final_answer: 'The solution is x = -15/4 or x = -3.75.' } ``` ## Stream **Run** ```bash pnpm run example structured_output_streaming ``` (which core logic is:) ```js const stream = openai.responses .stream({ model: "Qwen/Qwen2.5-VL-72B-Instruct", provider: "nebius", instructions: "Extract the event information.", input: "Alice and Bob are going to a science fair on Friday.", text: { format: zodTextFormat(CalendarEvent, "calendar_event"), }, }) .on("response.refusal.delta", (event) => { process.stdout.write(event.delta); }) .on("response.output_text.delta", (event) => { process.stdout.write(event.delta); }) .on("response.output_text.done", () => { process.stdout.write("\n"); }) .on("response.error", (event) => { console.error(event.error); }); const result = await stream.finalResponse(); console.log(result.output_parsed); ``` **Output:** ```js { "name": "Science Fair", "date": "Friday", "participants": ["Alice", "Bob"] } { name: 'Science Fair', date: 'Friday', participants: [ 'Alice', 'Bob' ] } ```
1 parent 69f0cd0 commit 9c06344

File tree

4 files changed

+113
-9
lines changed

4 files changed

+113
-9
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import OpenAI from "openai";
2+
import { zodTextFormat } from "openai/helpers/zod";
3+
import { z } from "zod";
4+
5+
const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN });
6+
7+
const Step = z.object({
8+
explanation: z.string(),
9+
output: z.string(),
10+
});
11+
12+
const MathReasoning = z.object({
13+
steps: z.array(Step),
14+
final_answer: z.string(),
15+
});
16+
17+
const response = await openai.responses.parse({
18+
model: "Qwen/Qwen2.5-VL-72B-Instruct",
19+
provider: "nebius",
20+
input: [
21+
{
22+
role: "system",
23+
content: "You are a helpful math tutor. Guide the user through the solution step by step.",
24+
},
25+
{ role: "user", content: "how can I solve 8x + 7 = -23" },
26+
],
27+
text: {
28+
format: zodTextFormat(MathReasoning, "math_reasoning"),
29+
},
30+
});
31+
32+
console.log(response.output_parsed);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { OpenAI } from "openai";
2+
import { zodTextFormat } from "openai/helpers/zod";
3+
import { z } from "zod";
4+
5+
const CalendarEvent = z.object({
6+
name: z.string(),
7+
date: z.string(),
8+
participants: z.array(z.string()),
9+
});
10+
11+
const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN });
12+
const stream = openai.responses
13+
.stream({
14+
model: "Qwen/Qwen2.5-VL-72B-Instruct",
15+
provider: "nebius",
16+
instructions: "Extract the event information.",
17+
input: "Alice and Bob are going to a science fair on Friday.",
18+
text: {
19+
format: zodTextFormat(CalendarEvent, "calendar_event"),
20+
},
21+
})
22+
.on("response.refusal.delta", (event) => {
23+
process.stdout.write(event.delta);
24+
})
25+
.on("response.output_text.delta", (event) => {
26+
process.stdout.write(event.delta);
27+
})
28+
.on("response.output_text.done", () => {
29+
process.stdout.write("\n");
30+
})
31+
.on("response.error", (event) => {
32+
console.error(event.error);
33+
});
34+
35+
const result = await stream.finalResponse();
36+
console.log(result.output_parsed);

packages/responses-server/src/routes/responses.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { type ValidatedRequest } from "../middleware/validation.js";
33
import { type CreateResponseParams } from "../schemas.js";
44
import { generateUniqueId } from "../lib/generateUniqueId.js";
55
import { InferenceClient } from "@huggingface/inference";
6-
import type { ChatCompletionInputMessage, ChatCompletionInputMessageChunkType } from "@huggingface/tasks";
6+
import type {
7+
ChatCompletionInputMessage,
8+
ChatCompletionInputMessageChunkType,
9+
ChatCompletionInput,
10+
} from "@huggingface/tasks";
711

812
import type {
913
Response,
@@ -69,13 +73,28 @@ export const postCreateResponse = async (
6973
messages.push({ role: "user", content: req.body.input });
7074
}
7175

72-
const payload = {
76+
const payload: ChatCompletionInput = {
7377
model: req.body.model,
78+
provider: req.body.provider,
7479
messages: messages,
7580
max_tokens: req.body.max_output_tokens === null ? undefined : req.body.max_output_tokens,
7681
temperature: req.body.temperature,
7782
top_p: req.body.top_p,
7883
stream: req.body.stream,
84+
response_format: req.body.text?.format
85+
? {
86+
type: req.body.text.format.type,
87+
json_schema:
88+
req.body.text.format.type === "json_schema"
89+
? {
90+
description: req.body.text.format.description,
91+
name: req.body.text.format.name,
92+
schema: req.body.text.format.schema,
93+
strict: req.body.text.format.strict,
94+
}
95+
: undefined,
96+
}
97+
: undefined,
7998
};
8099

81100
const responseObject: Omit<
@@ -225,12 +244,7 @@ export const postCreateResponse = async (
225244
}
226245

227246
try {
228-
const chatCompletionResponse = await client.chatCompletion({
229-
model: req.body.model,
230-
messages: messages,
231-
temperature: req.body.temperature,
232-
top_p: req.body.top_p,
233-
});
247+
const chatCompletionResponse = await client.chatCompletion(payload);
234248

235249
responseObject.status = "completed";
236250
responseObject.output = chatCompletionResponse.choices[0].message.content

packages/responses-server/src/schemas.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const createResponseParamsSchema = z.object({
8383
.nullable()
8484
.default(null),
8585
model: z.string(),
86+
provider: z.string().optional(),
8687
// previous_response_id: z.string().nullable().default(null),
8788
// reasoning: z.object({
8889
// effort: z.enum(["low", "medium", "high"]).default("medium"),
@@ -91,7 +92,28 @@ export const createResponseParamsSchema = z.object({
9192
// store: z.boolean().default(true),
9293
stream: z.boolean().default(false),
9394
temperature: z.number().min(0).max(2).default(1),
94-
// text:
95+
text: z
96+
.object({
97+
format: z.union([
98+
z.object({
99+
type: z.literal("text"),
100+
}),
101+
z.object({
102+
type: z.literal("json_object"),
103+
}),
104+
z.object({
105+
type: z.literal("json_schema"),
106+
name: z
107+
.string()
108+
.max(64, "Must be at most 64 characters")
109+
.regex(/^[a-zA-Z0-9_-]+$/, "Only letters, numbers, underscores, and dashes are allowed"),
110+
description: z.string().optional(),
111+
schema: z.record(z.any()),
112+
strict: z.boolean().default(false),
113+
}),
114+
]),
115+
})
116+
.optional(),
95117
// tool_choice:
96118
// tools:
97119
// top_logprobs: z.number().min(0).max(20).nullable().default(null),

0 commit comments

Comments
 (0)