Skip to content

Commit 95694b6

Browse files
committed
update to new async iterable pattern
update types start setting up tests get non streaming working begin work on modifying anthropic tests to openai update models
1 parent 8e55b1e commit 95694b6

File tree

9 files changed

+1386
-93
lines changed

9 files changed

+1386
-93
lines changed

examples/tool-calls-beta-zod.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import OpenAI from 'openai';
44
import { betaZodTool } from 'openai/helpers/beta/zod';
5-
// import { BetaToolUseBlock } from 'openai/helpers/beta';
65
import { z } from 'zod';
76

87
const client = new OpenAI();
@@ -12,7 +11,7 @@ async function main() {
1211
messages: [
1312
{
1413
role: 'user',
15-
content: `I'm planning a trip to San Francisco and I need some information. Can you help me with the weather, current time, and currency exchange rates (from EUR)? Please use parallel tool use`,
14+
content: `I'm planning a trip to San Francisco and I need some information. Can you help me with the weather, current time, and currency exchange rates (from EUR)? Please use parallel tool use.`,
1615
},
1716
],
1817
tools: [
@@ -57,6 +56,8 @@ async function main() {
5756
console.log(`\n🚀 Running tools...\n`);
5857

5958
for await (const message of runner) {
59+
if (!message) continue;
60+
6061
console.log(`┌─ Message ${message.id} `.padEnd(process.stdout.columns, '─'));
6162
console.log();
6263

@@ -99,8 +100,6 @@ async function main() {
99100
console.log();
100101
}
101102
}
102-
103-
console.log(JSON.stringify(runner.params, null, 2));
104103
}
105104

106105
main();

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@
2626
"lint": "./scripts/lint",
2727
"fix": "./scripts/format"
2828
},
29-
"dependencies": {},
29+
"dependencies": {
30+
"nock": "^14.0.10"
31+
},
3032
"devDependencies": {
3133
"@arethetypeswrong/cli": "^0.17.0",
3234
"@swc/core": "^1.3.102",
3335
"@swc/jest": "^0.2.29",
3436
"@types/jest": "^29.4.0",
35-
"@types/ws": "^8.5.13",
3637
"@types/node": "^20.17.6",
38+
"@types/ws": "^8.5.13",
3739
"@typescript-eslint/eslint-plugin": "8.31.1",
3840
"@typescript-eslint/parser": "8.31.1",
39-
"deep-object-diff": "^1.1.9",
4041
"@typescript-eslint/typescript-estree": "8.31.1",
42+
"deep-object-diff": "^1.1.9",
4143
"eslint": "^9.20.1",
4244
"eslint-plugin-prettier": "^5.4.1",
4345
"eslint-plugin-unused-imports": "^4.1.4",
@@ -53,9 +55,9 @@
5355
"tsconfig-paths": "^4.0.0",
5456
"tslib": "^2.8.1",
5557
"typescript": "5.8.3",
58+
"typescript-eslint": "8.31.1",
5659
"ws": "^8.18.0",
57-
"zod": "^3.25 || ^4.0",
58-
"typescript-eslint": "8.31.1"
60+
"zod": "^3.25 || ^4.0"
5961
},
6062
"bin": {
6163
"openai": "bin/cli"

src/lib/beta/BetaToolRunner.ts

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { OpenAI } from '../..';
1+
import { OpenAI } from '../..';
22
import { OpenAIError } from '../../core/error';
33
import { buildHeaders } from '../../internal/headers';
44
import type { RequestOptions } from '../../internal/request-options';
55
import type {
66
ChatCompletion,
77
ChatCompletionCreateParams,
8+
ChatCompletionMessage,
89
ChatCompletionMessageParam,
910
ChatCompletionStream,
1011
ChatCompletionTool,
@@ -45,14 +46,18 @@ export class BetaToolRunner<Stream extends boolean> {
4546
/** Current state containing the request parameters */
4647
#state: { params: BetaToolRunnerParams };
4748
#options: BetaToolRunnerRequestOptions;
48-
/** Promise for the last message received from the assistant */
49-
#message?: Promise<ChatCompletion> | undefined;
49+
/**
50+
* Promise for the last message received from the assistant.
51+
*
52+
* This resolves to undefined in non-streaming mode if there are no choices provided.
53+
*/
54+
#message?: Promise<ChatCompletionMessage> | undefined;
5055
/** Cached tool response to avoid redundant executions */
5156
#toolResponse?: Promise<null | ChatCompletionToolMessageParam[]> | undefined;
5257
/** Promise resolvers for waiting on completion */
5358
#completion: {
54-
promise: Promise<ChatCompletion>;
55-
resolve: (value: ChatCompletion) => void;
59+
promise: Promise<ChatCompletionMessage>;
60+
resolve: (value: ChatCompletionMessage) => void;
5661
reject: (reason?: any) => void;
5762
};
5863
/** Number of iterations (API requests) made so far */
@@ -83,8 +88,7 @@ export class BetaToolRunner<Stream extends boolean> {
8388
async *[Symbol.asyncIterator](): AsyncIterator<
8489
Stream extends true ?
8590
ChatCompletionStream // TODO: for now!
86-
: Stream extends false ? ChatCompletion
87-
: ChatCompletionCreateParams | ChatCompletionCreateParams
91+
: ChatCompletion | undefined
8892
> {
8993
if (this.#consumed) {
9094
throw new OpenAIError('Cannot iterate over a consumed stream');
@@ -96,7 +100,7 @@ export class BetaToolRunner<Stream extends boolean> {
96100

97101
try {
98102
while (true) {
99-
let stream: any;
103+
let stream: ChatCompletionStream<null> | undefined;
100104
try {
101105
if (
102106
this.#state.params.max_iterations &&
@@ -111,44 +115,48 @@ export class BetaToolRunner<Stream extends boolean> {
111115
this.#iterationCount++;
112116

113117
const { ...params } = this.#state.params;
118+
const apiParams = { ...params };
119+
delete apiParams.max_iterations; // our own param
120+
114121
if (params.stream) {
115-
stream = this.client.beta.chat.completions.stream({ ...params, stream: true }, this.#options);
122+
stream = this.client.beta.chat.completions.stream({ ...apiParams, stream: true }, this.#options);
116123
this.#message = stream.finalMessage();
124+
117125
// Make sure that this promise doesn't throw before we get the option to do something about it.
118126
// Error will be caught when we call await this.#message ultimately
119127
this.#message?.catch(() => {});
120128
yield stream as any;
121129
} else {
122-
this.#message = this.client.beta.chat.completions.create(
130+
const currentCompletion = this.client.beta.chat.completions.create(
123131
{
132+
...apiParams, // spread and explicit so we get better types
124133
stream: false,
125134
tools: params.tools,
126135
messages: params.messages,
127136
model: params.model,
128137
},
129138
this.#options,
130139
);
131-
yield this.#message as any;
132-
}
133140

134-
if (!this.#message) {
135-
throw new Error('No message created'); // TODO: use better error
141+
yield currentCompletion as any;
142+
143+
this.#message = currentCompletion.then((resp) => resp.choices.at(0)!.message);
136144
}
137145

138-
// TODO: we should probably hit the user with a callback or somehow offer for them to choice between the choices
139-
if (!this.#message) {
140-
throw new Error('No choices found in message'); // TODO: use better error
146+
const prevMessage = await this.#message;
147+
148+
if (!prevMessage) {
149+
throw new OpenAIError('ToolRunner concluded without a message from the server');
141150
}
142151

143152
if (!this.#mutated) {
144-
const completion = await this.#message;
145153
// TODO: what if it is empty?
146-
if (completion?.choices && completion.choices.length > 0 && completion.choices[0]!.message) {
147-
this.#state.params.messages.push(completion.choices[0]!.message);
154+
if (prevMessage) {
155+
this.#state.params.messages.push(prevMessage);
148156
}
149157
}
150158

151-
const toolMessages = await this.#generateToolResponse(await this.#message);
159+
const toolMessages = await this.#generateToolResponse(prevMessage);
152160
if (toolMessages) {
153161
for (const toolMessage of toolMessages) {
154162
this.#state.params.messages.push(toolMessage);
@@ -228,15 +236,16 @@ export class BetaToolRunner<Stream extends boolean> {
228236
* }
229237
*/
230238
async generateToolResponse() {
231-
const message = (await this.#message) ?? this.params.messages.at(-1);
239+
// The most recent message from the assistant. This prev had this.params.messages.at(-1) but I think that's wrong.
240+
const message = await this.#message;
232241
if (!message) {
233242
return null;
234243
}
235244
return this.#generateToolResponse(message);
236245
}
237246

238-
async #generateToolResponse(lastMessage: ChatCompletion | ChatCompletionMessageParam) {
239-
if (this.#toolResponse !== undefined) {
247+
async #generateToolResponse(lastMessage: ChatCompletionMessage) {
248+
if (this.#toolResponse) {
240249
return this.#toolResponse;
241250
}
242251
const toolsResponse = generateToolResponse(
@@ -263,8 +272,8 @@ export class BetaToolRunner<Stream extends boolean> {
263272
* const finalMessage = await runner.done();
264273
* console.log('Final response:', finalMessage.content);
265274
*/
266-
done(): Promise<ChatCompletion> {
267-
return this.#completion.promise;
275+
done(): Promise<ChatCompletionMessage> {
276+
return this.#completion.promise; // TODO: find a more type safe way to do this
268277
}
269278

270279
/**
@@ -280,7 +289,7 @@ export class BetaToolRunner<Stream extends boolean> {
280289
* const finalMessage = await runner.runUntilDone();
281290
* console.log('Final response:', finalMessage.content);
282291
*/
283-
async runUntilDone(): Promise<ChatCompletion> {
292+
async runUntilDone(): Promise<ChatCompletionMessage> {
284293
// If not yet consumed, start consuming and wait for completion
285294
if (!this.#consumed) {
286295
for await (const _ of this) {
@@ -334,27 +343,18 @@ export class BetaToolRunner<Stream extends boolean> {
334343
* Makes the ToolRunner directly awaitable, equivalent to calling .runUntilDone()
335344
* This allows using `await runner` instead of `await runner.runUntilDone()`
336345
*/
337-
then<TResult1 = ChatCompletion, TResult2 = never>(
338-
onfulfilled?: ((value: ChatCompletion) => TResult1 | PromiseLike<TResult1>) | undefined | null,
346+
then<TResult1 = ChatCompletionMessage, TResult2 = never>( // TODO: make sure these types are OK
347+
onfulfilled?: ((value: ChatCompletionMessage) => TResult1 | PromiseLike<TResult1>) | undefined | null,
339348
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
340349
): Promise<TResult1 | TResult2> {
341350
return this.runUntilDone().then(onfulfilled, onrejected);
342351
}
343352
}
344353

345354
async function generateToolResponse(
346-
params: ChatCompletion | ChatCompletionMessageParam,
355+
lastMessage: ChatCompletionMessage,
347356
tools: BetaRunnableTool<any>[],
348357
): Promise<null | ChatCompletionToolMessageParam[]> {
349-
if (!('choices' in params)) {
350-
return null;
351-
}
352-
const { choices } = params;
353-
const lastMessage = choices[0]?.message;
354-
if (!lastMessage) {
355-
return null;
356-
}
357-
358358
// Only process if the last message is from the assistant and has tool use blocks
359359
if (
360360
!lastMessage ||

src/resources/beta/beta.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
ThreadUpdateParams,
9494
Threads,
9595
} from './threads/threads';
96+
import { Chat } from './../chat';
9697

9798
export class Beta extends APIResource {
9899
realtime: RealtimeAPI.Realtime = new RealtimeAPI.Realtime(this._client);
@@ -106,8 +107,11 @@ Beta.Realtime = Realtime;
106107
Beta.ChatKit = ChatKit;
107108
Beta.Assistants = Assistants;
108109
Beta.Threads = Threads;
110+
Beta.Chat = Chat;
109111

110112
export declare namespace Beta {
113+
export { Chat };
114+
111115
export {
112116
Realtime as Realtime,
113117
type ConversationCreatedEvent as ConversationCreatedEvent,

src/resources/chat/completions/completions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import { ChatCompletionToolRunnerParams } from '../../../lib/ChatCompletionRunne
1919
import { ChatCompletionStreamingToolRunnerParams } from '../../../lib/ChatCompletionStreamingRunner';
2020
import { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream';
2121
import { ExtractParsedContentFromParams, parseChatCompletion, validateInputTools } from '../../../lib/parser';
22-
import { BetaToolRunner, BetaToolRunnerRequestOptions, type BetaToolRunnerParams } from '../../../lib/beta/BetaToolRunner';
22+
import {
23+
BetaToolRunner,
24+
type BetaToolRunnerRequestOptions,
25+
type BetaToolRunnerParams,
26+
} from '../../../lib/beta/BetaToolRunner';
2327
import type OpenAI from '../../../index';
2428

2529
export class Completions extends APIResource {
@@ -1998,6 +2002,7 @@ export interface ChatCompletionListParams extends CursorPageParams {
19982002
}
19992003

20002004
Completions.Messages = Messages;
2005+
Completions.BetaToolRunner = BetaToolRunner;
20012006

20022007
export declare namespace Completions {
20032008
export {
@@ -2047,4 +2052,6 @@ export declare namespace Completions {
20472052
};
20482053

20492054
export { Messages as Messages, type MessageListParams as MessageListParams };
2055+
2056+
export { BetaToolRunner };
20502057
}

0 commit comments

Comments
 (0)