feat(gateway): add OpenResponses /v1/responses endpoint

Add a new `/v1/responses` endpoint implementing the OpenResponses API
standard for agentic workflows. This provides:

- Item-based input (messages, function_call_output, reasoning)
- Semantic streaming events (response.created, response.output_text.delta,
  response.completed, etc.)
- Full SSE event support with both event: and data: lines
- Configuration via gateway.http.endpoints.responses.enabled

The endpoint is disabled by default and can be enabled independently
from the existing Chat Completions endpoint.

Phase 1 implementation supports:
- String or ItemParam[] input
- system/developer/user/assistant message roles
- function_call_output items
- instructions parameter
- Agent routing via headers or model parameter
- Session key management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ryan Lisse
2026-01-19 10:44:48 +01:00
committed by Peter Steinberger
parent 7f6e87e918
commit f4b03599f0
11 changed files with 1563 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
/**
* OpenResponses API Zod Schemas
*
* Zod schemas for the OpenResponses `/v1/responses` endpoint.
* This module is isolated from gateway imports to enable future codegen and prevent drift.
*
* @see https://www.open-responses.com/
*/
import { z } from "zod";
// ─────────────────────────────────────────────────────────────────────────────
// Content Parts
// ─────────────────────────────────────────────────────────────────────────────
export const InputTextContentPartSchema = z
.object({
type: z.literal("input_text"),
text: z.string(),
})
.strict();
export const OutputTextContentPartSchema = z
.object({
type: z.literal("output_text"),
text: z.string(),
})
.strict();
// For Phase 1, we reject image/file content with helpful errors
export const InputImageContentPartSchema = z
.object({
type: z.literal("input_image"),
})
.passthrough();
export const InputFileContentPartSchema = z
.object({
type: z.literal("input_file"),
})
.passthrough();
export const ContentPartSchema = z.discriminatedUnion("type", [
InputTextContentPartSchema,
OutputTextContentPartSchema,
InputImageContentPartSchema,
InputFileContentPartSchema,
]);
export type ContentPart = z.infer<typeof ContentPartSchema>;
// ─────────────────────────────────────────────────────────────────────────────
// Item Types (ItemParam)
// ─────────────────────────────────────────────────────────────────────────────
export const MessageItemRoleSchema = z.enum(["system", "developer", "user", "assistant"]);
export type MessageItemRole = z.infer<typeof MessageItemRoleSchema>;
export const MessageItemSchema = z
.object({
type: z.literal("message"),
role: MessageItemRoleSchema,
content: z.union([z.string(), z.array(ContentPartSchema)]),
})
.strict();
export const FunctionCallItemSchema = z
.object({
type: z.literal("function_call"),
id: z.string().optional(),
call_id: z.string().optional(),
name: z.string(),
arguments: z.string(),
})
.strict();
export const FunctionCallOutputItemSchema = z
.object({
type: z.literal("function_call_output"),
call_id: z.string(),
output: z.string(),
})
.strict();
export const ReasoningItemSchema = z
.object({
type: z.literal("reasoning"),
content: z.string().optional(),
encrypted_content: z.string().optional(),
summary: z.string().optional(),
})
.strict();
export const ItemReferenceItemSchema = z
.object({
type: z.literal("item_reference"),
id: z.string(),
})
.strict();
export const ItemParamSchema = z.discriminatedUnion("type", [
MessageItemSchema,
FunctionCallItemSchema,
FunctionCallOutputItemSchema,
ReasoningItemSchema,
ItemReferenceItemSchema,
]);
export type ItemParam = z.infer<typeof ItemParamSchema>;
// ─────────────────────────────────────────────────────────────────────────────
// Tool Definitions
// ─────────────────────────────────────────────────────────────────────────────
export const FunctionToolDefinitionSchema = z
.object({
type: z.literal("function"),
function: z.object({
name: z.string(),
description: z.string().optional(),
parameters: z.record(z.string(), z.unknown()).optional(),
}),
})
.strict();
export const ToolDefinitionSchema = FunctionToolDefinitionSchema;
export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
// ─────────────────────────────────────────────────────────────────────────────
// Request Body
// ─────────────────────────────────────────────────────────────────────────────
export const ToolChoiceSchema = z.union([
z.literal("auto"),
z.literal("none"),
z.literal("required"),
z.object({
type: z.literal("function"),
function: z.object({ name: z.string() }),
}),
]);
export const CreateResponseBodySchema = z
.object({
model: z.string(),
input: z.union([z.string(), z.array(ItemParamSchema)]),
instructions: z.string().optional(),
tools: z.array(ToolDefinitionSchema).optional(),
tool_choice: ToolChoiceSchema.optional(),
stream: z.boolean().optional(),
max_output_tokens: z.number().int().positive().optional(),
max_tool_calls: z.number().int().positive().optional(),
user: z.string().optional(),
// Phase 1: ignore but accept these fields
temperature: z.number().optional(),
top_p: z.number().optional(),
metadata: z.record(z.string(), z.string()).optional(),
store: z.boolean().optional(),
previous_response_id: z.string().optional(),
reasoning: z
.object({
effort: z.enum(["low", "medium", "high"]).optional(),
summary: z.enum(["auto", "concise", "detailed"]).optional(),
})
.optional(),
truncation: z.enum(["auto", "disabled"]).optional(),
})
.strict();
export type CreateResponseBody = z.infer<typeof CreateResponseBodySchema>;
// ─────────────────────────────────────────────────────────────────────────────
// Response Resource
// ─────────────────────────────────────────────────────────────────────────────
export const ResponseStatusSchema = z.enum([
"in_progress",
"completed",
"failed",
"cancelled",
"incomplete",
]);
export type ResponseStatus = z.infer<typeof ResponseStatusSchema>;
export const OutputItemSchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("message"),
id: z.string(),
role: z.literal("assistant"),
content: z.array(OutputTextContentPartSchema),
status: z.enum(["in_progress", "completed"]).optional(),
})
.strict(),
z
.object({
type: z.literal("function_call"),
id: z.string(),
call_id: z.string(),
name: z.string(),
arguments: z.string(),
status: z.enum(["in_progress", "completed"]).optional(),
})
.strict(),
z
.object({
type: z.literal("reasoning"),
id: z.string(),
content: z.string().optional(),
summary: z.string().optional(),
})
.strict(),
]);
export type OutputItem = z.infer<typeof OutputItemSchema>;
export const UsageSchema = z.object({
input_tokens: z.number().int().nonnegative(),
output_tokens: z.number().int().nonnegative(),
total_tokens: z.number().int().nonnegative(),
});
export type Usage = z.infer<typeof UsageSchema>;
export const ResponseResourceSchema = z.object({
id: z.string(),
object: z.literal("response"),
created_at: z.number().int(),
status: ResponseStatusSchema,
model: z.string(),
output: z.array(OutputItemSchema),
usage: UsageSchema,
// Optional fields for future phases
error: z
.object({
code: z.string(),
message: z.string(),
})
.optional(),
});
export type ResponseResource = z.infer<typeof ResponseResourceSchema>;
// ─────────────────────────────────────────────────────────────────────────────
// Streaming Event Types
// ─────────────────────────────────────────────────────────────────────────────
export const ResponseCreatedEventSchema = z.object({
type: z.literal("response.created"),
response: ResponseResourceSchema,
});
export const ResponseInProgressEventSchema = z.object({
type: z.literal("response.in_progress"),
response: ResponseResourceSchema,
});
export const ResponseCompletedEventSchema = z.object({
type: z.literal("response.completed"),
response: ResponseResourceSchema,
});
export const ResponseFailedEventSchema = z.object({
type: z.literal("response.failed"),
response: ResponseResourceSchema,
});
export const OutputItemAddedEventSchema = z.object({
type: z.literal("response.output_item.added"),
output_index: z.number().int().nonnegative(),
item: OutputItemSchema,
});
export const OutputItemDoneEventSchema = z.object({
type: z.literal("response.output_item.done"),
output_index: z.number().int().nonnegative(),
item: OutputItemSchema,
});
export const ContentPartAddedEventSchema = z.object({
type: z.literal("response.content_part.added"),
item_id: z.string(),
output_index: z.number().int().nonnegative(),
content_index: z.number().int().nonnegative(),
part: OutputTextContentPartSchema,
});
export const ContentPartDoneEventSchema = z.object({
type: z.literal("response.content_part.done"),
item_id: z.string(),
output_index: z.number().int().nonnegative(),
content_index: z.number().int().nonnegative(),
part: OutputTextContentPartSchema,
});
export const OutputTextDeltaEventSchema = z.object({
type: z.literal("response.output_text.delta"),
item_id: z.string(),
output_index: z.number().int().nonnegative(),
content_index: z.number().int().nonnegative(),
delta: z.string(),
});
export const OutputTextDoneEventSchema = z.object({
type: z.literal("response.output_text.done"),
item_id: z.string(),
output_index: z.number().int().nonnegative(),
content_index: z.number().int().nonnegative(),
text: z.string(),
});
export type StreamingEvent =
| z.infer<typeof ResponseCreatedEventSchema>
| z.infer<typeof ResponseInProgressEventSchema>
| z.infer<typeof ResponseCompletedEventSchema>
| z.infer<typeof ResponseFailedEventSchema>
| z.infer<typeof OutputItemAddedEventSchema>
| z.infer<typeof OutputItemDoneEventSchema>
| z.infer<typeof ContentPartAddedEventSchema>
| z.infer<typeof ContentPartDoneEventSchema>
| z.infer<typeof OutputTextDeltaEventSchema>
| z.infer<typeof OutputTextDoneEventSchema>;