diff --git a/src/lib/responses/ResponseInputItems.ts b/src/lib/responses/ResponseInputItems.ts new file mode 100644 index 000000000..e26e08121 --- /dev/null +++ b/src/lib/responses/ResponseInputItems.ts @@ -0,0 +1,70 @@ +import type { + ResponseFunctionShellCallOutputContent, + ResponseInputItem, + ResponseOutputItem, +} from '../../resources/responses/responses'; + +export type ResponseInputItemLike = ResponseInputItem | ResponseOutputItem; + +type ResponseShellCallOutputInputItem = Extract; + +/** + * Normalizes a mixed array of stored response history items into clean + * `ResponseInputItem`s that can be sent back to `responses.create()`. + */ +export function toResponseInputItems(items: Iterable): ResponseInputItem[] { + return Array.from(items, toResponseInputItem); +} + +/** + * Normalizes a stored response history item into a clean `ResponseInputItem`. + */ +export function toResponseInputItem(item: ResponseInputItemLike): ResponseInputItem { + switch (item.type) { + case 'apply_patch_call': { + return stripCreatedBy(item); + } + + case 'apply_patch_call_output': { + return stripCreatedBy(item); + } + + case 'compaction': { + return stripCreatedBy(item); + } + + case 'shell_call': { + return stripCreatedBy(item); + } + + case 'shell_call_output': { + const output: ResponseShellCallOutputInputItem['output'] = item.output.map( + (chunk) => stripCreatedBy(chunk) as ResponseFunctionShellCallOutputContent, + ); + return { + ...(stripCreatedBy(item) as ResponseShellCallOutputInputItem), + output, + }; + } + + case 'tool_search_call': { + return stripCreatedBy(item); + } + + case 'tool_search_output': { + return stripCreatedBy(item); + } + + default: + return item; + } +} + +function stripCreatedBy(item: T): T { + if (!('created_by' in item)) { + return item; + } + + const { created_by: _createdBy, ...rest } = item as T & { created_by?: string }; + return rest as T; +} diff --git a/tests/lib/ResponseInputItems.test.ts b/tests/lib/ResponseInputItems.test.ts new file mode 100644 index 000000000..29178a040 --- /dev/null +++ b/tests/lib/ResponseInputItems.test.ts @@ -0,0 +1,143 @@ +import { toResponseInputItems } from 'openai/lib/responses/ResponseInputItems'; +import type { ResponseInputItem, ResponseOutputItem } from 'openai/resources/responses'; + +describe('toResponseInputItems', () => { + test('normalizes mixed response history items', () => { + const history: Array = [ + { + type: 'function_call_output', + call_id: 'function_call_123', + output: 'done', + }, + { + type: 'tool_search_call', + id: 'tool_search_item_123', + call_id: 'tool_search_call_123', + arguments: { query: 'schema' }, + execution: 'server', + status: 'completed', + created_by: 'assistant', + }, + { + type: 'compaction', + id: 'compaction_123', + encrypted_content: 'encrypted', + created_by: 'assistant', + }, + { + type: 'shell_call', + id: 'shell_call_123', + call_id: 'shell_call_123', + action: { + commands: ['pwd'], + max_output_length: 512, + timeout_ms: 1000, + }, + environment: null, + status: 'completed', + created_by: 'assistant', + }, + { + type: 'shell_call_output', + id: 'shell_call_output_123', + call_id: 'shell_call_123', + max_output_length: 512, + output: [ + { + stdout: '/workspace\n', + stderr: '', + outcome: { type: 'exit', exit_code: 0 }, + created_by: 'assistant', + }, + ], + status: 'completed', + created_by: 'assistant', + }, + { + type: 'apply_patch_call', + id: 'apply_patch_call_123', + call_id: 'apply_patch_call_123', + operation: { + type: 'create_file', + path: 'notes.txt', + diff: 'hello', + }, + status: 'completed', + created_by: 'assistant', + }, + { + type: 'apply_patch_call_output', + id: 'apply_patch_call_output_123', + call_id: 'apply_patch_call_123', + output: 'created notes.txt', + status: 'completed', + created_by: 'assistant', + }, + ]; + + expect(toResponseInputItems(history)).toEqual([ + { + type: 'function_call_output', + call_id: 'function_call_123', + output: 'done', + }, + { + type: 'tool_search_call', + id: 'tool_search_item_123', + call_id: 'tool_search_call_123', + arguments: { query: 'schema' }, + execution: 'server', + status: 'completed', + }, + { + type: 'compaction', + id: 'compaction_123', + encrypted_content: 'encrypted', + }, + { + type: 'shell_call', + id: 'shell_call_123', + call_id: 'shell_call_123', + action: { + commands: ['pwd'], + max_output_length: 512, + timeout_ms: 1000, + }, + environment: null, + status: 'completed', + }, + { + type: 'shell_call_output', + id: 'shell_call_output_123', + call_id: 'shell_call_123', + max_output_length: 512, + output: [ + { + stdout: '/workspace\n', + stderr: '', + outcome: { type: 'exit', exit_code: 0 }, + }, + ], + status: 'completed', + }, + { + type: 'apply_patch_call', + id: 'apply_patch_call_123', + call_id: 'apply_patch_call_123', + operation: { + type: 'create_file', + path: 'notes.txt', + diff: 'hello', + }, + status: 'completed', + }, + { + type: 'apply_patch_call_output', + id: 'apply_patch_call_output_123', + call_id: 'apply_patch_call_123', + output: 'created notes.txt', + status: 'completed', + }, + ]); + }); +}); diff --git a/tests/responsesItems.test.ts b/tests/responsesItems.test.ts index 84853fcf3..97bb50c53 100644 --- a/tests/responsesItems.test.ts +++ b/tests/responsesItems.test.ts @@ -1,4 +1,7 @@ import OpenAI from 'openai/index'; +import { toResponseInputItems } from 'openai/lib/responses/ResponseInputItems'; +import type { ResponseInputItem, ResponseOutputItem } from 'openai/resources/responses'; + const openai = new OpenAI({ apiKey: 'example-api-key' }); describe('responses item types', () => { @@ -12,10 +15,25 @@ const unused = async () => { model: 'gpt-5.1', input: 'You are a helpful assistant.', }); + + const history: Array = [ + { + type: 'function_call_output', + call_id: 'call_123', + output: 'done', + }, + ...response.output, + ]; + await openai.responses.create({ model: 'gpt-5.1', // check type compatibility input: response.output, }); + await openai.responses.create({ + model: 'gpt-5.1', + // check mixed history normalization + input: toResponseInputItems(history), + }); expect(true).toBe(true); };