test(gateway): add OpenResponses parity E2E tests
- Add schema validation tests for input_image, input_file, client tools - Add buildAgentPrompt tests for turn-based tool flow
This commit is contained in:
committed by
Peter Steinberger
parent
a5afe7bc2b
commit
4f02c74dca
315
src/gateway/openresponses-parity.e2e.test.ts
Normal file
315
src/gateway/openresponses-parity.e2e.test.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* OpenResponses Feature Parity E2E Tests
|
||||
*
|
||||
* Tests for input_image, input_file, and client-side tools (Hosted Tools)
|
||||
* support in the OpenResponses `/v1/responses` endpoint.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("OpenResponses Feature Parity", () => {
|
||||
describe("Schema Validation", () => {
|
||||
it("should validate input_image with url source", async () => {
|
||||
const { InputImageContentPartSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validImage = {
|
||||
type: "input_image" as const,
|
||||
source: {
|
||||
type: "url" as const,
|
||||
url: "https://example.com/image.png",
|
||||
},
|
||||
};
|
||||
|
||||
const result = InputImageContentPartSchema.safeParse(validImage);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate input_image with base64 source", async () => {
|
||||
const { InputImageContentPartSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validImage = {
|
||||
type: "input_image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: "image/png" as const,
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
},
|
||||
};
|
||||
|
||||
const result = InputImageContentPartSchema.safeParse(validImage);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject input_image with invalid mime type", async () => {
|
||||
const { InputImageContentPartSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const invalidImage = {
|
||||
type: "input_image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: "application/json" as const, // Not an image
|
||||
data: "SGVsbG8gV29ybGQh",
|
||||
},
|
||||
};
|
||||
|
||||
const result = InputImageContentPartSchema.safeParse(invalidImage);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate input_file with url source", async () => {
|
||||
const { InputFileContentPartSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validFile = {
|
||||
type: "input_file" as const,
|
||||
source: {
|
||||
type: "url" as const,
|
||||
url: "https://example.com/document.txt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = InputFileContentPartSchema.safeParse(validFile);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate input_file with base64 source", async () => {
|
||||
const { InputFileContentPartSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validFile = {
|
||||
type: "input_file" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: "text/plain" as const,
|
||||
data: "SGVsbG8gV29ybGQh",
|
||||
filename: "hello.txt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = InputFileContentPartSchema.safeParse(validFile);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate tool definition", async () => {
|
||||
const { ToolDefinitionSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = ToolDefinitionSchema.safeParse(validTool);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject tool definition without name", async () => {
|
||||
const { ToolDefinitionSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const invalidTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "", // Empty name
|
||||
description: "Get the current weather",
|
||||
},
|
||||
};
|
||||
|
||||
const result = ToolDefinitionSchema.safeParse(invalidTool);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CreateResponseBody Schema", () => {
|
||||
it("should validate request with input_image", async () => {
|
||||
const { CreateResponseBodySchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validRequest = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
input: [
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: [
|
||||
{
|
||||
type: "input_image" as const,
|
||||
source: {
|
||||
type: "url" as const,
|
||||
url: "https://example.com/photo.jpg",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "input_text" as const,
|
||||
text: "What's in this image?",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = CreateResponseBodySchema.safeParse(validRequest);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate request with client tools", async () => {
|
||||
const { CreateResponseBodySchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validRequest = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
input: [
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: "What's the weather?",
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = CreateResponseBodySchema.safeParse(validRequest);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate request with function_call_output for turn-based tools", async () => {
|
||||
const { CreateResponseBodySchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const validRequest = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
input: [
|
||||
{
|
||||
type: "function_call_output" as const,
|
||||
call_id: "call_123",
|
||||
output: '{"temperature": "72°F", "condition": "sunny"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = CreateResponseBodySchema.safeParse(validRequest);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate complete turn-based tool flow", async () => {
|
||||
const { CreateResponseBodySchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const turn1Request = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
input: [
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: "What's the weather in San Francisco?",
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const turn1Result = CreateResponseBodySchema.safeParse(turn1Request);
|
||||
expect(turn1Result.success).toBe(true);
|
||||
|
||||
// Turn 2: Client provides tool output
|
||||
const turn2Request = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
input: [
|
||||
{
|
||||
type: "function_call_output" as const,
|
||||
call_id: "call_123",
|
||||
output: '{"temperature": "72°F", "condition": "sunny"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const turn2Result = CreateResponseBodySchema.safeParse(turn2Request);
|
||||
expect(turn2Result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response Resource Schema", () => {
|
||||
it("should validate response with function_call output", async () => {
|
||||
const { OutputItemSchema } = await import("./open-responses.schema.js");
|
||||
|
||||
const functionCallOutput = {
|
||||
type: "function_call" as const,
|
||||
id: "msg_123",
|
||||
call_id: "call_456",
|
||||
name: "get_weather",
|
||||
arguments: '{"location": "San Francisco"}',
|
||||
};
|
||||
|
||||
const result = OutputItemSchema.safeParse(functionCallOutput);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentPrompt", () => {
|
||||
it("should convert function_call_output to tool entry", async () => {
|
||||
const { buildAgentPrompt } = await import("./openresponses-http.js");
|
||||
|
||||
const result = buildAgentPrompt([
|
||||
{
|
||||
type: "function_call_output" as const,
|
||||
call_id: "call_123",
|
||||
output: '{"temperature": "72°F"}',
|
||||
},
|
||||
]);
|
||||
|
||||
// When there's only a tool output (no history), returns just the body
|
||||
expect(result.message).toBe('{"temperature": "72°F"}');
|
||||
});
|
||||
|
||||
it("should handle mixed message and function_call_output items", async () => {
|
||||
const { buildAgentPrompt } = await import("./openresponses-http.js");
|
||||
|
||||
const result = buildAgentPrompt([
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: "What's the weather?",
|
||||
},
|
||||
{
|
||||
type: "function_call_output" as const,
|
||||
call_id: "call_123",
|
||||
output: '{"temperature": "72°F"}',
|
||||
},
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: "Thanks!",
|
||||
},
|
||||
]);
|
||||
|
||||
// Should include both user messages and tool output
|
||||
expect(result.message).toContain("weather");
|
||||
expect(result.message).toContain("72°F");
|
||||
expect(result.message).toContain("Thanks");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user