0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
710 lines
21 KiB
TypeScript
710 lines
21 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ResponsesProcessor } from '../../../src/providers/responses/processor';
|
|
|
|
// Mock dependencies
|
|
vi.mock('../../../src/providers/functionCallbackUtils');
|
|
|
|
const mockFunctionCallbackHandler = {
|
|
processCalls: vi.fn(),
|
|
} as any;
|
|
|
|
const mockCostCalculator = vi.fn().mockReturnValue(0.001);
|
|
|
|
describe('ResponsesProcessor', () => {
|
|
let processor: ResponsesProcessor;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
processor = new ResponsesProcessor({
|
|
modelName: 'gpt-4.1',
|
|
providerType: 'openai',
|
|
functionCallbackHandler: mockFunctionCallbackHandler,
|
|
costCalculator: mockCostCalculator,
|
|
});
|
|
});
|
|
|
|
describe('processResponseOutput', () => {
|
|
it('should process simple text output', async () => {
|
|
const mockData = {
|
|
id: 'resp_test123',
|
|
model: 'gpt-5-2025-08-07',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Hello, world!' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 5, output_tokens: 3 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('Hello, world!');
|
|
expect(result.cached).toBe(false);
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_test123',
|
|
model: 'gpt-5-2025-08-07',
|
|
});
|
|
});
|
|
|
|
it('should include Azure-style output token details in token usage', async () => {
|
|
const mockData = {
|
|
id: 'resp_usage_details',
|
|
model: 'gpt-5-2025-08-07',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Hello with usage details' }],
|
|
},
|
|
],
|
|
usage: {
|
|
input_tokens: 11,
|
|
output_tokens: 7,
|
|
total_tokens: 21,
|
|
cache_write_input_tokens: 2,
|
|
input_tokens_details: {
|
|
cached_tokens: 3,
|
|
},
|
|
output_tokens_details: {
|
|
reasoning_tokens: 4,
|
|
accepted_prediction_tokens: 2,
|
|
rejected_prediction_tokens: 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.tokenUsage).toEqual({
|
|
total: 21,
|
|
prompt: 11,
|
|
completion: 7,
|
|
numRequests: 1,
|
|
completionDetails: {
|
|
reasoning: 4,
|
|
acceptedPrediction: 2,
|
|
rejectedPrediction: 1,
|
|
cacheReadInputTokens: 3,
|
|
cacheCreationInputTokens: 2,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should process function calls', async () => {
|
|
mockFunctionCallbackHandler.processCalls.mockResolvedValue('Function executed successfully');
|
|
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'function_call',
|
|
name: 'test_function',
|
|
arguments: '{"param": "value"}',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
usage: { input_tokens: 10, output_tokens: 8 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('Function executed successfully');
|
|
expect(mockFunctionCallbackHandler.processCalls).toHaveBeenCalledWith(
|
|
mockData.output[0],
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('should handle empty function arguments correctly', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'function_call',
|
|
name: 'empty_function',
|
|
arguments: '{}',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
usage: { input_tokens: 8, output_tokens: 5 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('no_arguments_provided');
|
|
expect(result.output).toContain('empty_function');
|
|
// Should not call the function callback handler for empty args
|
|
expect(mockFunctionCallbackHandler.processCalls).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should process reasoning output', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'reasoning',
|
|
summary: [
|
|
{ text: 'Step 1: Analyze the problem' },
|
|
{ text: 'Step 2: Find the solution' },
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 15, output_tokens: 20 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('Reasoning: Step 1: Analyze the problem');
|
|
expect(result.output).toContain('Step 2: Find the solution');
|
|
});
|
|
|
|
it('should suppress reasoning output when requested', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'reasoning',
|
|
summary: [{ text: 'Internal reasoning summary' }],
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Final answer' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 15, output_tokens: 20 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false, {
|
|
suppressReasoningOutput: true,
|
|
});
|
|
|
|
expect(result.output).toBe('Final answer');
|
|
expect(result.output).not.toContain('Reasoning:');
|
|
expect(result.output).not.toContain('Internal reasoning summary');
|
|
});
|
|
|
|
it('should process web search calls', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'web_search_call',
|
|
action: { type: 'search', query: 'test query' },
|
|
status: 'completed',
|
|
},
|
|
{
|
|
type: 'web_search_call',
|
|
action: { type: 'open_page', url: 'https://example.com' },
|
|
status: 'completed',
|
|
},
|
|
],
|
|
usage: { input_tokens: 12, output_tokens: 15 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('Web Search: "test query"');
|
|
expect(result.output).toContain('Opening page: https://example.com');
|
|
});
|
|
|
|
it('should process code interpreter calls', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'code_interpreter_call',
|
|
code: 'print("Hello, world!")',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
usage: { input_tokens: 8, output_tokens: 6 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('Code Interpreter: print("Hello, world!")');
|
|
});
|
|
|
|
it('should process MCP tool calls', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'mcp_list_tools',
|
|
server_label: 'test_server',
|
|
tools: [{ name: 'test_tool' }],
|
|
},
|
|
{
|
|
type: 'mcp_call',
|
|
name: 'test_tool',
|
|
server_label: 'test_server',
|
|
output: 'Tool result',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
usage: { input_tokens: 18, output_tokens: 25 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('MCP Tools from test_server');
|
|
expect(result.output).toContain('MCP Tool Result (test_tool): Tool result');
|
|
});
|
|
|
|
it('should handle refusals correctly', async () => {
|
|
const mockData = {
|
|
id: 'resp_refusal456',
|
|
model: 'gpt-4.1',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'refusal',
|
|
refusal: 'I cannot help with that request.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 10, output_tokens: 8 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('I cannot help with that request.');
|
|
expect(result.isRefusal).toBe(true);
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_refusal456',
|
|
model: 'gpt-4.1',
|
|
});
|
|
});
|
|
|
|
it('should handle mixed response types', async () => {
|
|
mockFunctionCallbackHandler.processCalls.mockResolvedValue('Function result');
|
|
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'reasoning',
|
|
summary: [{ text: 'Let me think about this...' }],
|
|
},
|
|
{
|
|
type: 'function_call',
|
|
name: 'search',
|
|
arguments: '{"query": "test"}',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Here is the result.' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 25, output_tokens: 30 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toContain('Reasoning: Let me think about this');
|
|
expect(result.output).toContain('Function result');
|
|
expect(result.output).toContain('Here is the result');
|
|
});
|
|
|
|
it('should handle JSON schema parsing', async () => {
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: '{"result": "success"}' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 12, output_tokens: 8 },
|
|
};
|
|
|
|
const requestConfig = {
|
|
response_format: { type: 'json_schema' },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, requestConfig, false);
|
|
|
|
expect(result.output).toEqual({ result: 'success' });
|
|
});
|
|
|
|
it('should handle errors gracefully', async () => {
|
|
const mockData = {
|
|
output: 'invalid output format',
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.error).toContain('Invalid response format: Missing output array');
|
|
});
|
|
|
|
it('should omit cost when the calculator cannot price the response', async () => {
|
|
mockCostCalculator.mockReturnValueOnce(undefined);
|
|
const mockData = {
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Unpriced response' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 4, output_tokens: 2 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('Unpriced response');
|
|
expect(result).not.toHaveProperty('cost');
|
|
});
|
|
|
|
it('should preserve annotations for deep research', async () => {
|
|
const mockData = {
|
|
id: 'resp_research789',
|
|
model: 'o3-deep-research',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'Research result',
|
|
annotations: [{ citation: 'Source 1' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 15, output_tokens: 10 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('Research result');
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_research789',
|
|
model: 'o3-deep-research',
|
|
annotations: [{ citation: 'Source 1' }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Provider-specific behavior', () => {
|
|
it('should work with Azure provider configuration', async () => {
|
|
const azureProcessor = new ResponsesProcessor({
|
|
modelName: 'gpt-4.1-deployment',
|
|
providerType: 'azure',
|
|
functionCallbackHandler: mockFunctionCallbackHandler,
|
|
costCalculator: mockCostCalculator,
|
|
});
|
|
|
|
const mockData = {
|
|
id: 'resp_azure001',
|
|
model: 'gpt-4.1-deployment',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Azure response' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 8, output_tokens: 6 },
|
|
};
|
|
|
|
const result = await azureProcessor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.output).toBe('Azure response');
|
|
expect(mockCostCalculator).toHaveBeenCalledWith('gpt-4.1-deployment', mockData.usage, {});
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_azure001',
|
|
model: 'gpt-4.1-deployment',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Metadata extraction', () => {
|
|
it('should extract responseId and model from response data', async () => {
|
|
const mockData = {
|
|
id: 'resp_metadata123',
|
|
model: 'gpt-5-mini',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Response with metadata' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 10, output_tokens: 5 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toHaveProperty('responseId', 'resp_metadata123');
|
|
expect(result.metadata).toHaveProperty('model', 'gpt-5-mini');
|
|
});
|
|
|
|
it('should handle missing id and model gracefully', async () => {
|
|
const mockData = {
|
|
// No id or model fields
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Response without metadata' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 8, output_tokens: 4 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toEqual({});
|
|
});
|
|
|
|
it('should include only available metadata fields', async () => {
|
|
const mockData = {
|
|
id: 'resp_partial789',
|
|
// No model field
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Partial metadata' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 5, output_tokens: 3 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_partial789',
|
|
});
|
|
expect(result.metadata).not.toHaveProperty('model');
|
|
});
|
|
|
|
it('should combine metadata with annotations for deep research', async () => {
|
|
const mockData = {
|
|
id: 'resp_combined999',
|
|
model: 'o4-mini-deep-research',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'Deep research with citations',
|
|
annotations: [{ url: 'https://example.com', title: 'Example Source' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 20, output_tokens: 15 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_combined999',
|
|
model: 'o4-mini-deep-research',
|
|
annotations: [{ url: 'https://example.com', title: 'Example Source' }],
|
|
});
|
|
});
|
|
|
|
it('should include only model when id is missing', async () => {
|
|
const mockData = {
|
|
// No id field
|
|
model: 'gpt-4o-2024-11-20',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Response without id' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 5, output_tokens: 3 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toEqual({
|
|
model: 'gpt-4o-2024-11-20',
|
|
});
|
|
expect(result.metadata).not.toHaveProperty('responseId');
|
|
});
|
|
|
|
it('should handle multiple annotations from deep research', async () => {
|
|
const mockData = {
|
|
id: 'resp_multi123',
|
|
model: 'o3-deep-research',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'First citation',
|
|
annotations: [
|
|
{ url: 'https://source1.com', title: 'Source 1' },
|
|
{ url: 'https://source2.com', title: 'Source 2' },
|
|
],
|
|
},
|
|
{
|
|
type: 'output_text',
|
|
text: 'Second citation',
|
|
annotations: [{ url: 'https://source3.com', title: 'Source 3' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 30, output_tokens: 20 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toHaveProperty('responseId', 'resp_multi123');
|
|
expect(result.metadata).toHaveProperty('model', 'o3-deep-research');
|
|
expect(result.metadata?.annotations).toHaveLength(3);
|
|
expect(result.metadata?.annotations).toEqual([
|
|
{ url: 'https://source1.com', title: 'Source 1' },
|
|
{ url: 'https://source2.com', title: 'Source 2' },
|
|
{ url: 'https://source3.com', title: 'Source 3' },
|
|
]);
|
|
});
|
|
|
|
it('should not include annotations field when empty array', async () => {
|
|
const mockData = {
|
|
id: 'resp_noannotations',
|
|
model: 'gpt-4.1',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'No citations',
|
|
annotations: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 10, output_tokens: 5 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_noannotations',
|
|
model: 'gpt-4.1',
|
|
});
|
|
expect(result.metadata).not.toHaveProperty('annotations');
|
|
});
|
|
|
|
it('should handle empty string values gracefully', async () => {
|
|
const mockData = {
|
|
id: '',
|
|
model: '',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Empty strings' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 5, output_tokens: 3 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
// Empty strings are falsy, so they should not be included
|
|
expect(result.metadata).toEqual({});
|
|
});
|
|
|
|
it('should add annotations to both raw and metadata for backwards compatibility', async () => {
|
|
const mockData = {
|
|
id: 'resp_compat123',
|
|
model: 'o4-mini-deep-research',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'Research with citations',
|
|
annotations: [{ url: 'https://example.com', title: 'Example' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 15, output_tokens: 10 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
// Annotations should be in metadata (new behavior)
|
|
expect(result.metadata?.annotations).toEqual([
|
|
{ url: 'https://example.com', title: 'Example' },
|
|
]);
|
|
|
|
// Annotations should also be in raw for backwards compatibility
|
|
expect(result.raw).toHaveProperty('annotations');
|
|
expect(result.raw.annotations).toEqual([{ url: 'https://example.com', title: 'Example' }]);
|
|
});
|
|
|
|
it('should handle non-string id and model gracefully', async () => {
|
|
const mockData = {
|
|
id: { nested: 'object' }, // Wrong type - should be string
|
|
model: 12345, // Wrong type - should be string
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [{ type: 'output_text', text: 'Response with invalid types' }],
|
|
},
|
|
],
|
|
usage: { input_tokens: 5, output_tokens: 3 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
// Should exclude invalid types from metadata
|
|
expect(result.metadata).toEqual({});
|
|
});
|
|
|
|
it('should handle non-array annotations gracefully', async () => {
|
|
const mockData = {
|
|
id: 'resp_invalidannot',
|
|
model: 'gpt-4o',
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: 'Response with invalid annotations',
|
|
annotations: 'not-an-array', // Wrong type - should be array
|
|
},
|
|
],
|
|
},
|
|
],
|
|
usage: { input_tokens: 10, output_tokens: 5 },
|
|
};
|
|
|
|
const result = await processor.processResponseOutput(mockData, {}, false);
|
|
|
|
// Should include id and model but exclude invalid annotations
|
|
expect(result.metadata).toEqual({
|
|
responseId: 'resp_invalidannot',
|
|
model: 'gpt-4o',
|
|
});
|
|
expect(result.metadata).not.toHaveProperty('annotations');
|
|
});
|
|
});
|
|
});
|