import OpenAI from 'openai'; import { APIConnectionError } from 'openai/error'; import { PassThrough } from 'stream'; import { ParsingToolFunction, ChatCompletionStreamingRunner, } from 'openai/resources/beta/chat/completions'; import { Response } from 'node-fetch'; import { isAssistantMessage } from "./chatCompletionUtils.mjs"; /** * Creates a mock `fetch` function and a `handleRequest` function for intercepting `fetch` calls. * * You call `handleRequest` with a callback function that handles the next `fetch` call. * It returns a Promise that: * - waits for the next call to `fetch` * - calls the callback with the `fetch` arguments * - resolves `fetch` with the callback output */ function mockFetch() { const fetchQueue = []; const handlerQueue = []; const enqueueHandler = () => { handlerQueue.push(new Promise((resolve) => { fetchQueue.push((handle) => { enqueueHandler(); resolve(handle); }); })); }; enqueueHandler(); async function fetch(req, init) { const handler = await handlerQueue.shift(); if (!handler) throw new Error('expected handler to be defined'); const signal = init?.signal; if (!signal) return await handler(req, init); return await Promise.race([ handler(req, init), new Promise((resolve, reject) => { if (signal.aborted) { // @ts-ignore does exist in Node reject(new DOMException('The user aborted a request.', 'AbortError')); return; } signal.addEventListener('abort', (e) => { // @ts-ignore does exist in Node reject(new DOMException('The user aborted a request.', 'AbortError')); }); }), ]); } function handleRequest(handle) { return new Promise((resolve, reject) => { fetchQueue.shift()?.(async (req, init) => { try { return await handle(req, init); } catch (err) { reject(err); return err; } finally { resolve(); } }); }); } return { fetch, handleRequest }; } // mockChatCompletionFetch is like mockFetch, but with better a more convenient handleRequest to mock // chat completion request/responses. function mockChatCompletionFetch() { const { fetch, handleRequest: handleRawRequest } = mockFetch(); function handleRequest(handler) { return handleRawRequest(async (req, init) => { const rawBody = init?.body; if (typeof rawBody !== 'string') throw new Error(`expected init.body to be a string`); const body = JSON.parse(rawBody); return new Response(JSON.stringify(await handler(body)), { headers: { 'Content-Type': 'application/json' }, }); }); } return { fetch, handleRequest }; } // mockStreamingChatCompletionFetch is like mockFetch, but with better a more convenient handleRequest to mock // streaming chat completion request/responses. function mockStreamingChatCompletionFetch() { const { fetch, handleRequest: handleRawRequest } = mockFetch(); function handleRequest(handler) { return handleRawRequest(async (req, init) => { const rawBody = init?.body; if (typeof rawBody !== 'string') throw new Error(`expected init.body to be a string`); const body = JSON.parse(rawBody); const stream = new PassThrough(); (async () => { for await (const chunk of handler(body)) { stream.write(`data: ${JSON.stringify(chunk)}\n\n`); } stream.end(`data: [DONE]\n\n`); })(); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Transfer-Encoding': 'chunked', }, }); }); } return { fetch, handleRequest }; } // contentChoiceDeltas returns an async iterator which mocks a delta stream of a by splitting the // argument into chunks separated by whitespace. function* contentChoiceDeltas(content, { index = 0, role = 'assistant', } = {}) { const deltas = content.split(/\s+/g); for (let i = 0; i < deltas.length; i++) { yield { index, finish_reason: i === deltas.length - 1 ? 'stop' : null, logprobs: null, delta: { role, content: deltas[i] ? `${deltas[i]}${i === deltas.length - 1 ? '' : ' '}` : null, }, }; } } // functionCallDeltas returns an async iterator which mocks a delta stream of a functionCall by splitting // the argument into chunks separated by whitespace. function* functionCallDeltas(args, { index = 0, id = '123', name, role = 'assistant', }) { const deltas = args.split(/\s+/g); for (let i = 0; i < deltas.length; i++) { yield { index, finish_reason: i === deltas.length - 1 ? 'function_call' : null, delta: { role, tool_calls: [ { type: 'function', index: 0, id, function: { arguments: `${deltas[i] || ''}${i === deltas.length - 1 ? '' : ' '}`, ...(i === deltas.length - 1 ? { name } : null), }, }, ], }, }; } } class RunnerListener { constructor(runner) { this.runner = runner; this.contents = []; this.messages = []; this.chatCompletions = []; this.functionCalls = []; this.functionCallResults = []; this.finalContent = null; this.gotConnect = false; this.gotAbort = false; this.gotEnd = false; this.onceMessageCallCount = 0; runner .on('connect', () => (this.gotConnect = true)) .on('content', (content) => this.contents.push(content)) .on('message', (message) => this.messages.push(message)) .on('chatCompletion', (completion) => this.chatCompletions.push(completion)) .on('functionCall', (functionCall) => this.functionCalls.push(functionCall)) .on('functionCallResult', (result) => this.functionCallResults.push(result)) .on('finalContent', (content) => (this.finalContent = content)) .on('finalMessage', (message) => (this.finalMessage = message)) .on('finalChatCompletion', (completion) => (this.finalChatCompletion = completion)) .on('finalFunctionCall', (functionCall) => (this.finalFunctionCall = functionCall)) .on('finalFunctionCallResult', (result) => (this.finalFunctionCallResult = result)) .on('totalUsage', (usage) => (this.totalUsage = usage)) .on('error', (error) => (this.error = error)) .on('abort', (error) => ((this.error = error), (this.gotAbort = true))) .on('end', () => (this.gotEnd = true)) .once('message', () => this.onceMessageCallCount++); } async sanityCheck({ error } = {}) { expect(this.onceMessageCallCount).toBeLessThanOrEqual(1); expect(this.gotAbort).toEqual(this.runner.aborted); if (this.runner.aborted) expect(this.runner.errored).toBe(true); if (error) { expect(this.error?.message).toEqual(error); expect(this.runner.errored).toBe(true); await expect(this.runner.finalChatCompletion()).rejects.toThrow(error); await expect(this.runner.finalMessage()).rejects.toThrow(error); await expect(this.runner.finalContent()).rejects.toThrow(error); await expect(this.runner.finalFunctionCall()).rejects.toThrow(error); await expect(this.runner.finalFunctionCallResult()).rejects.toThrow(error); await expect(this.runner.totalUsage()).rejects.toThrow(error); await expect(this.runner.done()).rejects.toThrow(error); } else { expect(this.error).toBeUndefined(); expect(this.runner.errored).toBe(false); } if (!this.gotConnect) { expect(this.contents).toEqual([]); expect(this.messages).toEqual([]); expect(this.chatCompletions).toEqual([]); expect(this.functionCalls).toEqual([]); expect(this.functionCallResults).toEqual([]); expect(this.finalContent).toBeUndefined(); expect(this.finalMessage).toBeUndefined(); expect(this.finalChatCompletion).toBeUndefined(); expect(this.finalFunctionCall).toBeUndefined(); expect(this.finalFunctionCallResult).toBeUndefined(); expect(this.totalUsage).toBeUndefined(); expect(this.gotEnd).toBe(true); return; } if (error) return; const expectedContents = this.messages .filter(isAssistantMessage) .map((m) => m.content) .filter(Boolean); expect(this.contents).toEqual(expectedContents); expect(this.finalMessage).toEqual([...this.messages].reverse().find((x) => x.role === 'assistant')); expect(await this.runner.finalMessage()).toEqual(this.finalMessage); expect(this.finalContent).toEqual(expectedContents[expectedContents.length - 1] ?? null); expect(await this.runner.finalContent()).toEqual(this.finalContent); expect(this.finalChatCompletion).toEqual(this.chatCompletions[this.chatCompletions.length - 1]); expect(await this.runner.finalChatCompletion()).toEqual(this.finalChatCompletion); expect(this.finalFunctionCall).toEqual(this.functionCalls[this.functionCalls.length - 1]); expect(await this.runner.finalFunctionCall()).toEqual(this.finalFunctionCall); expect(this.finalFunctionCallResult).toEqual(this.functionCallResults[this.functionCallResults.length - 1]); expect(await this.runner.finalFunctionCallResult()).toEqual(this.finalFunctionCallResult); expect(this.chatCompletions).toEqual(this.runner.allChatCompletions()); expect(this.messages).toEqual(this.runner.messages.slice(-this.messages.length)); if (this.chatCompletions.some((c) => c.usage)) { const totalUsage = { completion_tokens: 0, prompt_tokens: 0, total_tokens: 0, }; for (const { usage } of this.chatCompletions) { if (usage) { totalUsage.completion_tokens += usage.completion_tokens; totalUsage.prompt_tokens += usage.prompt_tokens; totalUsage.total_tokens += usage.total_tokens; } } expect(this.totalUsage).toEqual(totalUsage); expect(await this.runner.totalUsage()).toEqual(totalUsage); } expect(this.gotEnd).toBe(true); } } class StreamingRunnerListener { constructor(runner) { this.runner = runner; this.eventChunks = []; this.eventContents = []; this.eventMessages = []; this.eventChatCompletions = []; this.eventFunctionCalls = []; this.eventFunctionCallResults = []; this.finalContent = null; this.gotConnect = false; this.gotEnd = false; runner .on('connect', () => (this.gotConnect = true)) .on('chunk', (chunk) => this.eventChunks.push(chunk)) .on('content', (delta, snapshot) => this.eventContents.push([delta, snapshot])) .on('message', (message) => this.eventMessages.push(message)) .on('chatCompletion', (completion) => this.eventChatCompletions.push(completion)) .on('functionCall', (functionCall) => this.eventFunctionCalls.push(functionCall)) .on('functionCallResult', (result) => this.eventFunctionCallResults.push(result)) .on('finalContent', (content) => (this.finalContent = content)) .on('finalMessage', (message) => (this.finalMessage = message)) .on('finalChatCompletion', (completion) => (this.finalChatCompletion = completion)) .on('finalFunctionCall', (functionCall) => (this.finalFunctionCall = functionCall)) .on('finalFunctionCallResult', (result) => (this.finalFunctionCallResult = result)) .on('error', (error) => (this.error = error)) .on('abort', (abort) => (this.error = abort)) .on('end', () => (this.gotEnd = true)); } async sanityCheck({ error } = {}) { if (error) { expect(this.error?.message).toEqual(error); expect(this.runner.errored).toBe(true); await expect(this.runner.finalChatCompletion()).rejects.toThrow(error); await expect(this.runner.finalMessage()).rejects.toThrow(error); await expect(this.runner.finalContent()).rejects.toThrow(error); await expect(this.runner.finalFunctionCall()).rejects.toThrow(error); await expect(this.runner.finalFunctionCallResult()).rejects.toThrow(error); await expect(this.runner.done()).rejects.toThrow(error); } else { expect(this.error).toBeUndefined(); expect(this.runner.errored).toBe(false); } if (!this.gotConnect) { expect(this.eventContents).toEqual([]); expect(this.eventMessages).toEqual([]); expect(this.eventChatCompletions).toEqual([]); expect(this.eventFunctionCalls).toEqual([]); expect(this.eventFunctionCallResults).toEqual([]); expect(this.finalContent).toBeUndefined(); expect(this.finalMessage).toBeUndefined(); expect(this.finalChatCompletion).toBeUndefined(); expect(this.finalFunctionCall).toBeUndefined(); expect(this.finalFunctionCallResult).toBeUndefined(); expect(this.gotEnd).toBe(true); return; } if (error) return; if (this.eventContents.length) expect(this.eventChunks.length).toBeGreaterThan(0); expect(this.finalMessage).toEqual([...this.eventMessages].reverse().find((x) => x.role === 'assistant')); expect(await this.runner.finalMessage()).toEqual(this.finalMessage); expect(this.finalContent).toEqual(this.eventContents[this.eventContents.length - 1]?.[1] ?? null); expect(await this.runner.finalContent()).toEqual(this.finalContent); expect(this.finalChatCompletion).toEqual(this.eventChatCompletions[this.eventChatCompletions.length - 1]); expect(await this.runner.finalChatCompletion()).toEqual(this.finalChatCompletion); expect(this.finalFunctionCall).toEqual(this.eventFunctionCalls[this.eventFunctionCalls.length - 1]); expect(await this.runner.finalFunctionCall()).toEqual(this.finalFunctionCall); expect(this.finalFunctionCallResult).toEqual(this.eventFunctionCallResults[this.eventFunctionCallResults.length - 1]); expect(await this.runner.finalFunctionCallResult()).toEqual(this.finalFunctionCallResult); expect(this.eventChatCompletions).toEqual(this.runner.allChatCompletions()); expect(this.eventMessages).toEqual(this.runner.messages.slice(-this.eventMessages.length)); if (error) { expect(this.error?.message).toEqual(error); expect(this.runner.errored).toBe(true); } else { expect(this.error).toBeUndefined(); expect(this.runner.errored).toBe(false); } expect(this.gotEnd).toBe(true); } } function _typeTests() { const openai = new OpenAI(); openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' }, ], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { name: 'numProperties', function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, description: 'gets the number of properties on an object', }, }, { type: 'function', function: { function: (str) => String(str.length), parameters: { type: 'string' }, description: 'gets the length of a string', }, }, { type: 'function', // @ts-expect-error function must accept string if parse is omitted function: { function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }, }, ], }); openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', // @ts-expect-error parse and function don't match parse: (str) => str, function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }), ], }); openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }), new ParsingToolFunction({ name: 'keys', parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object)) { throw new Error('must be an Object'); } return result; }, function: (obj) => Object.keys(obj).join(', '), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }), new ParsingToolFunction({ name: 'len2', // @ts-expect-error parse and function don't match parse: (str) => str, function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }), ], }); openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' }, ], model: 'gpt-3.5-turbo', // @ts-ignore error occurs here in TS 4 tools: [ { type: 'function', function: { name: 'numProperties', parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }, }, { type: 'function', function: { name: 'keys', parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object)) { throw new Error('must be an Object'); } return result; }, function: (obj) => Object.keys(obj).join(', '), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }, }, { type: 'function', function: { name: 'len2', parse: (str) => str, // @ts-ignore error occurs here in TS 5 // function input doesn't match parse output function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, description: 'gets the number of properties on an object', }, }, ], }); } describe('resource completions', () => { describe('runTools with stream: false', () => { test('successful flow', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new RunnerListener(runner); await handleRequest(async (request) => { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }); await handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123', }, ]); return { id: '2', choices: [ { index: 0, finish_reason: 'stop', logprobs: null, message: { role: 'assistant', content: `it's raining`, }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }); await runner.done(); expect(listener.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123' }, { role: 'assistant', content: "it's raining" }, ]); expect(listener.functionCallResults).toEqual([`it's raining`]); await listener.sanityCheck(); }); test('flow with abort', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const controller = new AbortController(); const runner = openai.beta.chat.completions.runTools({ messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }, { signal: controller.signal }); const listener = new RunnerListener(runner); await handleRequest(async (request) => { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }); controller.abort(); await runner.done().catch(() => { }); expect(listener.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123' }, ]); expect(listener.functionCallResults).toEqual([`it's raining`]); await listener.sanityCheck({ error: 'Request was aborted.' }); expect(runner.aborted).toBe(true); }); test('successful flow with parse', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, description: 'gets the number of properties on an object', }), ], }); const listener = new RunnerListener(runner); await handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', usage: { completion_tokens: 5, prompt_tokens: 20, total_tokens: 25, }, }; }); await handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, { role: 'tool', content: '3', tool_call_id: '123', }, ]); return { id: '2', choices: [ { index: 0, finish_reason: 'stop', logprobs: null, message: { role: 'assistant', content: `there are 3 properties in {"a": 1, "b": 2, "c": 3}`, }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', usage: { completion_tokens: 10, prompt_tokens: 25, total_tokens: 35, }, }; }); await runner.done(); expect(listener.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' }, }, ], }, { role: 'tool', content: '3', tool_call_id: '123' }, { role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' }, ]); expect(listener.functionCallResults).toEqual(['3']); await listener.sanityCheck(); }); test('flow with parse error', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, description: 'gets the number of properties on an object', }), ], }); const listener = new RunnerListener(runner); await Promise.all([ handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123', }, ]); return { id: '2', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, { role: 'tool', content: '3', tool_call_id: '1234', }, ]); return { id: '3', choices: [ { index: 0, finish_reason: 'stop', logprobs: null, message: { role: 'assistant', content: `there are 3 properties in {"a": 1, "b": 2, "c": 3}`, }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), runner.done(), ]); expect(listener.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { name: 'numProperties', arguments: '[{"a": 1, "b": 2, "c": 3}]' }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' }, }, ], }, { role: 'tool', content: '3', tool_call_id: '1234' }, { role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' }, ]); expect(listener.functionCallResults).toEqual([`must be an object`, '3']); await listener.sanityCheck(); }); test('single function call', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tool_choice: { type: 'function', function: { name: 'getWeather', }, }, tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new RunnerListener(runner); await Promise.all([ handleRequest(async (request) => { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), runner.done(), ]); expect(listener.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123' }, ]); expect(listener.functionCallResults).toEqual([`it's raining`]); await listener.sanityCheck(); }); test('wrong function name', async () => { const { fetch, handleRequest } = mockChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new RunnerListener(runner); await Promise.all([ handleRequest(async (request) => { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); return { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, ]); return { id: '2', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, message: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), handleRequest(async (request) => { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '1234', }, ]); return { id: '3', choices: [ { index: 0, finish_reason: 'stop', logprobs: null, message: { role: 'assistant', content: `it's raining`, }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion', }; }), runner.done(), ]); expect(listener.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [{ type: 'function', id: '123', function: { name: 'get_weather', arguments: '' } }], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [{ type: 'function', id: '1234', function: { name: 'getWeather', arguments: '' } }], }, { role: 'tool', content: `it's raining`, tool_call_id: '1234' }, { role: 'assistant', content: "it's raining" }, ]); expect(listener.functionCallResults).toEqual([ `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, `it's raining`, ]); await listener.sanityCheck(); }); }); describe('runTools with stream: true', () => { test('successful flow', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); yield { id: '1', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', index: 0, id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123', }, ]); for (const choice of contentChoiceDeltas(`it's raining`)) { yield { id: '2', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), runner.done(), ]); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123' }, { role: 'assistant', content: "it's raining" }, ]); expect(listener.eventFunctionCallResults).toEqual([`it's raining`]); await listener.sanityCheck(); }); test('flow with abort', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const controller = new AbortController(); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }, { signal: controller.signal }); runner.on('functionCallResult', () => controller.abort()); const listener = new StreamingRunnerListener(runner); await handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); yield { id: '1', choices: [ { index: 0, finish_reason: 'function_call', delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', index: 0, id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }); await runner.done().catch(() => { }); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '123' }, ]); expect(listener.eventFunctionCallResults).toEqual([`it's raining`]); await listener.sanityCheck({ error: 'Request was aborted.' }); expect(runner.aborted).toBe(true); }); test('successful flow with parse', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, description: 'gets the number of properties on an object', }), ], }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ]); yield { id: '1', choices: [ { index: 0, finish_reason: 'function_call', delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', index: 0, function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, { role: 'tool', content: '3', tool_call_id: '123', }, ]); for (const choice of contentChoiceDeltas(`there are 3 properties in {"a": 1, "b": 2, "c": 3}`)) { yield { id: '2', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), runner.done(), ]); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' }, }, ], }, { role: 'tool', content: '3', tool_call_id: '123' }, { role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' }, ]); expect(listener.eventFunctionCallResults).toEqual(['3']); await listener.sanityCheck(); }); test('flow with parse error', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ], model: 'gpt-3.5-turbo', tools: [ new ParsingToolFunction({ name: 'numProperties', function: (obj) => String(Object.keys(obj).length), parameters: { type: 'object' }, parse: (str) => { const result = JSON.parse(str); if (!(result instanceof Object) || Array.isArray(result)) { throw new Error('must be an object'); } return result; }, description: 'gets the number of properties on an object', }), ], }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, ]); for (const choice of functionCallDeltas('[{"a": 1, "b": 2, "c": 3}]', { name: 'numProperties', id: '123', })) { yield { id: '1', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123', }, ]); for (const choice of functionCallDeltas('{"a": 1, "b": 2, "c": 3}', { name: 'numProperties', id: '1234', })) { yield { id: '2', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '[{"a": 1, "b": 2, "c": 3}]', name: 'numProperties', }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '{"a": 1, "b": 2, "c": 3}', name: 'numProperties', }, }, ], }, { role: 'tool', content: '3', tool_call_id: '1234', }, ]); for (const choice of contentChoiceDeltas(`there are 3 properties in {"a": 1, "b": 2, "c": 3}`)) { yield { id: '3', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), runner.done(), ]); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { name: 'numProperties', arguments: '[{"a": 1, "b": 2, "c": 3}]' }, }, ], }, { role: 'tool', content: `must be an object`, tool_call_id: '123' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' }, }, ], }, { role: 'tool', content: '3', tool_call_id: '1234' }, { role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' }, ]); expect(listener.eventFunctionCallResults).toEqual([`must be an object`, '3']); await listener.sanityCheck(); }); test('single function call', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tool_choice: { type: 'function', function: { name: 'getWeather', }, }, tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); yield { id: '1', choices: [ { index: 0, finish_reason: 'function_call', delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', index: 0, id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }), runner.done(), ]); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', tool_call_id: '123', content: `it's raining` }, ]); expect(listener.eventFunctionCallResults).toEqual([`it's raining`]); await listener.sanityCheck(); }); test('wrong function name', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.runTools({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', tools: [ { type: 'function', function: { function: function getWeather() { return `it's raining`; }, parameters: {}, description: 'gets the weather', }, }, ], }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); yield { id: '1', choices: [ { index: 0, finish_reason: 'function_call', delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', index: 0, id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, ]); yield { id: '2', choices: [ { index: 0, finish_reason: 'function_call', logprobs: null, delta: { role: 'assistant', content: null, tool_calls: [ { type: 'function', index: 0, id: '1234', function: { arguments: '', name: 'getWeather', }, }, ], }, }, ], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; }), handleRequest(async function* (request) { expect(request.messages).toEqual([ { role: 'user', content: 'tell me what the weather is like' }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '1234', }, ]); for (const choice of contentChoiceDeltas(`it's raining`)) { yield { id: '3', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), runner.done(), ]); expect(listener.eventMessages).toEqual([ { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '123', function: { arguments: '', name: 'get_weather', }, }, ], }, { role: 'tool', content: `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, tool_call_id: '123', }, { role: 'assistant', content: null, tool_calls: [ { type: 'function', id: '1234', function: { arguments: '', name: 'getWeather', }, }, ], }, { role: 'tool', content: `it's raining`, tool_call_id: '1234' }, { role: 'assistant', content: "it's raining" }, ]); expect(listener.eventFunctionCallResults).toEqual([ `Invalid tool_call: "get_weather". Available options are: "getWeather". Please try again`, `it's raining`, ]); await listener.sanityCheck(); }); }); describe('stream', () => { test('successful flow', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.stream({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', }); const listener = new StreamingRunnerListener(runner); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); for (const choice of contentChoiceDeltas(`The weather is great today!`)) { yield { id: '1', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), runner.done(), ]); expect(listener.finalMessage).toEqual({ role: 'assistant', content: 'The weather is great today!' }); await listener.sanityCheck(); }); test('toReadableStream and fromReadableStream', async () => { const { fetch, handleRequest } = mockStreamingChatCompletionFetch(); const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch }); const runner = openai.beta.chat.completions.stream({ stream: true, messages: [{ role: 'user', content: 'tell me what the weather is like' }], model: 'gpt-3.5-turbo', }); const proxied = ChatCompletionStreamingRunner.fromReadableStream(runner.toReadableStream()); const listener = new StreamingRunnerListener(proxied); await Promise.all([ handleRequest(async function* (request) { expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]); for (const choice of contentChoiceDeltas(`The weather is great today!`)) { yield { id: '1', choices: [choice], created: Math.floor(Date.now() / 1000), model: 'gpt-3.5-turbo', object: 'chat.completion.chunk', }; } }), proxied.done(), ]); expect(listener.finalMessage).toEqual({ role: 'assistant', content: 'The weather is great today!' }); await listener.sanityCheck(); }); test('handles network errors', async () => { const { fetch, handleRequest } = mockFetch(); const openai = new OpenAI({ apiKey: '...', fetch }); const stream = openai.beta.chat.completions.stream({ max_tokens: 1024, model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Say hello there!' }], }, { maxRetries: 0 }); handleRequest(async () => { throw new Error('mock request error'); }).catch(() => { }); async function runStream() { await stream.done(); } await expect(runStream).rejects.toThrow(APIConnectionError); }); test('handles network errors on async iterator', async () => { const { fetch, handleRequest } = mockFetch(); const openai = new OpenAI({ apiKey: '...', fetch }); const stream = openai.beta.chat.completions.stream({ max_tokens: 1024, model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Say hello there!' }], }, { maxRetries: 0 }); handleRequest(async () => { throw new Error('mock request error'); }).catch(() => { }); async function runStream() { for await (const _event of stream) { continue; } } await expect(runStream).rejects.toThrow(APIConnectionError); }); }); }); //# sourceMappingURL=ChatCompletionRunFunctions.test.mjs.map