Files
clawdbot/src/gateway/open-responses.schema.ts
Ryan Lisse a5afe7bc2b feat(gateway): implement OpenResponses /v1/responses endpoint phase 2
- Add input_image and input_file support with SSRF protection
- Add client-side tools (Hosted Tools) support
- Add turn-based tool flow with function_call_output handling
- Export buildAgentPrompt for testing
2026-01-20 07:37:01 +00:00

355 lines
12 KiB
TypeScript

/**
* 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();
// OpenResponses Image Content: Supports URL or base64 sources
export const InputImageSourceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("url"),
url: z.string().url(),
}),
z.object({
type: z.literal("base64"),
media_type: z.enum(["image/jpeg", "image/png", "image/gif", "image/webp"]),
data: z.string().min(1), // base64-encoded
}),
]);
export const InputImageContentPartSchema = z
.object({
type: z.literal("input_image"),
source: InputImageSourceSchema,
})
.strict();
// OpenResponses File Content: Supports URL or base64 sources
export const InputFileSourceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("url"),
url: z.string().url(),
}),
z.object({
type: z.literal("base64"),
media_type: z.string().min(1), // MIME type
data: z.string().min(1), // base64-encoded
filename: z.string().optional(),
}),
]);
export const InputFileContentPartSchema = z
.object({
type: z.literal("input_file"),
source: InputFileSourceSchema,
})
.strict();
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().min(1, "Tool name cannot be empty"),
description: z.string().optional(),
parameters: z.record(z.string(), z.unknown()).optional(),
}),
})
.strict();
// OpenResponses tool definitions match internal ToolDefinition structure
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>;