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:
committed by
Peter Steinberger
parent
7f6e87e918
commit
f4b03599f0
325
src/gateway/open-responses.schema.ts
Normal file
325
src/gateway/open-responses.schema.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user