Skip to content
This repository was archived by the owner on Sep 29, 2025. It is now read-only.

Commit d289b2f

Browse files
authored
Responses api - allow input_text objects in input (#849)
* allow input content to be an array with an object as well as a string * add tests for createResponse service * add test for new input_text object type to chatbot-server-public * update zod schema refine checks * update check for userMessage to be more robust * type cleanup * cleanup responses test
1 parent a34a582 commit d289b2f

File tree

3 files changed

+121
-40
lines changed

3 files changed

+121
-40
lines changed

packages/chatbot-server-mongodb-public/src/responsesApi.test.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ describe("Responses API with OpenAI Client", () => {
8585
await expectValidResponses({ stream, requestBody });
8686
});
8787

88+
it("Should return responses given a message array with input_text content type", async () => {
89+
const requestBody: Partial<CreateResponseRequest["body"]> = {
90+
input: [
91+
{
92+
role: "user",
93+
content: [{ type: "input_text", text: "What is MongoDB?" }],
94+
},
95+
],
96+
};
97+
const stream = await createResponseRequestStream(requestBody);
98+
99+
await expectValidResponses({ stream, requestBody });
100+
});
101+
88102
it("Should return responses given a valid request with instructions", async () => {
89103
const requestBody: Partial<CreateResponseRequest["body"]> = {
90104
instructions: "You are a helpful chatbot.",
@@ -184,24 +198,16 @@ describe("Responses API with OpenAI Client", () => {
184198
const stream = createResponseRequestStream({
185199
input: "",
186200
});
187-
expectInvalidResponses({
201+
202+
await expectInvalidResponses({
188203
stream,
189204
errorMessage: "Input must be a non-empty string",
190205
});
191206
});
192-
it("Should return error responses if empty message array", async () => {
193-
const stream = createResponseRequestStream({
194-
input: [],
195-
});
196-
expectInvalidResponses({
197-
stream,
198-
errorMessage:
199-
"Path: body.input - Input must be a string or array of messages. See https://platform.openai.com/docs/api-reference/responses/create#responses-create-input for more information.",
200-
});
201-
});
202207

203208
it("Should return error responses if empty message array", async () => {
204209
const stream = createResponseRequestStream({
210+
// @ts-expect-error - empty array is valid input
205211
input: [],
206212
});
207213

packages/mongodb-chatbot-server/src/routes/responses/createResponse.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,20 @@ describe("POST /responses", () => {
271271
await expectValidResponses({ requestBody, stream });
272272
});
273273

274+
it("Should return responses given a message array with input_text content type", async () => {
275+
const requestBody: Partial<CreateResponseRequest["body"]> = {
276+
input: [
277+
{
278+
role: "user",
279+
content: [{ type: "input_text", text: "What is MongoDB?" }],
280+
},
281+
],
282+
};
283+
const stream = await makeClientAndRequest(requestBody);
284+
285+
await expectValidResponses({ requestBody, stream });
286+
});
287+
274288
it("Should store conversation messages if `storeMessageContent: undefined` and `store: true`", async () => {
275289
const storeMessageContent = undefined;
276290
const initialMessages: Array<SomeMessage> = [
@@ -433,7 +447,8 @@ describe("POST /responses", () => {
433447

434448
it("Should return error responses if empty message array", async () => {
435449
const stream = await makeClientAndRequest({
436-
input: [],
450+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
451+
input: [] as any,
437452
});
438453

439454
await expectInvalidResponses({
@@ -701,6 +716,41 @@ describe("POST /responses", () => {
701716
message: ERR_MSG.CONVERSATION_STORE_MISMATCH,
702717
});
703718
});
719+
720+
it("Should return error responses if input content array has invalid type", async () => {
721+
const stream = await makeClientAndRequest({
722+
input: [
723+
{
724+
role: "user",
725+
content: [{ type: "input_image" as any, text: "What is MongoDB?" }],
726+
},
727+
],
728+
});
729+
730+
await expectInvalidResponses({
731+
stream,
732+
message: "Path: body.input - Invalid input",
733+
});
734+
});
735+
736+
it("Should return error responses if input content array has more than one input_text element", async () => {
737+
const stream = await makeClientAndRequest({
738+
input: [
739+
{
740+
role: "user",
741+
content: [
742+
{ type: "input_text", text: "What is MongoDB?" },
743+
{ type: "input_text", text: "Tell me more about it." },
744+
],
745+
},
746+
],
747+
});
748+
749+
await expectInvalidResponses({
750+
stream,
751+
message: `Path: body.input[0].content - ${ERR_MSG.INPUT_TEXT_ARRAY}`,
752+
});
753+
});
704754
});
705755
});
706756

packages/mongodb-chatbot-server/src/routes/responses/createResponse.ts

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
type ResponseStreamInProgress,
1212
type ResponseStreamCompleted,
1313
type ResponseStreamError,
14-
type UserMessage,
1514
makeDataStreamer,
1615
} from "mongodb-rag-core";
1716
import { SomeExpressRequest } from "../../middleware";
@@ -30,6 +29,8 @@ export const ERR_MSG = {
3029
INPUT_STRING: "Input must be a non-empty string",
3130
INPUT_ARRAY:
3231
"Input must be a string or array of messages. See https://platform.openai.com/docs/api-reference/responses/create#responses-create-input for more information.",
32+
INPUT_TEXT_ARRAY:
33+
'Input content array only supports "input_text" type with exactly one element.',
3334
CONVERSATION_USER_ID_CHANGED:
3435
"Path: body.user - User ID has changed since the conversation was created.",
3536
METADATA_LENGTH: "Too many metadata fields. Max 16.",
@@ -53,19 +54,34 @@ export const ERR_MSG = {
5354
"Path: body.previous_response_id | body.store - the conversation store flag does not match the store flag provided",
5455
};
5556

57+
const InputMessageSchema = z.object({
58+
type: z.literal("message").optional(),
59+
role: z.enum(["user", "assistant", "system"]),
60+
content: z.union([
61+
z.string(),
62+
z
63+
.array(
64+
z.object({
65+
type: z.literal("input_text"),
66+
text: z.string(),
67+
})
68+
)
69+
.length(1, ERR_MSG.INPUT_TEXT_ARRAY),
70+
]),
71+
});
72+
73+
type InputMessage = z.infer<typeof InputMessageSchema>;
74+
type UserMessage = Omit<InputMessage, "role"> & { role: "user" };
75+
5676
const CreateResponseRequestBodySchema = z.object({
5777
model: z.string(),
5878
instructions: z.string().optional(),
5979
input: z.union([
60-
z.string().refine((input) => input.length > 0, ERR_MSG.INPUT_STRING),
80+
z.string().nonempty(ERR_MSG.INPUT_STRING),
6181
z
6282
.array(
6383
z.union([
64-
z.object({
65-
type: z.literal("message").optional(),
66-
role: z.enum(["user", "assistant", "system"]),
67-
content: z.string(),
68-
}),
84+
InputMessageSchema,
6985
// function tool call
7086
z.object({
7187
type: z.literal("function_call"),
@@ -97,7 +113,7 @@ const CreateResponseRequestBodySchema = z.object({
97113
}),
98114
])
99115
)
100-
.refine((input) => input.length > 0, ERR_MSG.INPUT_ARRAY),
116+
.nonempty(ERR_MSG.INPUT_ARRAY),
101117
]),
102118
max_output_tokens: z.number().min(0).default(1000),
103119
metadata: z
@@ -114,15 +130,15 @@ const CreateResponseRequestBodySchema = z.object({
114130
store: z
115131
.boolean()
116132
.optional()
117-
.describe("Whether to store the response in the conversation.")
118-
.default(true),
133+
.default(true)
134+
.describe("Whether to store the response in the conversation."),
119135
stream: z.boolean().refine((stream) => stream, ERR_MSG.STREAM),
120136
temperature: z
121137
.number()
122-
.refine((temperature) => temperature === 0, ERR_MSG.TEMPERATURE)
123138
.optional()
124-
.describe("Temperature for the model. Defaults to 0.")
125-
.default(0),
139+
.default(0)
140+
.refine((temperature) => temperature === 0, ERR_MSG.TEMPERATURE)
141+
.describe("Temperature for the model. Defaults to 0."),
126142
tool_choice: z
127143
.union([
128144
z.enum(["none", "auto", "required"]),
@@ -134,8 +150,8 @@ const CreateResponseRequestBodySchema = z.object({
134150
.describe("Function tool choice"),
135151
])
136152
.optional()
137-
.describe("Tool choice for the model. Defaults to 'auto'.")
138-
.default("auto"),
153+
.default("auto")
154+
.describe("Tool choice for the model. Defaults to 'auto'."),
139155
tools: z
140156
.array(
141157
z.object({
@@ -491,10 +507,14 @@ const convertInputToDBMessages = (
491507
}
492508

493509
return input.map((message) => {
494-
// handle function tool calls and outputs
495-
const role = message.type === "message" ? message.role : "system";
496-
const content =
497-
message.type === "message" ? message.content : message.type ?? "";
510+
// set default role and content for function tool calls and outputs
511+
let role: MessagesParam[number]["role"] = "system";
512+
let content = message.type ?? "";
513+
// set role and content for all other messages
514+
if (message.type === "message") {
515+
role = message.role;
516+
content = formatUserMessageContent(message.content);
517+
}
498518

499519
return formatMessage({ role, content }, store, metadata);
500520
});
@@ -559,19 +579,24 @@ const convertInputToLatestMessageText = (
559579
return input;
560580
}
561581

562-
const lastUserMessageString = input.findLast(
563-
(message): message is UserMessage =>
564-
(message.type === "message" || !message.type) &&
565-
message.role === "user" &&
566-
!!message.content
567-
)?.content;
568-
569-
if (!lastUserMessageString) {
582+
const lastUserMessage = input.findLast(isUserMessage);
583+
if (!lastUserMessage) {
570584
throw makeBadRequestError({
571585
error: new Error("No user message found in input"),
572586
headers,
573587
});
574588
}
575589

576-
return lastUserMessageString;
590+
return formatUserMessageContent(lastUserMessage.content);
591+
};
592+
593+
const isInputMessage = (message: unknown): message is InputMessage =>
594+
InputMessageSchema.safeParse(message).success;
595+
596+
const isUserMessage = (message: unknown): message is UserMessage =>
597+
isInputMessage(message) && message.role === "user";
598+
599+
const formatUserMessageContent = (content: InputMessage["content"]): string => {
600+
if (typeof content === "string") return content;
601+
return content[0].text;
577602
};

0 commit comments

Comments
 (0)