openai 4.14.2 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +114 -1
- package/core.d.ts +1 -0
- package/core.d.ts.map +1 -1
- package/core.js +5 -1
- package/core.js.map +1 -1
- package/core.mjs +5 -1
- package/core.mjs.map +1 -1
- package/index.d.mts +2 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/lib/AbstractChatCompletionRunner.d.ts +111 -0
- package/lib/AbstractChatCompletionRunner.d.ts.map +1 -0
- package/lib/AbstractChatCompletionRunner.js +386 -0
- package/lib/AbstractChatCompletionRunner.js.map +1 -0
- package/lib/AbstractChatCompletionRunner.mjs +382 -0
- package/lib/AbstractChatCompletionRunner.mjs.map +1 -0
- package/lib/ChatCompletionRunFunctions.test.d.ts +2 -0
- package/lib/ChatCompletionRunFunctions.test.d.ts.map +1 -0
- package/lib/ChatCompletionRunFunctions.test.js +1850 -0
- package/lib/ChatCompletionRunFunctions.test.js.map +1 -0
- package/lib/ChatCompletionRunFunctions.test.mjs +1845 -0
- package/lib/ChatCompletionRunFunctions.test.mjs.map +1 -0
- package/lib/ChatCompletionRunner.d.ts +17 -0
- package/lib/ChatCompletionRunner.d.ts.map +1 -0
- package/lib/ChatCompletionRunner.js +19 -0
- package/lib/ChatCompletionRunner.js.map +1 -0
- package/lib/ChatCompletionRunner.mjs +15 -0
- package/lib/ChatCompletionRunner.mjs.map +1 -0
- package/lib/ChatCompletionStream.d.ts +111 -0
- package/lib/ChatCompletionStream.d.ts.map +1 -0
- package/lib/ChatCompletionStream.js +210 -0
- package/lib/ChatCompletionStream.js.map +1 -0
- package/lib/ChatCompletionStream.mjs +206 -0
- package/lib/ChatCompletionStream.mjs.map +1 -0
- package/lib/ChatCompletionStreamingRunner.d.ts +20 -0
- package/lib/ChatCompletionStreamingRunner.d.ts.map +1 -0
- package/lib/ChatCompletionStreamingRunner.js +18 -0
- package/lib/ChatCompletionStreamingRunner.js.map +1 -0
- package/lib/ChatCompletionStreamingRunner.mjs +14 -0
- package/lib/ChatCompletionStreamingRunner.mjs.map +1 -0
- package/lib/RunnableFunction.d.ts +70 -0
- package/lib/RunnableFunction.d.ts.map +1 -0
- package/lib/RunnableFunction.js +22 -0
- package/lib/RunnableFunction.js.map +1 -0
- package/lib/RunnableFunction.mjs +17 -0
- package/lib/RunnableFunction.mjs.map +1 -0
- package/lib/jsonschema.d.ts +106 -0
- package/lib/jsonschema.d.ts.map +1 -0
- package/lib/jsonschema.js +11 -0
- package/lib/jsonschema.js.map +1 -0
- package/lib/jsonschema.mjs +10 -0
- package/lib/jsonschema.mjs.map +1 -0
- package/package.json +1 -1
- package/resources/beta/beta.d.ts +9 -0
- package/resources/beta/beta.d.ts.map +1 -0
- package/resources/beta/beta.js +40 -0
- package/resources/beta/beta.js.map +1 -0
- package/resources/beta/beta.mjs +13 -0
- package/resources/beta/beta.mjs.map +1 -0
- package/resources/beta/chat/chat.d.ts +9 -0
- package/resources/beta/chat/chat.d.ts.map +1 -0
- package/resources/beta/chat/chat.js +40 -0
- package/resources/beta/chat/chat.js.map +1 -0
- package/resources/beta/chat/chat.mjs +13 -0
- package/resources/beta/chat/chat.mjs.map +1 -0
- package/resources/beta/chat/completions.d.ts +28 -0
- package/resources/beta/chat/completions.d.ts.map +1 -0
- package/resources/beta/chat/completions.js +32 -0
- package/resources/beta/chat/completions.js.map +1 -0
- package/resources/beta/chat/completions.mjs +24 -0
- package/resources/beta/chat/completions.mjs.map +1 -0
- package/resources/beta/chat/index.d.ts +3 -0
- package/resources/beta/chat/index.d.ts.map +1 -0
- package/resources/beta/chat/index.js +9 -0
- package/resources/beta/chat/index.js.map +1 -0
- package/resources/beta/chat/index.mjs +4 -0
- package/resources/beta/chat/index.mjs.map +1 -0
- package/resources/beta/index.d.ts +3 -0
- package/resources/beta/index.d.ts.map +1 -0
- package/resources/beta/index.js +9 -0
- package/resources/beta/index.js.map +1 -0
- package/resources/beta/index.mjs +4 -0
- package/resources/beta/index.mjs.map +1 -0
- package/resources/index.d.ts +1 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +3 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -0
- package/resources/index.mjs.map +1 -1
- package/src/core.ts +11 -2
- package/src/index.ts +3 -0
- package/src/lib/AbstractChatCompletionRunner.ts +488 -0
- package/src/lib/ChatCompletionRunFunctions.test.ts +1987 -0
- package/src/lib/ChatCompletionRunner.ts +42 -0
- package/src/lib/ChatCompletionStream.ts +327 -0
- package/src/lib/ChatCompletionStreamingRunner.ts +43 -0
- package/src/lib/RunnableFunction.ts +96 -0
- package/src/lib/jsonschema.ts +148 -0
- package/src/resources/beta/beta.ts +12 -0
- package/src/resources/beta/chat/chat.ts +12 -0
- package/src/resources/beta/chat/completions.ts +70 -0
- package/src/resources/beta/chat/index.ts +4 -0
- package/src/resources/beta/index.ts +4 -0
- package/src/resources/index.ts +1 -0
- package/src/streaming.ts +13 -4
- package/src/version.ts +1 -1
- package/streaming.d.ts +13 -0
- package/streaming.d.ts.map +1 -1
- package/streaming.js +13 -4
- package/streaming.js.map +1 -1
- package/streaming.mjs +13 -4
- package/streaming.mjs.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -0,0 +1,1845 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { PassThrough } from 'stream';
|
|
3
|
+
import { ParsingFunction, ChatCompletionStreamingRunner, } from 'openai/resources/beta/chat/completions';
|
|
4
|
+
import { Response } from 'node-fetch';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a mock `fetch` function and a `handleRequest` function for intercepting `fetch` calls.
|
|
7
|
+
*
|
|
8
|
+
* You call `handleRequest` with a callback function that handles the next `fetch` call.
|
|
9
|
+
* It returns a Promise that:
|
|
10
|
+
* - waits for the next call to `fetch`
|
|
11
|
+
* - calls the callback with the `fetch` arguments
|
|
12
|
+
* - resolves `fetch` with the callback output
|
|
13
|
+
*/
|
|
14
|
+
function mockFetch() {
|
|
15
|
+
const fetchQueue = [];
|
|
16
|
+
const handlerQueue = [];
|
|
17
|
+
const enqueueHandler = () => {
|
|
18
|
+
handlerQueue.push(new Promise((resolve) => {
|
|
19
|
+
fetchQueue.push((handle) => {
|
|
20
|
+
enqueueHandler();
|
|
21
|
+
resolve(handle);
|
|
22
|
+
});
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
25
|
+
enqueueHandler();
|
|
26
|
+
async function fetch(req, init) {
|
|
27
|
+
const handler = await handlerQueue.shift();
|
|
28
|
+
if (!handler)
|
|
29
|
+
throw new Error('expected handler to be defined');
|
|
30
|
+
const signal = init?.signal;
|
|
31
|
+
if (!signal)
|
|
32
|
+
return await handler(req, init);
|
|
33
|
+
return await Promise.race([
|
|
34
|
+
handler(req, init),
|
|
35
|
+
new Promise((resolve, reject) => {
|
|
36
|
+
if (signal.aborted) {
|
|
37
|
+
// @ts-ignore does exist in Node
|
|
38
|
+
reject(new DOMException('The user aborted a request.', 'AbortError'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
signal.addEventListener('abort', (e) => {
|
|
42
|
+
// @ts-ignore does exist in Node
|
|
43
|
+
reject(new DOMException('The user aborted a request.', 'AbortError'));
|
|
44
|
+
});
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
function handleRequest(handle) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
fetchQueue.shift()?.(async (req, init) => {
|
|
51
|
+
try {
|
|
52
|
+
return await handle(req, init);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
resolve();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { fetch, handleRequest };
|
|
61
|
+
}
|
|
62
|
+
// mockChatCompletionFetch is like mockFetch, but with better a more convenient handleRequest to mock
|
|
63
|
+
// chat completion request/responses.
|
|
64
|
+
function mockChatCompletionFetch() {
|
|
65
|
+
const { fetch, handleRequest: handleRawRequest } = mockFetch();
|
|
66
|
+
function handleRequest(handler) {
|
|
67
|
+
return handleRawRequest(async (req, init) => {
|
|
68
|
+
const rawBody = init?.body;
|
|
69
|
+
if (typeof rawBody !== 'string')
|
|
70
|
+
throw new Error(`expected init.body to be a string`);
|
|
71
|
+
const body = JSON.parse(rawBody);
|
|
72
|
+
return new Response(JSON.stringify(await handler(body)), {
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return { fetch, handleRequest };
|
|
78
|
+
}
|
|
79
|
+
// mockStreamingChatCompletionFetch is like mockFetch, but with better a more convenient handleRequest to mock
|
|
80
|
+
// streaming chat completion request/responses.
|
|
81
|
+
function mockStreamingChatCompletionFetch() {
|
|
82
|
+
const { fetch, handleRequest: handleRawRequest } = mockFetch();
|
|
83
|
+
function handleRequest(handler) {
|
|
84
|
+
return handleRawRequest(async (req, init) => {
|
|
85
|
+
const rawBody = init?.body;
|
|
86
|
+
if (typeof rawBody !== 'string')
|
|
87
|
+
throw new Error(`expected init.body to be a string`);
|
|
88
|
+
const body = JSON.parse(rawBody);
|
|
89
|
+
const stream = new PassThrough();
|
|
90
|
+
(async () => {
|
|
91
|
+
for await (const chunk of handler(body)) {
|
|
92
|
+
stream.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
93
|
+
}
|
|
94
|
+
stream.end(`data: [DONE]\n\n`);
|
|
95
|
+
})();
|
|
96
|
+
return new Response(stream, {
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'text/event-stream',
|
|
99
|
+
'Transfer-Encoding': 'chunked',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { fetch, handleRequest };
|
|
105
|
+
}
|
|
106
|
+
// contentChoiceDeltas returns an async iterator which mocks a delta stream of a by splitting the
|
|
107
|
+
// argument into chunks separated by whitespace.
|
|
108
|
+
function* contentChoiceDeltas(content, { index = 0, role = 'assistant', } = {}) {
|
|
109
|
+
const deltas = content.split(/\s+/g);
|
|
110
|
+
for (let i = 0; i < deltas.length; i++) {
|
|
111
|
+
yield {
|
|
112
|
+
index,
|
|
113
|
+
finish_reason: i === deltas.length - 1 ? 'stop' : null,
|
|
114
|
+
delta: {
|
|
115
|
+
role,
|
|
116
|
+
content: deltas[i] ? `${deltas[i]}${i === deltas.length - 1 ? '' : ' '}` : null,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// functionCallDeltas returns an async iterator which mocks a delta stream of a functionCall by splitting
|
|
122
|
+
// the argument into chunks separated by whitespace.
|
|
123
|
+
function* functionCallDeltas(args, { index = 0, name, role = 'assistant', }) {
|
|
124
|
+
const deltas = args.split(/\s+/g);
|
|
125
|
+
for (let i = 0; i < deltas.length; i++) {
|
|
126
|
+
yield {
|
|
127
|
+
index,
|
|
128
|
+
finish_reason: i === deltas.length - 1 ? 'function_call' : null,
|
|
129
|
+
delta: {
|
|
130
|
+
role,
|
|
131
|
+
function_call: {
|
|
132
|
+
arguments: `${deltas[i] || ''}${i === deltas.length - 1 ? '' : ' '}`,
|
|
133
|
+
...(i === deltas.length - 1 ? { name } : null),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
class RunnerListener {
|
|
140
|
+
constructor(runner) {
|
|
141
|
+
this.runner = runner;
|
|
142
|
+
this.contents = [];
|
|
143
|
+
this.messages = [];
|
|
144
|
+
this.chatCompletions = [];
|
|
145
|
+
this.functionCalls = [];
|
|
146
|
+
this.functionCallResults = [];
|
|
147
|
+
this.finalContent = null;
|
|
148
|
+
this.gotConnect = false;
|
|
149
|
+
this.gotAbort = false;
|
|
150
|
+
this.gotEnd = false;
|
|
151
|
+
this.onceMessageCallCount = 0;
|
|
152
|
+
runner
|
|
153
|
+
.on('connect', () => (this.gotConnect = true))
|
|
154
|
+
.on('content', (content) => this.contents.push(content))
|
|
155
|
+
.on('message', (message) => this.messages.push(message))
|
|
156
|
+
.on('chatCompletion', (completion) => this.chatCompletions.push(completion))
|
|
157
|
+
.on('functionCall', (functionCall) => this.functionCalls.push(functionCall))
|
|
158
|
+
.on('functionCallResult', (result) => this.functionCallResults.push(result))
|
|
159
|
+
.on('finalContent', (content) => (this.finalContent = content))
|
|
160
|
+
.on('finalMessage', (message) => (this.finalMessage = message))
|
|
161
|
+
.on('finalChatCompletion', (completion) => (this.finalChatCompletion = completion))
|
|
162
|
+
.on('finalFunctionCall', (functionCall) => (this.finalFunctionCall = functionCall))
|
|
163
|
+
.on('finalFunctionCallResult', (result) => (this.finalFunctionCallResult = result))
|
|
164
|
+
.on('totalUsage', (usage) => (this.totalUsage = usage))
|
|
165
|
+
.on('error', (error) => (this.error = error))
|
|
166
|
+
.on('abort', () => (this.gotAbort = true))
|
|
167
|
+
.on('end', () => (this.gotEnd = true))
|
|
168
|
+
.once('message', () => this.onceMessageCallCount++);
|
|
169
|
+
}
|
|
170
|
+
async sanityCheck({ error } = {}) {
|
|
171
|
+
expect(this.onceMessageCallCount).toBeLessThanOrEqual(1);
|
|
172
|
+
expect(this.gotAbort).toEqual(this.runner.aborted);
|
|
173
|
+
if (this.runner.aborted)
|
|
174
|
+
expect(this.runner.errored).toBe(true);
|
|
175
|
+
if (error) {
|
|
176
|
+
expect(this.error?.message).toEqual(error);
|
|
177
|
+
expect(this.runner.errored).toBe(true);
|
|
178
|
+
await expect(this.runner.finalChatCompletion()).rejects.toThrow(error);
|
|
179
|
+
await expect(this.runner.finalMessage()).rejects.toThrow(error);
|
|
180
|
+
await expect(this.runner.finalContent()).rejects.toThrow(error);
|
|
181
|
+
await expect(this.runner.finalFunctionCall()).rejects.toThrow(error);
|
|
182
|
+
await expect(this.runner.finalFunctionCallResult()).rejects.toThrow(error);
|
|
183
|
+
await expect(this.runner.totalUsage()).rejects.toThrow(error);
|
|
184
|
+
await expect(this.runner.done()).rejects.toThrow(error);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
expect(this.error).toBeUndefined();
|
|
188
|
+
expect(this.runner.errored).toBe(false);
|
|
189
|
+
}
|
|
190
|
+
if (!this.gotConnect) {
|
|
191
|
+
expect(this.contents).toEqual([]);
|
|
192
|
+
expect(this.messages).toEqual([]);
|
|
193
|
+
expect(this.chatCompletions).toEqual([]);
|
|
194
|
+
expect(this.functionCalls).toEqual([]);
|
|
195
|
+
expect(this.functionCallResults).toEqual([]);
|
|
196
|
+
expect(this.finalContent).toBeUndefined();
|
|
197
|
+
expect(this.finalMessage).toBeUndefined();
|
|
198
|
+
expect(this.finalChatCompletion).toBeUndefined();
|
|
199
|
+
expect(this.finalFunctionCall).toBeUndefined();
|
|
200
|
+
expect(this.finalFunctionCallResult).toBeUndefined();
|
|
201
|
+
expect(this.totalUsage).toBeUndefined();
|
|
202
|
+
expect(this.gotEnd).toBe(true);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (error)
|
|
206
|
+
return;
|
|
207
|
+
const expectedContents = this.messages
|
|
208
|
+
.filter((m) => m.role === 'assistant')
|
|
209
|
+
.map((m) => m.content)
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
expect(this.contents).toEqual(expectedContents);
|
|
212
|
+
expect(this.finalMessage).toEqual(this.messages[this.messages.length - 1]);
|
|
213
|
+
expect(await this.runner.finalMessage()).toEqual(this.finalMessage);
|
|
214
|
+
expect(this.finalContent).toEqual(expectedContents[expectedContents.length - 1] ?? null);
|
|
215
|
+
expect(await this.runner.finalContent()).toEqual(this.finalContent);
|
|
216
|
+
expect(this.finalChatCompletion).toEqual(this.chatCompletions[this.chatCompletions.length - 1]);
|
|
217
|
+
expect(await this.runner.finalChatCompletion()).toEqual(this.finalChatCompletion);
|
|
218
|
+
expect(this.finalFunctionCall).toEqual(this.functionCalls[this.functionCalls.length - 1]);
|
|
219
|
+
expect(await this.runner.finalFunctionCall()).toEqual(this.finalFunctionCall);
|
|
220
|
+
expect(this.finalFunctionCallResult).toEqual(this.functionCallResults[this.functionCallResults.length - 1]);
|
|
221
|
+
expect(await this.runner.finalFunctionCallResult()).toEqual(this.finalFunctionCallResult);
|
|
222
|
+
expect(this.chatCompletions).toEqual(this.runner.allChatCompletions());
|
|
223
|
+
expect(this.messages).toEqual(this.runner.messages.slice(-this.messages.length));
|
|
224
|
+
if (this.chatCompletions.some((c) => c.usage)) {
|
|
225
|
+
const totalUsage = {
|
|
226
|
+
completion_tokens: 0,
|
|
227
|
+
prompt_tokens: 0,
|
|
228
|
+
total_tokens: 0,
|
|
229
|
+
};
|
|
230
|
+
for (const { usage } of this.chatCompletions) {
|
|
231
|
+
if (usage) {
|
|
232
|
+
totalUsage.completion_tokens += usage.completion_tokens;
|
|
233
|
+
totalUsage.prompt_tokens += usage.prompt_tokens;
|
|
234
|
+
totalUsage.total_tokens += usage.total_tokens;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
expect(this.totalUsage).toEqual(totalUsage);
|
|
238
|
+
expect(await this.runner.totalUsage()).toEqual(totalUsage);
|
|
239
|
+
}
|
|
240
|
+
expect(this.gotEnd).toBe(true);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
class StreamingRunnerListener {
|
|
244
|
+
constructor(runner) {
|
|
245
|
+
this.runner = runner;
|
|
246
|
+
this.eventChunks = [];
|
|
247
|
+
this.eventContents = [];
|
|
248
|
+
this.eventMessages = [];
|
|
249
|
+
this.eventChatCompletions = [];
|
|
250
|
+
this.eventFunctionCalls = [];
|
|
251
|
+
this.eventFunctionCallResults = [];
|
|
252
|
+
this.finalContent = null;
|
|
253
|
+
this.gotConnect = false;
|
|
254
|
+
this.gotEnd = false;
|
|
255
|
+
runner
|
|
256
|
+
.on('connect', () => (this.gotConnect = true))
|
|
257
|
+
.on('chunk', (chunk) => this.eventChunks.push(chunk))
|
|
258
|
+
.on('content', (delta, snapshot) => this.eventContents.push([delta, snapshot]))
|
|
259
|
+
.on('message', (message) => this.eventMessages.push(message))
|
|
260
|
+
.on('chatCompletion', (completion) => this.eventChatCompletions.push(completion))
|
|
261
|
+
.on('functionCall', (functionCall) => this.eventFunctionCalls.push(functionCall))
|
|
262
|
+
.on('functionCallResult', (result) => this.eventFunctionCallResults.push(result))
|
|
263
|
+
.on('finalContent', (content) => (this.finalContent = content))
|
|
264
|
+
.on('finalMessage', (message) => (this.finalMessage = message))
|
|
265
|
+
.on('finalChatCompletion', (completion) => (this.finalChatCompletion = completion))
|
|
266
|
+
.on('finalFunctionCall', (functionCall) => (this.finalFunctionCall = functionCall))
|
|
267
|
+
.on('finalFunctionCallResult', (result) => (this.finalFunctionCallResult = result))
|
|
268
|
+
.on('error', (error) => (this.error = error))
|
|
269
|
+
.on('end', () => (this.gotEnd = true));
|
|
270
|
+
}
|
|
271
|
+
async sanityCheck({ error } = {}) {
|
|
272
|
+
if (error) {
|
|
273
|
+
expect(this.error?.message).toEqual(error);
|
|
274
|
+
expect(this.runner.errored).toBe(true);
|
|
275
|
+
await expect(this.runner.finalChatCompletion()).rejects.toThrow(error);
|
|
276
|
+
await expect(this.runner.finalMessage()).rejects.toThrow(error);
|
|
277
|
+
await expect(this.runner.finalContent()).rejects.toThrow(error);
|
|
278
|
+
await expect(this.runner.finalFunctionCall()).rejects.toThrow(error);
|
|
279
|
+
await expect(this.runner.finalFunctionCallResult()).rejects.toThrow(error);
|
|
280
|
+
await expect(this.runner.done()).rejects.toThrow(error);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
expect(this.error).toBeUndefined();
|
|
284
|
+
expect(this.runner.errored).toBe(false);
|
|
285
|
+
}
|
|
286
|
+
if (!this.gotConnect) {
|
|
287
|
+
expect(this.eventContents).toEqual([]);
|
|
288
|
+
expect(this.eventMessages).toEqual([]);
|
|
289
|
+
expect(this.eventChatCompletions).toEqual([]);
|
|
290
|
+
expect(this.eventFunctionCalls).toEqual([]);
|
|
291
|
+
expect(this.eventFunctionCallResults).toEqual([]);
|
|
292
|
+
expect(this.finalContent).toBeUndefined();
|
|
293
|
+
expect(this.finalMessage).toBeUndefined();
|
|
294
|
+
expect(this.finalChatCompletion).toBeUndefined();
|
|
295
|
+
expect(this.finalFunctionCall).toBeUndefined();
|
|
296
|
+
expect(this.finalFunctionCallResult).toBeUndefined();
|
|
297
|
+
expect(this.gotEnd).toBe(true);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (error)
|
|
301
|
+
return;
|
|
302
|
+
if (this.eventContents.length)
|
|
303
|
+
expect(this.eventChunks.length).toBeGreaterThan(0);
|
|
304
|
+
expect(this.finalMessage).toEqual(this.eventMessages[this.eventMessages.length - 1]);
|
|
305
|
+
expect(await this.runner.finalMessage()).toEqual(this.finalMessage);
|
|
306
|
+
expect(this.finalContent).toEqual(this.eventContents[this.eventContents.length - 1]?.[1] ?? null);
|
|
307
|
+
expect(await this.runner.finalContent()).toEqual(this.finalContent);
|
|
308
|
+
expect(this.finalChatCompletion).toEqual(this.eventChatCompletions[this.eventChatCompletions.length - 1]);
|
|
309
|
+
expect(await this.runner.finalChatCompletion()).toEqual(this.finalChatCompletion);
|
|
310
|
+
expect(this.finalFunctionCall).toEqual(this.eventFunctionCalls[this.eventFunctionCalls.length - 1]);
|
|
311
|
+
expect(await this.runner.finalFunctionCall()).toEqual(this.finalFunctionCall);
|
|
312
|
+
expect(this.finalFunctionCallResult).toEqual(this.eventFunctionCallResults[this.eventFunctionCallResults.length - 1]);
|
|
313
|
+
expect(await this.runner.finalFunctionCallResult()).toEqual(this.finalFunctionCallResult);
|
|
314
|
+
expect(this.eventChatCompletions).toEqual(this.runner.allChatCompletions());
|
|
315
|
+
expect(this.eventMessages).toEqual(this.runner.messages.slice(-this.eventMessages.length));
|
|
316
|
+
if (error) {
|
|
317
|
+
expect(this.error?.message).toEqual(error);
|
|
318
|
+
expect(this.runner.errored).toBe(true);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
expect(this.error).toBeUndefined();
|
|
322
|
+
expect(this.runner.errored).toBe(false);
|
|
323
|
+
}
|
|
324
|
+
expect(this.gotEnd).toBe(true);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function _typeTests() {
|
|
328
|
+
const openai = new OpenAI();
|
|
329
|
+
openai.beta.chat.completions.runFunctions({
|
|
330
|
+
messages: [
|
|
331
|
+
{ role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' },
|
|
332
|
+
],
|
|
333
|
+
model: 'gpt-3.5-turbo',
|
|
334
|
+
functions: [
|
|
335
|
+
{
|
|
336
|
+
name: 'numProperties',
|
|
337
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
338
|
+
parameters: { type: 'object' },
|
|
339
|
+
parse: (str) => {
|
|
340
|
+
const result = JSON.parse(str);
|
|
341
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
342
|
+
throw new Error('must be an object');
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
},
|
|
346
|
+
description: 'gets the number of properties on an object',
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
function: (str) => String(str.length),
|
|
350
|
+
parameters: { type: 'string' },
|
|
351
|
+
description: 'gets the length of a string',
|
|
352
|
+
},
|
|
353
|
+
// @ts-expect-error function must accept string if parse is omitted
|
|
354
|
+
{
|
|
355
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
356
|
+
parameters: { type: 'object' },
|
|
357
|
+
description: 'gets the number of properties on an object',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
openai.beta.chat.completions.runFunctions({
|
|
362
|
+
messages: [
|
|
363
|
+
{ role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' },
|
|
364
|
+
],
|
|
365
|
+
model: 'gpt-3.5-turbo',
|
|
366
|
+
functions: [
|
|
367
|
+
new ParsingFunction({
|
|
368
|
+
name: 'numProperties',
|
|
369
|
+
// @ts-expect-error parse and function don't match
|
|
370
|
+
parse: (str) => str,
|
|
371
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
372
|
+
parameters: { type: 'object' },
|
|
373
|
+
description: 'gets the number of properties on an object',
|
|
374
|
+
}),
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
openai.beta.chat.completions.runFunctions({
|
|
378
|
+
messages: [
|
|
379
|
+
{ role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' },
|
|
380
|
+
],
|
|
381
|
+
model: 'gpt-3.5-turbo',
|
|
382
|
+
functions: [
|
|
383
|
+
new ParsingFunction({
|
|
384
|
+
name: 'numProperties',
|
|
385
|
+
parse: (str) => {
|
|
386
|
+
const result = JSON.parse(str);
|
|
387
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
388
|
+
throw new Error('must be an object');
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
},
|
|
392
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
393
|
+
parameters: { type: 'object' },
|
|
394
|
+
description: 'gets the number of properties on an object',
|
|
395
|
+
}),
|
|
396
|
+
new ParsingFunction({
|
|
397
|
+
name: 'keys',
|
|
398
|
+
parse: (str) => {
|
|
399
|
+
const result = JSON.parse(str);
|
|
400
|
+
if (!(result instanceof Object)) {
|
|
401
|
+
throw new Error('must be an Object');
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
},
|
|
405
|
+
function: (obj) => Object.keys(obj).join(', '),
|
|
406
|
+
parameters: { type: 'object' },
|
|
407
|
+
description: 'gets the number of properties on an object',
|
|
408
|
+
}),
|
|
409
|
+
new ParsingFunction({
|
|
410
|
+
name: 'len2',
|
|
411
|
+
// @ts-expect-error parse and function don't match
|
|
412
|
+
parse: (str) => str,
|
|
413
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
414
|
+
parameters: { type: 'object' },
|
|
415
|
+
description: 'gets the number of properties on an object',
|
|
416
|
+
}),
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
openai.beta.chat.completions.runFunctions({
|
|
420
|
+
messages: [
|
|
421
|
+
{ role: 'user', content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}' },
|
|
422
|
+
],
|
|
423
|
+
model: 'gpt-3.5-turbo',
|
|
424
|
+
// @ts-ignore error occurs here in TS 4
|
|
425
|
+
functions: [
|
|
426
|
+
{
|
|
427
|
+
name: 'numProperties',
|
|
428
|
+
parse: (str) => {
|
|
429
|
+
const result = JSON.parse(str);
|
|
430
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
431
|
+
throw new Error('must be an object');
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
},
|
|
435
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
436
|
+
parameters: { type: 'object' },
|
|
437
|
+
description: 'gets the number of properties on an object',
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'keys',
|
|
441
|
+
parse: (str) => {
|
|
442
|
+
const result = JSON.parse(str);
|
|
443
|
+
if (!(result instanceof Object)) {
|
|
444
|
+
throw new Error('must be an Object');
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
},
|
|
448
|
+
function: (obj) => Object.keys(obj).join(', '),
|
|
449
|
+
parameters: { type: 'object' },
|
|
450
|
+
description: 'gets the number of properties on an object',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: 'len2',
|
|
454
|
+
parse: (str) => str,
|
|
455
|
+
// @ts-ignore error occurs here in TS 5
|
|
456
|
+
// function input doesn't match parse output
|
|
457
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
458
|
+
parameters: { type: 'object' },
|
|
459
|
+
description: 'gets the number of properties on an object',
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
describe('resource completions', () => {
|
|
465
|
+
describe('runFunctions with stream: false', () => {
|
|
466
|
+
test('successful flow', async () => {
|
|
467
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
468
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
469
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
470
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
471
|
+
model: 'gpt-3.5-turbo',
|
|
472
|
+
functions: [
|
|
473
|
+
{
|
|
474
|
+
function: function getWeather() {
|
|
475
|
+
return `it's raining`;
|
|
476
|
+
},
|
|
477
|
+
parameters: {},
|
|
478
|
+
description: 'gets the weather',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
});
|
|
482
|
+
const listener = new RunnerListener(runner);
|
|
483
|
+
await Promise.all([
|
|
484
|
+
handleRequest(async (request) => {
|
|
485
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
486
|
+
return {
|
|
487
|
+
id: '1',
|
|
488
|
+
choices: [
|
|
489
|
+
{
|
|
490
|
+
index: 0,
|
|
491
|
+
finish_reason: 'function_call',
|
|
492
|
+
message: {
|
|
493
|
+
role: 'assistant',
|
|
494
|
+
content: null,
|
|
495
|
+
function_call: {
|
|
496
|
+
arguments: '',
|
|
497
|
+
name: 'getWeather',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
created: Math.floor(Date.now() / 1000),
|
|
503
|
+
model: 'gpt-3.5-turbo',
|
|
504
|
+
object: 'chat.completion',
|
|
505
|
+
};
|
|
506
|
+
}),
|
|
507
|
+
handleRequest(async (request) => {
|
|
508
|
+
expect(request.messages).toEqual([
|
|
509
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
510
|
+
{
|
|
511
|
+
role: 'assistant',
|
|
512
|
+
content: null,
|
|
513
|
+
function_call: {
|
|
514
|
+
arguments: '',
|
|
515
|
+
name: 'getWeather',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
role: 'function',
|
|
520
|
+
content: `it's raining`,
|
|
521
|
+
name: 'getWeather',
|
|
522
|
+
},
|
|
523
|
+
]);
|
|
524
|
+
return {
|
|
525
|
+
id: '2',
|
|
526
|
+
choices: [
|
|
527
|
+
{
|
|
528
|
+
index: 0,
|
|
529
|
+
finish_reason: 'stop',
|
|
530
|
+
message: {
|
|
531
|
+
role: 'assistant',
|
|
532
|
+
content: `it's raining`,
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
created: Math.floor(Date.now() / 1000),
|
|
537
|
+
model: 'gpt-3.5-turbo',
|
|
538
|
+
object: 'chat.completion',
|
|
539
|
+
};
|
|
540
|
+
}),
|
|
541
|
+
runner.done(),
|
|
542
|
+
]);
|
|
543
|
+
expect(listener.messages).toEqual([
|
|
544
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
545
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
546
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
547
|
+
{ role: 'assistant', content: "it's raining" },
|
|
548
|
+
]);
|
|
549
|
+
expect(listener.functionCallResults).toEqual([`it's raining`]);
|
|
550
|
+
await listener.sanityCheck();
|
|
551
|
+
});
|
|
552
|
+
test('flow with abort', async () => {
|
|
553
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
554
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
557
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
558
|
+
model: 'gpt-3.5-turbo',
|
|
559
|
+
functions: [
|
|
560
|
+
{
|
|
561
|
+
function: function getWeather() {
|
|
562
|
+
return `it's raining`;
|
|
563
|
+
},
|
|
564
|
+
parameters: {},
|
|
565
|
+
description: 'gets the weather',
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
}, { signal: controller.signal });
|
|
569
|
+
const listener = new RunnerListener(runner);
|
|
570
|
+
await handleRequest(async (request) => {
|
|
571
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
572
|
+
return {
|
|
573
|
+
id: '1',
|
|
574
|
+
choices: [
|
|
575
|
+
{
|
|
576
|
+
index: 0,
|
|
577
|
+
finish_reason: 'function_call',
|
|
578
|
+
message: {
|
|
579
|
+
role: 'assistant',
|
|
580
|
+
content: null,
|
|
581
|
+
function_call: {
|
|
582
|
+
arguments: '',
|
|
583
|
+
name: 'getWeather',
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
created: Math.floor(Date.now() / 1000),
|
|
589
|
+
model: 'gpt-3.5-turbo',
|
|
590
|
+
object: 'chat.completion',
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
controller.abort();
|
|
594
|
+
await runner.done().catch(() => { });
|
|
595
|
+
expect(listener.messages).toEqual([
|
|
596
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
597
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
598
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
599
|
+
]);
|
|
600
|
+
expect(listener.functionCallResults).toEqual([`it's raining`]);
|
|
601
|
+
await listener.sanityCheck({ error: 'Request was aborted.' });
|
|
602
|
+
expect(runner.aborted).toBe(true);
|
|
603
|
+
});
|
|
604
|
+
test('successful flow with parse', async () => {
|
|
605
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
606
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
607
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
608
|
+
messages: [
|
|
609
|
+
{
|
|
610
|
+
role: 'user',
|
|
611
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
model: 'gpt-3.5-turbo',
|
|
615
|
+
functions: [
|
|
616
|
+
new ParsingFunction({
|
|
617
|
+
name: 'numProperties',
|
|
618
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
619
|
+
parameters: { type: 'object' },
|
|
620
|
+
parse: (str) => {
|
|
621
|
+
const result = JSON.parse(str);
|
|
622
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
623
|
+
throw new Error('must be an object');
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
},
|
|
627
|
+
description: 'gets the number of properties on an object',
|
|
628
|
+
}),
|
|
629
|
+
],
|
|
630
|
+
});
|
|
631
|
+
const listener = new RunnerListener(runner);
|
|
632
|
+
await Promise.all([
|
|
633
|
+
handleRequest(async (request) => {
|
|
634
|
+
expect(request.messages).toEqual([
|
|
635
|
+
{
|
|
636
|
+
role: 'user',
|
|
637
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
638
|
+
},
|
|
639
|
+
]);
|
|
640
|
+
return {
|
|
641
|
+
id: '1',
|
|
642
|
+
choices: [
|
|
643
|
+
{
|
|
644
|
+
index: 0,
|
|
645
|
+
finish_reason: 'function_call',
|
|
646
|
+
message: {
|
|
647
|
+
role: 'assistant',
|
|
648
|
+
content: null,
|
|
649
|
+
function_call: {
|
|
650
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
651
|
+
name: 'numProperties',
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
created: Math.floor(Date.now() / 1000),
|
|
657
|
+
model: 'gpt-3.5-turbo',
|
|
658
|
+
object: 'chat.completion',
|
|
659
|
+
usage: {
|
|
660
|
+
completion_tokens: 5,
|
|
661
|
+
prompt_tokens: 20,
|
|
662
|
+
total_tokens: 25,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}),
|
|
666
|
+
handleRequest(async (request) => {
|
|
667
|
+
expect(request.messages).toEqual([
|
|
668
|
+
{
|
|
669
|
+
role: 'user',
|
|
670
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
role: 'assistant',
|
|
674
|
+
content: null,
|
|
675
|
+
function_call: {
|
|
676
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
677
|
+
name: 'numProperties',
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
role: 'function',
|
|
682
|
+
content: '3',
|
|
683
|
+
name: 'numProperties',
|
|
684
|
+
},
|
|
685
|
+
]);
|
|
686
|
+
return {
|
|
687
|
+
id: '2',
|
|
688
|
+
choices: [
|
|
689
|
+
{
|
|
690
|
+
index: 0,
|
|
691
|
+
finish_reason: 'stop',
|
|
692
|
+
message: {
|
|
693
|
+
role: 'assistant',
|
|
694
|
+
content: `there are 3 properties in {"a": 1, "b": 2, "c": 3}`,
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
created: Math.floor(Date.now() / 1000),
|
|
699
|
+
model: 'gpt-3.5-turbo',
|
|
700
|
+
object: 'chat.completion',
|
|
701
|
+
usage: {
|
|
702
|
+
completion_tokens: 10,
|
|
703
|
+
prompt_tokens: 25,
|
|
704
|
+
total_tokens: 35,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}),
|
|
708
|
+
runner.done(),
|
|
709
|
+
]);
|
|
710
|
+
expect(listener.messages).toEqual([
|
|
711
|
+
{
|
|
712
|
+
role: 'user',
|
|
713
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
role: 'assistant',
|
|
717
|
+
content: null,
|
|
718
|
+
function_call: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' },
|
|
719
|
+
},
|
|
720
|
+
{ role: 'function', content: '3', name: 'numProperties' },
|
|
721
|
+
{ role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' },
|
|
722
|
+
]);
|
|
723
|
+
expect(listener.functionCallResults).toEqual(['3']);
|
|
724
|
+
await listener.sanityCheck();
|
|
725
|
+
});
|
|
726
|
+
test('flow with parse error', async () => {
|
|
727
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
728
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
729
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
730
|
+
messages: [
|
|
731
|
+
{
|
|
732
|
+
role: 'user',
|
|
733
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
model: 'gpt-3.5-turbo',
|
|
737
|
+
functions: [
|
|
738
|
+
new ParsingFunction({
|
|
739
|
+
name: 'numProperties',
|
|
740
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
741
|
+
parameters: { type: 'object' },
|
|
742
|
+
parse: (str) => {
|
|
743
|
+
const result = JSON.parse(str);
|
|
744
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
745
|
+
throw new Error('must be an object');
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
},
|
|
749
|
+
description: 'gets the number of properties on an object',
|
|
750
|
+
}),
|
|
751
|
+
],
|
|
752
|
+
});
|
|
753
|
+
const listener = new RunnerListener(runner);
|
|
754
|
+
await Promise.all([
|
|
755
|
+
handleRequest(async (request) => {
|
|
756
|
+
expect(request.messages).toEqual([
|
|
757
|
+
{
|
|
758
|
+
role: 'user',
|
|
759
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
760
|
+
},
|
|
761
|
+
]);
|
|
762
|
+
return {
|
|
763
|
+
id: '1',
|
|
764
|
+
choices: [
|
|
765
|
+
{
|
|
766
|
+
index: 0,
|
|
767
|
+
finish_reason: 'function_call',
|
|
768
|
+
message: {
|
|
769
|
+
role: 'assistant',
|
|
770
|
+
content: null,
|
|
771
|
+
function_call: {
|
|
772
|
+
arguments: '[{"a": 1, "b": 2, "c": 3}]',
|
|
773
|
+
name: 'numProperties',
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
created: Math.floor(Date.now() / 1000),
|
|
779
|
+
model: 'gpt-3.5-turbo',
|
|
780
|
+
object: 'chat.completion',
|
|
781
|
+
};
|
|
782
|
+
}),
|
|
783
|
+
handleRequest(async (request) => {
|
|
784
|
+
expect(request.messages).toEqual([
|
|
785
|
+
{
|
|
786
|
+
role: 'user',
|
|
787
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
role: 'assistant',
|
|
791
|
+
content: null,
|
|
792
|
+
function_call: {
|
|
793
|
+
arguments: '[{"a": 1, "b": 2, "c": 3}]',
|
|
794
|
+
name: 'numProperties',
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
role: 'function',
|
|
799
|
+
content: `must be an object`,
|
|
800
|
+
name: 'numProperties',
|
|
801
|
+
},
|
|
802
|
+
]);
|
|
803
|
+
return {
|
|
804
|
+
id: '2',
|
|
805
|
+
choices: [
|
|
806
|
+
{
|
|
807
|
+
index: 0,
|
|
808
|
+
finish_reason: 'function_call',
|
|
809
|
+
message: {
|
|
810
|
+
role: 'assistant',
|
|
811
|
+
content: null,
|
|
812
|
+
function_call: {
|
|
813
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
814
|
+
name: 'numProperties',
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
created: Math.floor(Date.now() / 1000),
|
|
820
|
+
model: 'gpt-3.5-turbo',
|
|
821
|
+
object: 'chat.completion',
|
|
822
|
+
};
|
|
823
|
+
}),
|
|
824
|
+
handleRequest(async (request) => {
|
|
825
|
+
expect(request.messages).toEqual([
|
|
826
|
+
{
|
|
827
|
+
role: 'user',
|
|
828
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
role: 'assistant',
|
|
832
|
+
content: null,
|
|
833
|
+
function_call: {
|
|
834
|
+
arguments: '[{"a": 1, "b": 2, "c": 3}]',
|
|
835
|
+
name: 'numProperties',
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
role: 'function',
|
|
840
|
+
content: `must be an object`,
|
|
841
|
+
name: 'numProperties',
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
role: 'assistant',
|
|
845
|
+
content: null,
|
|
846
|
+
function_call: {
|
|
847
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
848
|
+
name: 'numProperties',
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
role: 'function',
|
|
853
|
+
content: '3',
|
|
854
|
+
name: 'numProperties',
|
|
855
|
+
},
|
|
856
|
+
]);
|
|
857
|
+
return {
|
|
858
|
+
id: '3',
|
|
859
|
+
choices: [
|
|
860
|
+
{
|
|
861
|
+
index: 0,
|
|
862
|
+
finish_reason: 'stop',
|
|
863
|
+
message: {
|
|
864
|
+
role: 'assistant',
|
|
865
|
+
content: `there are 3 properties in {"a": 1, "b": 2, "c": 3}`,
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
created: Math.floor(Date.now() / 1000),
|
|
870
|
+
model: 'gpt-3.5-turbo',
|
|
871
|
+
object: 'chat.completion',
|
|
872
|
+
};
|
|
873
|
+
}),
|
|
874
|
+
runner.done(),
|
|
875
|
+
]);
|
|
876
|
+
expect(listener.messages).toEqual([
|
|
877
|
+
{
|
|
878
|
+
role: 'user',
|
|
879
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
role: 'assistant',
|
|
883
|
+
content: null,
|
|
884
|
+
function_call: { name: 'numProperties', arguments: '[{"a": 1, "b": 2, "c": 3}]' },
|
|
885
|
+
},
|
|
886
|
+
{ role: 'function', content: `must be an object`, name: 'numProperties' },
|
|
887
|
+
{
|
|
888
|
+
role: 'assistant',
|
|
889
|
+
content: null,
|
|
890
|
+
function_call: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' },
|
|
891
|
+
},
|
|
892
|
+
{ role: 'function', content: '3', name: 'numProperties' },
|
|
893
|
+
{ role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' },
|
|
894
|
+
]);
|
|
895
|
+
expect(listener.functionCallResults).toEqual([`must be an object`, '3']);
|
|
896
|
+
await listener.sanityCheck();
|
|
897
|
+
});
|
|
898
|
+
test('single function call', async () => {
|
|
899
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
900
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
901
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
902
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
903
|
+
model: 'gpt-3.5-turbo',
|
|
904
|
+
function_call: {
|
|
905
|
+
name: 'getWeather',
|
|
906
|
+
},
|
|
907
|
+
functions: [
|
|
908
|
+
{
|
|
909
|
+
function: function getWeather() {
|
|
910
|
+
return `it's raining`;
|
|
911
|
+
},
|
|
912
|
+
parameters: {},
|
|
913
|
+
description: 'gets the weather',
|
|
914
|
+
},
|
|
915
|
+
],
|
|
916
|
+
});
|
|
917
|
+
const listener = new RunnerListener(runner);
|
|
918
|
+
await Promise.all([
|
|
919
|
+
handleRequest(async (request) => {
|
|
920
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
921
|
+
return {
|
|
922
|
+
id: '1',
|
|
923
|
+
choices: [
|
|
924
|
+
{
|
|
925
|
+
index: 0,
|
|
926
|
+
finish_reason: 'function_call',
|
|
927
|
+
message: {
|
|
928
|
+
role: 'assistant',
|
|
929
|
+
content: null,
|
|
930
|
+
function_call: {
|
|
931
|
+
arguments: '',
|
|
932
|
+
name: 'getWeather',
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
created: Math.floor(Date.now() / 1000),
|
|
938
|
+
model: 'gpt-3.5-turbo',
|
|
939
|
+
object: 'chat.completion',
|
|
940
|
+
};
|
|
941
|
+
}),
|
|
942
|
+
runner.done(),
|
|
943
|
+
]);
|
|
944
|
+
expect(listener.messages).toEqual([
|
|
945
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
946
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
947
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
948
|
+
]);
|
|
949
|
+
expect(listener.functionCallResults).toEqual([`it's raining`]);
|
|
950
|
+
await listener.sanityCheck();
|
|
951
|
+
});
|
|
952
|
+
test('wrong function name', async () => {
|
|
953
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
954
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
955
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
956
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
957
|
+
model: 'gpt-3.5-turbo',
|
|
958
|
+
functions: [
|
|
959
|
+
{
|
|
960
|
+
function: function getWeather() {
|
|
961
|
+
return `it's raining`;
|
|
962
|
+
},
|
|
963
|
+
parameters: {},
|
|
964
|
+
description: 'gets the weather',
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
});
|
|
968
|
+
const listener = new RunnerListener(runner);
|
|
969
|
+
await Promise.all([
|
|
970
|
+
handleRequest(async (request) => {
|
|
971
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
972
|
+
return {
|
|
973
|
+
id: '1',
|
|
974
|
+
choices: [
|
|
975
|
+
{
|
|
976
|
+
index: 0,
|
|
977
|
+
finish_reason: 'function_call',
|
|
978
|
+
message: {
|
|
979
|
+
role: 'assistant',
|
|
980
|
+
content: null,
|
|
981
|
+
function_call: {
|
|
982
|
+
arguments: '',
|
|
983
|
+
name: 'get_weather',
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
],
|
|
988
|
+
created: Math.floor(Date.now() / 1000),
|
|
989
|
+
model: 'gpt-3.5-turbo',
|
|
990
|
+
object: 'chat.completion',
|
|
991
|
+
};
|
|
992
|
+
}),
|
|
993
|
+
handleRequest(async (request) => {
|
|
994
|
+
expect(request.messages).toEqual([
|
|
995
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
996
|
+
{
|
|
997
|
+
role: 'assistant',
|
|
998
|
+
content: null,
|
|
999
|
+
function_call: {
|
|
1000
|
+
arguments: '',
|
|
1001
|
+
name: 'get_weather',
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
role: 'function',
|
|
1006
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1007
|
+
name: 'get_weather',
|
|
1008
|
+
},
|
|
1009
|
+
]);
|
|
1010
|
+
return {
|
|
1011
|
+
id: '2',
|
|
1012
|
+
choices: [
|
|
1013
|
+
{
|
|
1014
|
+
index: 0,
|
|
1015
|
+
finish_reason: 'function_call',
|
|
1016
|
+
message: {
|
|
1017
|
+
role: 'assistant',
|
|
1018
|
+
content: null,
|
|
1019
|
+
function_call: {
|
|
1020
|
+
arguments: '',
|
|
1021
|
+
name: 'getWeather',
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
1026
|
+
created: Math.floor(Date.now() / 1000),
|
|
1027
|
+
model: 'gpt-3.5-turbo',
|
|
1028
|
+
object: 'chat.completion',
|
|
1029
|
+
};
|
|
1030
|
+
}),
|
|
1031
|
+
handleRequest(async (request) => {
|
|
1032
|
+
expect(request.messages).toEqual([
|
|
1033
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1034
|
+
{
|
|
1035
|
+
role: 'assistant',
|
|
1036
|
+
content: null,
|
|
1037
|
+
function_call: {
|
|
1038
|
+
arguments: '',
|
|
1039
|
+
name: 'get_weather',
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
role: 'function',
|
|
1044
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1045
|
+
name: 'get_weather',
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
role: 'assistant',
|
|
1049
|
+
content: null,
|
|
1050
|
+
function_call: {
|
|
1051
|
+
arguments: '',
|
|
1052
|
+
name: 'getWeather',
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
role: 'function',
|
|
1057
|
+
content: `it's raining`,
|
|
1058
|
+
name: 'getWeather',
|
|
1059
|
+
},
|
|
1060
|
+
]);
|
|
1061
|
+
return {
|
|
1062
|
+
id: '3',
|
|
1063
|
+
choices: [
|
|
1064
|
+
{
|
|
1065
|
+
index: 0,
|
|
1066
|
+
finish_reason: 'stop',
|
|
1067
|
+
message: {
|
|
1068
|
+
role: 'assistant',
|
|
1069
|
+
content: `it's raining`,
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
],
|
|
1073
|
+
created: Math.floor(Date.now() / 1000),
|
|
1074
|
+
model: 'gpt-3.5-turbo',
|
|
1075
|
+
object: 'chat.completion',
|
|
1076
|
+
};
|
|
1077
|
+
}),
|
|
1078
|
+
runner.done(),
|
|
1079
|
+
]);
|
|
1080
|
+
expect(listener.messages).toEqual([
|
|
1081
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1082
|
+
{ role: 'assistant', content: null, function_call: { name: 'get_weather', arguments: '' } },
|
|
1083
|
+
{
|
|
1084
|
+
role: 'function',
|
|
1085
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1086
|
+
name: 'get_weather',
|
|
1087
|
+
},
|
|
1088
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
1089
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
1090
|
+
{ role: 'assistant', content: "it's raining" },
|
|
1091
|
+
]);
|
|
1092
|
+
expect(listener.functionCallResults).toEqual([
|
|
1093
|
+
`Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1094
|
+
`it's raining`,
|
|
1095
|
+
]);
|
|
1096
|
+
await listener.sanityCheck();
|
|
1097
|
+
});
|
|
1098
|
+
test('wrong function name with single function call', async () => {
|
|
1099
|
+
const { fetch, handleRequest } = mockChatCompletionFetch();
|
|
1100
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1101
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1102
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1103
|
+
model: 'gpt-3.5-turbo',
|
|
1104
|
+
function_call: {
|
|
1105
|
+
name: 'getWeather',
|
|
1106
|
+
},
|
|
1107
|
+
functions: [
|
|
1108
|
+
{
|
|
1109
|
+
function: function getWeather() {
|
|
1110
|
+
return `it's raining`;
|
|
1111
|
+
},
|
|
1112
|
+
parameters: {},
|
|
1113
|
+
description: 'gets the weather',
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
});
|
|
1117
|
+
const listener = new RunnerListener(runner);
|
|
1118
|
+
await Promise.all([
|
|
1119
|
+
handleRequest(async (request) => {
|
|
1120
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1121
|
+
return {
|
|
1122
|
+
id: '1',
|
|
1123
|
+
choices: [
|
|
1124
|
+
{
|
|
1125
|
+
index: 0,
|
|
1126
|
+
finish_reason: 'function_call',
|
|
1127
|
+
message: {
|
|
1128
|
+
role: 'assistant',
|
|
1129
|
+
content: null,
|
|
1130
|
+
function_call: {
|
|
1131
|
+
arguments: '',
|
|
1132
|
+
name: 'get_weather',
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
created: Math.floor(Date.now() / 1000),
|
|
1138
|
+
model: 'gpt-3.5-turbo',
|
|
1139
|
+
object: 'chat.completion',
|
|
1140
|
+
};
|
|
1141
|
+
}),
|
|
1142
|
+
runner.done(),
|
|
1143
|
+
]);
|
|
1144
|
+
expect(listener.messages).toEqual([
|
|
1145
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1146
|
+
{ role: 'assistant', content: null, function_call: { name: 'get_weather', arguments: '' } },
|
|
1147
|
+
{
|
|
1148
|
+
role: 'function',
|
|
1149
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1150
|
+
name: 'get_weather',
|
|
1151
|
+
},
|
|
1152
|
+
]);
|
|
1153
|
+
expect(listener.functionCallResults).toEqual([
|
|
1154
|
+
`Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1155
|
+
]);
|
|
1156
|
+
await listener.sanityCheck();
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
describe('runFunctions with stream: true', () => {
|
|
1160
|
+
test('successful flow', async () => {
|
|
1161
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1162
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1163
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1164
|
+
stream: true,
|
|
1165
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1166
|
+
model: 'gpt-3.5-turbo',
|
|
1167
|
+
functions: [
|
|
1168
|
+
{
|
|
1169
|
+
function: function getWeather() {
|
|
1170
|
+
return `it's raining`;
|
|
1171
|
+
},
|
|
1172
|
+
parameters: {},
|
|
1173
|
+
description: 'gets the weather',
|
|
1174
|
+
},
|
|
1175
|
+
],
|
|
1176
|
+
});
|
|
1177
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1178
|
+
await Promise.all([
|
|
1179
|
+
handleRequest(async function* (request) {
|
|
1180
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1181
|
+
yield {
|
|
1182
|
+
id: '1',
|
|
1183
|
+
choices: [
|
|
1184
|
+
{
|
|
1185
|
+
index: 0,
|
|
1186
|
+
finish_reason: 'function_call',
|
|
1187
|
+
delta: {
|
|
1188
|
+
role: 'assistant',
|
|
1189
|
+
content: null,
|
|
1190
|
+
function_call: {
|
|
1191
|
+
arguments: '',
|
|
1192
|
+
name: 'getWeather',
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
created: Math.floor(Date.now() / 1000),
|
|
1198
|
+
model: 'gpt-3.5-turbo',
|
|
1199
|
+
object: 'chat.completion.chunk',
|
|
1200
|
+
};
|
|
1201
|
+
}),
|
|
1202
|
+
handleRequest(async function* (request) {
|
|
1203
|
+
expect(request.messages).toEqual([
|
|
1204
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1205
|
+
{
|
|
1206
|
+
role: 'assistant',
|
|
1207
|
+
content: null,
|
|
1208
|
+
function_call: {
|
|
1209
|
+
arguments: '',
|
|
1210
|
+
name: 'getWeather',
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
role: 'function',
|
|
1215
|
+
content: `it's raining`,
|
|
1216
|
+
name: 'getWeather',
|
|
1217
|
+
},
|
|
1218
|
+
]);
|
|
1219
|
+
for (const choice of contentChoiceDeltas(`it's raining`)) {
|
|
1220
|
+
yield {
|
|
1221
|
+
id: '2',
|
|
1222
|
+
choices: [choice],
|
|
1223
|
+
created: Math.floor(Date.now() / 1000),
|
|
1224
|
+
model: 'gpt-3.5-turbo',
|
|
1225
|
+
object: 'chat.completion',
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
}),
|
|
1229
|
+
runner.done(),
|
|
1230
|
+
]);
|
|
1231
|
+
expect(listener.eventMessages).toEqual([
|
|
1232
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
1233
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
1234
|
+
{ role: 'assistant', content: "it's raining" },
|
|
1235
|
+
]);
|
|
1236
|
+
expect(listener.eventFunctionCallResults).toEqual([`it's raining`]);
|
|
1237
|
+
await listener.sanityCheck();
|
|
1238
|
+
});
|
|
1239
|
+
test('flow with abort', async () => {
|
|
1240
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1241
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1242
|
+
const controller = new AbortController();
|
|
1243
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1244
|
+
stream: true,
|
|
1245
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1246
|
+
model: 'gpt-3.5-turbo',
|
|
1247
|
+
functions: [
|
|
1248
|
+
{
|
|
1249
|
+
function: function getWeather() {
|
|
1250
|
+
return `it's raining`;
|
|
1251
|
+
},
|
|
1252
|
+
parameters: {},
|
|
1253
|
+
description: 'gets the weather',
|
|
1254
|
+
},
|
|
1255
|
+
],
|
|
1256
|
+
}, { signal: controller.signal });
|
|
1257
|
+
runner.on('functionCallResult', () => controller.abort());
|
|
1258
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1259
|
+
await handleRequest(async function* (request) {
|
|
1260
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1261
|
+
yield {
|
|
1262
|
+
id: '1',
|
|
1263
|
+
choices: [
|
|
1264
|
+
{
|
|
1265
|
+
index: 0,
|
|
1266
|
+
finish_reason: 'function_call',
|
|
1267
|
+
delta: {
|
|
1268
|
+
role: 'assistant',
|
|
1269
|
+
content: null,
|
|
1270
|
+
function_call: {
|
|
1271
|
+
arguments: '',
|
|
1272
|
+
name: 'getWeather',
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
],
|
|
1277
|
+
created: Math.floor(Date.now() / 1000),
|
|
1278
|
+
model: 'gpt-3.5-turbo',
|
|
1279
|
+
object: 'chat.completion',
|
|
1280
|
+
};
|
|
1281
|
+
});
|
|
1282
|
+
await runner.done().catch(() => { });
|
|
1283
|
+
expect(listener.eventMessages).toEqual([
|
|
1284
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
1285
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
1286
|
+
]);
|
|
1287
|
+
expect(listener.eventFunctionCallResults).toEqual([`it's raining`]);
|
|
1288
|
+
await listener.sanityCheck({ error: 'Request was aborted.' });
|
|
1289
|
+
expect(runner.aborted).toBe(true);
|
|
1290
|
+
});
|
|
1291
|
+
test('successful flow with parse', async () => {
|
|
1292
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1293
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1294
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1295
|
+
stream: true,
|
|
1296
|
+
messages: [
|
|
1297
|
+
{
|
|
1298
|
+
role: 'user',
|
|
1299
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1300
|
+
},
|
|
1301
|
+
],
|
|
1302
|
+
model: 'gpt-3.5-turbo',
|
|
1303
|
+
functions: [
|
|
1304
|
+
new ParsingFunction({
|
|
1305
|
+
name: 'numProperties',
|
|
1306
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
1307
|
+
parameters: { type: 'object' },
|
|
1308
|
+
parse: (str) => {
|
|
1309
|
+
const result = JSON.parse(str);
|
|
1310
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
1311
|
+
throw new Error('must be an object');
|
|
1312
|
+
}
|
|
1313
|
+
return result;
|
|
1314
|
+
},
|
|
1315
|
+
description: 'gets the number of properties on an object',
|
|
1316
|
+
}),
|
|
1317
|
+
],
|
|
1318
|
+
});
|
|
1319
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1320
|
+
await Promise.all([
|
|
1321
|
+
handleRequest(async function* (request) {
|
|
1322
|
+
expect(request.messages).toEqual([
|
|
1323
|
+
{
|
|
1324
|
+
role: 'user',
|
|
1325
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1326
|
+
},
|
|
1327
|
+
]);
|
|
1328
|
+
yield {
|
|
1329
|
+
id: '1',
|
|
1330
|
+
choices: [
|
|
1331
|
+
{
|
|
1332
|
+
index: 0,
|
|
1333
|
+
finish_reason: 'function_call',
|
|
1334
|
+
delta: {
|
|
1335
|
+
role: 'assistant',
|
|
1336
|
+
content: null,
|
|
1337
|
+
function_call: {
|
|
1338
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
1339
|
+
name: 'numProperties',
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
],
|
|
1344
|
+
created: Math.floor(Date.now() / 1000),
|
|
1345
|
+
model: 'gpt-3.5-turbo',
|
|
1346
|
+
object: 'chat.completion',
|
|
1347
|
+
};
|
|
1348
|
+
}),
|
|
1349
|
+
handleRequest(async function* (request) {
|
|
1350
|
+
expect(request.messages).toEqual([
|
|
1351
|
+
{
|
|
1352
|
+
role: 'user',
|
|
1353
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
role: 'assistant',
|
|
1357
|
+
content: null,
|
|
1358
|
+
function_call: {
|
|
1359
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
1360
|
+
name: 'numProperties',
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
role: 'function',
|
|
1365
|
+
content: '3',
|
|
1366
|
+
name: 'numProperties',
|
|
1367
|
+
},
|
|
1368
|
+
]);
|
|
1369
|
+
for (const choice of contentChoiceDeltas(`there are 3 properties in {"a": 1, "b": 2, "c": 3}`)) {
|
|
1370
|
+
yield {
|
|
1371
|
+
id: '2',
|
|
1372
|
+
choices: [choice],
|
|
1373
|
+
created: Math.floor(Date.now() / 1000),
|
|
1374
|
+
model: 'gpt-3.5-turbo',
|
|
1375
|
+
object: 'chat.completion',
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}),
|
|
1379
|
+
runner.done(),
|
|
1380
|
+
]);
|
|
1381
|
+
expect(listener.eventMessages).toEqual([
|
|
1382
|
+
{
|
|
1383
|
+
role: 'assistant',
|
|
1384
|
+
content: null,
|
|
1385
|
+
function_call: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' },
|
|
1386
|
+
},
|
|
1387
|
+
{ role: 'function', content: '3', name: 'numProperties' },
|
|
1388
|
+
{ role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' },
|
|
1389
|
+
]);
|
|
1390
|
+
expect(listener.eventFunctionCallResults).toEqual(['3']);
|
|
1391
|
+
await listener.sanityCheck();
|
|
1392
|
+
});
|
|
1393
|
+
test('flow with parse error', async () => {
|
|
1394
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1395
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1396
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1397
|
+
stream: true,
|
|
1398
|
+
messages: [
|
|
1399
|
+
{
|
|
1400
|
+
role: 'user',
|
|
1401
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1402
|
+
},
|
|
1403
|
+
],
|
|
1404
|
+
model: 'gpt-3.5-turbo',
|
|
1405
|
+
functions: [
|
|
1406
|
+
new ParsingFunction({
|
|
1407
|
+
name: 'numProperties',
|
|
1408
|
+
function: (obj) => String(Object.keys(obj).length),
|
|
1409
|
+
parameters: { type: 'object' },
|
|
1410
|
+
parse: (str) => {
|
|
1411
|
+
const result = JSON.parse(str);
|
|
1412
|
+
if (!(result instanceof Object) || Array.isArray(result)) {
|
|
1413
|
+
throw new Error('must be an object');
|
|
1414
|
+
}
|
|
1415
|
+
return result;
|
|
1416
|
+
},
|
|
1417
|
+
description: 'gets the number of properties on an object',
|
|
1418
|
+
}),
|
|
1419
|
+
],
|
|
1420
|
+
});
|
|
1421
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1422
|
+
await Promise.all([
|
|
1423
|
+
handleRequest(async function* (request) {
|
|
1424
|
+
expect(request.messages).toEqual([
|
|
1425
|
+
{
|
|
1426
|
+
role: 'user',
|
|
1427
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1428
|
+
},
|
|
1429
|
+
]);
|
|
1430
|
+
for (const choice of functionCallDeltas('[{"a": 1, "b": 2, "c": 3}]', { name: 'numProperties' })) {
|
|
1431
|
+
yield {
|
|
1432
|
+
id: '1',
|
|
1433
|
+
choices: [choice],
|
|
1434
|
+
created: Math.floor(Date.now() / 1000),
|
|
1435
|
+
model: 'gpt-3.5-turbo',
|
|
1436
|
+
object: 'chat.completion',
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
}),
|
|
1440
|
+
handleRequest(async function* (request) {
|
|
1441
|
+
expect(request.messages).toEqual([
|
|
1442
|
+
{
|
|
1443
|
+
role: 'user',
|
|
1444
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
role: 'assistant',
|
|
1448
|
+
content: null,
|
|
1449
|
+
function_call: {
|
|
1450
|
+
arguments: '[{"a": 1, "b": 2, "c": 3}]',
|
|
1451
|
+
name: 'numProperties',
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
role: 'function',
|
|
1456
|
+
content: `must be an object`,
|
|
1457
|
+
name: 'numProperties',
|
|
1458
|
+
},
|
|
1459
|
+
]);
|
|
1460
|
+
for (const choice of functionCallDeltas('{"a": 1, "b": 2, "c": 3}', { name: 'numProperties' })) {
|
|
1461
|
+
yield {
|
|
1462
|
+
id: '2',
|
|
1463
|
+
choices: [choice],
|
|
1464
|
+
created: Math.floor(Date.now() / 1000),
|
|
1465
|
+
model: 'gpt-3.5-turbo',
|
|
1466
|
+
object: 'chat.completion',
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}),
|
|
1470
|
+
handleRequest(async function* (request) {
|
|
1471
|
+
expect(request.messages).toEqual([
|
|
1472
|
+
{
|
|
1473
|
+
role: 'user',
|
|
1474
|
+
content: 'can you tell me how many properties are in {"a": 1, "b": 2, "c": 3}',
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
role: 'assistant',
|
|
1478
|
+
content: null,
|
|
1479
|
+
function_call: {
|
|
1480
|
+
arguments: '[{"a": 1, "b": 2, "c": 3}]',
|
|
1481
|
+
name: 'numProperties',
|
|
1482
|
+
},
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
role: 'function',
|
|
1486
|
+
content: `must be an object`,
|
|
1487
|
+
name: 'numProperties',
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
role: 'assistant',
|
|
1491
|
+
content: null,
|
|
1492
|
+
function_call: {
|
|
1493
|
+
arguments: '{"a": 1, "b": 2, "c": 3}',
|
|
1494
|
+
name: 'numProperties',
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
role: 'function',
|
|
1499
|
+
content: '3',
|
|
1500
|
+
name: 'numProperties',
|
|
1501
|
+
},
|
|
1502
|
+
]);
|
|
1503
|
+
for (const choice of contentChoiceDeltas(`there are 3 properties in {"a": 1, "b": 2, "c": 3}`)) {
|
|
1504
|
+
yield {
|
|
1505
|
+
id: '3',
|
|
1506
|
+
choices: [choice],
|
|
1507
|
+
created: Math.floor(Date.now() / 1000),
|
|
1508
|
+
model: 'gpt-3.5-turbo',
|
|
1509
|
+
object: 'chat.completion',
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}),
|
|
1513
|
+
runner.done(),
|
|
1514
|
+
]);
|
|
1515
|
+
expect(listener.eventMessages).toEqual([
|
|
1516
|
+
{
|
|
1517
|
+
role: 'assistant',
|
|
1518
|
+
content: null,
|
|
1519
|
+
function_call: { name: 'numProperties', arguments: '[{"a": 1, "b": 2, "c": 3}]' },
|
|
1520
|
+
},
|
|
1521
|
+
{ role: 'function', content: `must be an object`, name: 'numProperties' },
|
|
1522
|
+
{
|
|
1523
|
+
role: 'assistant',
|
|
1524
|
+
content: null,
|
|
1525
|
+
function_call: { name: 'numProperties', arguments: '{"a": 1, "b": 2, "c": 3}' },
|
|
1526
|
+
},
|
|
1527
|
+
{ role: 'function', content: '3', name: 'numProperties' },
|
|
1528
|
+
{ role: 'assistant', content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}' },
|
|
1529
|
+
]);
|
|
1530
|
+
expect(listener.eventFunctionCallResults).toEqual([`must be an object`, '3']);
|
|
1531
|
+
await listener.sanityCheck();
|
|
1532
|
+
});
|
|
1533
|
+
test('single function call', async () => {
|
|
1534
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1535
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1536
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1537
|
+
stream: true,
|
|
1538
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1539
|
+
model: 'gpt-3.5-turbo',
|
|
1540
|
+
function_call: {
|
|
1541
|
+
name: 'getWeather',
|
|
1542
|
+
},
|
|
1543
|
+
functions: [
|
|
1544
|
+
{
|
|
1545
|
+
function: function getWeather() {
|
|
1546
|
+
return `it's raining`;
|
|
1547
|
+
},
|
|
1548
|
+
parameters: {},
|
|
1549
|
+
description: 'gets the weather',
|
|
1550
|
+
},
|
|
1551
|
+
],
|
|
1552
|
+
});
|
|
1553
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1554
|
+
await Promise.all([
|
|
1555
|
+
handleRequest(async function* (request) {
|
|
1556
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1557
|
+
yield {
|
|
1558
|
+
id: '1',
|
|
1559
|
+
choices: [
|
|
1560
|
+
{
|
|
1561
|
+
index: 0,
|
|
1562
|
+
finish_reason: 'function_call',
|
|
1563
|
+
delta: {
|
|
1564
|
+
role: 'assistant',
|
|
1565
|
+
content: null,
|
|
1566
|
+
function_call: {
|
|
1567
|
+
arguments: '',
|
|
1568
|
+
name: 'getWeather',
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
],
|
|
1573
|
+
created: Math.floor(Date.now() / 1000),
|
|
1574
|
+
model: 'gpt-3.5-turbo',
|
|
1575
|
+
object: 'chat.completion',
|
|
1576
|
+
};
|
|
1577
|
+
}),
|
|
1578
|
+
runner.done(),
|
|
1579
|
+
]);
|
|
1580
|
+
expect(listener.eventMessages).toEqual([
|
|
1581
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
1582
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
1583
|
+
]);
|
|
1584
|
+
expect(listener.eventFunctionCallResults).toEqual([`it's raining`]);
|
|
1585
|
+
await listener.sanityCheck();
|
|
1586
|
+
});
|
|
1587
|
+
test('wrong function name', async () => {
|
|
1588
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1589
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1590
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1591
|
+
stream: true,
|
|
1592
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1593
|
+
model: 'gpt-3.5-turbo',
|
|
1594
|
+
functions: [
|
|
1595
|
+
{
|
|
1596
|
+
function: function getWeather() {
|
|
1597
|
+
return `it's raining`;
|
|
1598
|
+
},
|
|
1599
|
+
parameters: {},
|
|
1600
|
+
description: 'gets the weather',
|
|
1601
|
+
},
|
|
1602
|
+
],
|
|
1603
|
+
});
|
|
1604
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1605
|
+
await Promise.all([
|
|
1606
|
+
handleRequest(async function* (request) {
|
|
1607
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1608
|
+
yield {
|
|
1609
|
+
id: '1',
|
|
1610
|
+
choices: [
|
|
1611
|
+
{
|
|
1612
|
+
index: 0,
|
|
1613
|
+
finish_reason: 'function_call',
|
|
1614
|
+
delta: {
|
|
1615
|
+
role: 'assistant',
|
|
1616
|
+
content: null,
|
|
1617
|
+
function_call: {
|
|
1618
|
+
arguments: '',
|
|
1619
|
+
name: 'get_weather',
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
],
|
|
1624
|
+
created: Math.floor(Date.now() / 1000),
|
|
1625
|
+
model: 'gpt-3.5-turbo',
|
|
1626
|
+
object: 'chat.completion',
|
|
1627
|
+
};
|
|
1628
|
+
}),
|
|
1629
|
+
handleRequest(async function* (request) {
|
|
1630
|
+
expect(request.messages).toEqual([
|
|
1631
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1632
|
+
{
|
|
1633
|
+
role: 'assistant',
|
|
1634
|
+
content: null,
|
|
1635
|
+
function_call: {
|
|
1636
|
+
arguments: '',
|
|
1637
|
+
name: 'get_weather',
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
role: 'function',
|
|
1642
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1643
|
+
name: 'get_weather',
|
|
1644
|
+
},
|
|
1645
|
+
]);
|
|
1646
|
+
yield {
|
|
1647
|
+
id: '2',
|
|
1648
|
+
choices: [
|
|
1649
|
+
{
|
|
1650
|
+
index: 0,
|
|
1651
|
+
finish_reason: 'function_call',
|
|
1652
|
+
delta: {
|
|
1653
|
+
role: 'assistant',
|
|
1654
|
+
content: null,
|
|
1655
|
+
function_call: {
|
|
1656
|
+
arguments: '',
|
|
1657
|
+
name: 'getWeather',
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
created: Math.floor(Date.now() / 1000),
|
|
1663
|
+
model: 'gpt-3.5-turbo',
|
|
1664
|
+
object: 'chat.completion',
|
|
1665
|
+
};
|
|
1666
|
+
}),
|
|
1667
|
+
handleRequest(async function* (request) {
|
|
1668
|
+
expect(request.messages).toEqual([
|
|
1669
|
+
{ role: 'user', content: 'tell me what the weather is like' },
|
|
1670
|
+
{
|
|
1671
|
+
role: 'assistant',
|
|
1672
|
+
content: null,
|
|
1673
|
+
function_call: {
|
|
1674
|
+
arguments: '',
|
|
1675
|
+
name: 'get_weather',
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
role: 'function',
|
|
1680
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1681
|
+
name: 'get_weather',
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
role: 'assistant',
|
|
1685
|
+
content: null,
|
|
1686
|
+
function_call: {
|
|
1687
|
+
arguments: '',
|
|
1688
|
+
name: 'getWeather',
|
|
1689
|
+
},
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
role: 'function',
|
|
1693
|
+
content: `it's raining`,
|
|
1694
|
+
name: 'getWeather',
|
|
1695
|
+
},
|
|
1696
|
+
]);
|
|
1697
|
+
for (const choice of contentChoiceDeltas(`it's raining`)) {
|
|
1698
|
+
yield {
|
|
1699
|
+
id: '3',
|
|
1700
|
+
choices: [choice],
|
|
1701
|
+
created: Math.floor(Date.now() / 1000),
|
|
1702
|
+
model: 'gpt-3.5-turbo',
|
|
1703
|
+
object: 'chat.completion',
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
}),
|
|
1707
|
+
runner.done(),
|
|
1708
|
+
]);
|
|
1709
|
+
expect(listener.eventMessages).toEqual([
|
|
1710
|
+
{ role: 'assistant', content: null, function_call: { name: 'get_weather', arguments: '' } },
|
|
1711
|
+
{
|
|
1712
|
+
role: 'function',
|
|
1713
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1714
|
+
name: 'get_weather',
|
|
1715
|
+
},
|
|
1716
|
+
{ role: 'assistant', content: null, function_call: { name: 'getWeather', arguments: '' } },
|
|
1717
|
+
{ role: 'function', content: `it's raining`, name: 'getWeather' },
|
|
1718
|
+
{ role: 'assistant', content: "it's raining" },
|
|
1719
|
+
]);
|
|
1720
|
+
expect(listener.eventFunctionCallResults).toEqual([
|
|
1721
|
+
`Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1722
|
+
`it's raining`,
|
|
1723
|
+
]);
|
|
1724
|
+
await listener.sanityCheck();
|
|
1725
|
+
});
|
|
1726
|
+
test('wrong function name with single function call', async () => {
|
|
1727
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1728
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1729
|
+
const runner = openai.beta.chat.completions.runFunctions({
|
|
1730
|
+
stream: true,
|
|
1731
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1732
|
+
model: 'gpt-3.5-turbo',
|
|
1733
|
+
function_call: {
|
|
1734
|
+
name: 'getWeather',
|
|
1735
|
+
},
|
|
1736
|
+
functions: [
|
|
1737
|
+
{
|
|
1738
|
+
function: function getWeather() {
|
|
1739
|
+
return `it's raining`;
|
|
1740
|
+
},
|
|
1741
|
+
parameters: {},
|
|
1742
|
+
description: 'gets the weather',
|
|
1743
|
+
},
|
|
1744
|
+
],
|
|
1745
|
+
});
|
|
1746
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1747
|
+
await Promise.all([
|
|
1748
|
+
handleRequest(async function* (request) {
|
|
1749
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1750
|
+
yield {
|
|
1751
|
+
id: '1',
|
|
1752
|
+
choices: [
|
|
1753
|
+
{
|
|
1754
|
+
index: 0,
|
|
1755
|
+
finish_reason: 'function_call',
|
|
1756
|
+
delta: {
|
|
1757
|
+
role: 'assistant',
|
|
1758
|
+
content: null,
|
|
1759
|
+
function_call: {
|
|
1760
|
+
arguments: '',
|
|
1761
|
+
name: 'get_weather',
|
|
1762
|
+
},
|
|
1763
|
+
},
|
|
1764
|
+
},
|
|
1765
|
+
],
|
|
1766
|
+
created: Math.floor(Date.now() / 1000),
|
|
1767
|
+
model: 'gpt-3.5-turbo',
|
|
1768
|
+
object: 'chat.completion',
|
|
1769
|
+
};
|
|
1770
|
+
}),
|
|
1771
|
+
runner.done(),
|
|
1772
|
+
]);
|
|
1773
|
+
expect(listener.eventMessages).toEqual([
|
|
1774
|
+
{ role: 'assistant', content: null, function_call: { name: 'get_weather', arguments: '' } },
|
|
1775
|
+
{
|
|
1776
|
+
role: 'function',
|
|
1777
|
+
content: `Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1778
|
+
name: 'get_weather',
|
|
1779
|
+
},
|
|
1780
|
+
]);
|
|
1781
|
+
expect(listener.eventFunctionCallResults).toEqual([
|
|
1782
|
+
`Invalid function_call: "get_weather". Available options are: "getWeather". Please try again`,
|
|
1783
|
+
]);
|
|
1784
|
+
await listener.sanityCheck();
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
describe('stream', () => {
|
|
1788
|
+
test('successful flow', async () => {
|
|
1789
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1790
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1791
|
+
const runner = openai.beta.chat.completions.stream({
|
|
1792
|
+
stream: true,
|
|
1793
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1794
|
+
model: 'gpt-3.5-turbo',
|
|
1795
|
+
});
|
|
1796
|
+
const listener = new StreamingRunnerListener(runner);
|
|
1797
|
+
await Promise.all([
|
|
1798
|
+
handleRequest(async function* (request) {
|
|
1799
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1800
|
+
for (const choice of contentChoiceDeltas(`The weather is great today!`)) {
|
|
1801
|
+
yield {
|
|
1802
|
+
id: '1',
|
|
1803
|
+
choices: [choice],
|
|
1804
|
+
created: Math.floor(Date.now() / 1000),
|
|
1805
|
+
model: 'gpt-3.5-turbo',
|
|
1806
|
+
object: 'chat.completion',
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
}),
|
|
1810
|
+
runner.done(),
|
|
1811
|
+
]);
|
|
1812
|
+
expect(listener.finalMessage).toEqual({ role: 'assistant', content: 'The weather is great today!' });
|
|
1813
|
+
await listener.sanityCheck();
|
|
1814
|
+
});
|
|
1815
|
+
test('toReadableStream and fromReadableStream', async () => {
|
|
1816
|
+
const { fetch, handleRequest } = mockStreamingChatCompletionFetch();
|
|
1817
|
+
const openai = new OpenAI({ apiKey: 'something1234', baseURL: 'http://127.0.0.1:4010', fetch });
|
|
1818
|
+
const runner = openai.beta.chat.completions.stream({
|
|
1819
|
+
stream: true,
|
|
1820
|
+
messages: [{ role: 'user', content: 'tell me what the weather is like' }],
|
|
1821
|
+
model: 'gpt-3.5-turbo',
|
|
1822
|
+
});
|
|
1823
|
+
const proxied = ChatCompletionStreamingRunner.fromReadableStream(runner.toReadableStream());
|
|
1824
|
+
const listener = new StreamingRunnerListener(proxied);
|
|
1825
|
+
await Promise.all([
|
|
1826
|
+
handleRequest(async function* (request) {
|
|
1827
|
+
expect(request.messages).toEqual([{ role: 'user', content: 'tell me what the weather is like' }]);
|
|
1828
|
+
for (const choice of contentChoiceDeltas(`The weather is great today!`)) {
|
|
1829
|
+
yield {
|
|
1830
|
+
id: '1',
|
|
1831
|
+
choices: [choice],
|
|
1832
|
+
created: Math.floor(Date.now() / 1000),
|
|
1833
|
+
model: 'gpt-3.5-turbo',
|
|
1834
|
+
object: 'chat.completion',
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
}),
|
|
1838
|
+
proxied.done(),
|
|
1839
|
+
]);
|
|
1840
|
+
expect(listener.finalMessage).toEqual({ role: 'assistant', content: 'The weather is great today!' });
|
|
1841
|
+
await listener.sanityCheck();
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
//# sourceMappingURL=ChatCompletionRunFunctions.test.mjs.map
|