Skip to content

Conversation

@404Wolf
Copy link

@404Wolf 404Wolf commented Dec 1, 2025

  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Update the tool call pattern to use a new async iterable style pattern that makes more easily accessible tool calls as they are fired, and API responses.

Additional context & links

Copy link

@zanllan23-dev zanllan23-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

Comment on lines 2 to 3
import type { infer as zodInfer, ZodType } from 'zod';
import * as z from 'zod';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chatkit: ChatKitAPI.ChatKit = new ChatKitAPI.ChatKit(this._client);
assistants: AssistantsAPI.Assistants = new AssistantsAPI.Assistants(this._client);
threads: ThreadsAPI.Threads = new ThreadsAPI.Threads(this._client);
chat: ChatAPI.Chat = new ChatAPI.Chat(this._client);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should define a sibling chat.ts file in this directory and define a new class there instead of adding toolRunner() to the existing resource class, as now it'll be accessible from client.chat.completions.toolRunner() which isn't what we want yet

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extended and created a BetaChat, I'm not entirely sure if this is what you had in mind, but it limits it to .beta.chat and not just .chat like we want

@404Wolf 404Wolf requested a review from RobertCraigie December 3, 2025 17:09
} from '../../lib/beta/BetaToolRunner';
import { Chat as ChatResource } from '../../resources';

export class BetaCompletions extends ChatResource.Completions {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should just extend the base APIResource as there's no benefit to exposing the other methods on Completions here as well

Suggested change
export class BetaCompletions extends ChatResource.Completions {
export class BetaCompletions extends APIResource {

Copy link
Collaborator

@RobertCraigie RobertCraigie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good.

Once we've ironed out some of the naming questions, could you also add docs for this?

*/
export function betaZodTool<InputSchema extends ZodType>(options: {
name: string;
inputSchema: InputSchema;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should be parameters to match the API shape

Comment on lines 20 to 25
if (jsonSchema.type !== 'object') {
throw new Error(`Zod schema for tool "${options.name}" must be an object, but got ${jsonSchema.type}`);
}

// TypeScript doesn't narrow the type after the runtime check, so we need to assert it
const objectSchema = jsonSchema as typeof jsonSchema & { type: 'object' };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think we need this check? I think we should be able to just do parameters: objectSchema below?

},
],
tools: [
betaZodTool({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking through these again, I wonder if we should call these helpers betaZodFunction()? because that feels more accurate with respect to the API.

thoughts?

Copy link
Author

@404Wolf 404Wolf Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a big mix --

They use "tool_calls" in the actual messages for the list of tool calls, "function" as the type of the tool (or at least the ones we are using), and title their docs page "function calling."

Because we're talking about a type of tool call called a function call, for full transparency I'm actually starting to think we should call it "betaZodFunctionTool" (on their page they also refer user supplied functions as "function tool")

/**
* A ToolRunner handles the automatic conversation loop between the assistant and tools.
*
* A ToolRunner is an async iterable that yields either BetaMessage or BetaMessageStream objects
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some incorrect references here 😅

Comment on lines 143 to 145
tools: params.tools,
messages: params.messages,
model: params.model,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why are these explicitly listed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because typescript can't infer that stream: false even though we're in the else. tools/messages/model are not necessary

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In completions.ts I saw we always explicitly provide stream (we don't leave it as undefined) so I think it's okay to override it here to get nicer inference

Comment on lines 163 to 164
// TODO: what if it is empty?
if (prevMessage) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems redundant given we check prevMessage above?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove both checks -- both are no longer relevant under the assumption that the API always will return at least one choice

);

this.#message = this.#chatCompletion.then((resp) => resp.choices.at(0)!.message);
this.#message.catch(() => {});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: should we be logging the error? or is this safe to just ignore if the error is already reported somewhere else?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the caller does an "await" (i.e. for await const of), if it was rejected they will get to see the error since they'll be awaiting a rejected promise (of which we ignored the error previously -- here)

If i'm understanding right, the footgun is when they do iterator.next() without an await or looking at the result then there'll be an uncaught exception that is caught and ignored. In the way people use this I think that that is okay

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in the "handles api errors" test we show where you can allow an api error to happen but keep on iterating anyway, which is an artifact of how the exception doesn't throw in the actual iterator

@404Wolf 404Wolf force-pushed the add-new-tool-call-streaming branch from 50dbc89 to d5d3818 Compare December 3, 2025 21:48
@404Wolf 404Wolf force-pushed the add-new-tool-call-streaming branch from d5d3818 to c1dd29b Compare December 3, 2025 21:55
@404Wolf 404Wolf force-pushed the add-new-tool-call-streaming branch from b870a1f to d18bbfb Compare December 3, 2025 22:55
Copy link
Collaborator

@RobertCraigie RobertCraigie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking through naming more again (sorry), we'll likely want to add a similar helper for the Responses API as well in the future, so we should namespace these helpers in some fashion to make it clear they're for Chat Completions instead of Responses, e.g. betaChatFunction() etc.

Could you review the current naming and suggest some alternatives taking this into account?


## Tool Helpers

The SDK makes it easy to create and run [function tools with the chats API](https://platform.openai.com/docs/guides/function-calling). You can use Zod schemas or direct JSON schemas to describe the shape of tool input, and then you can run the tools using the `client.beta.messages.toolRunner` method. This method will automatically handle passing the inputs generated by the model into your tools and providing the results back to the model.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI looks like the primary examples in those linked docs are for the Responses API, not Chat Completions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's where they explain what a function tool call actually is, but yes it focuses on the responses API. Should we re-explain it here instead and not link to it, since it could cause confusion?

@404Wolf
Copy link
Author

404Wolf commented Dec 4, 2025

Thinking through naming more again (sorry), we'll likely want to add a similar helper for the Responses API as well in the future, so we should namespace these helpers in some fashion to make it clear they're for Chat Completions instead of Responses, e.g. betaChatFunction() etc.

Could you review the current naming and suggest some alternatives taking this into account?

Sure, some thoughts (in order of preference):

  • I looked at both types, and they're very similar. In fact, they're the same, except that function tools for responses api have the literal {type: 'function'}. What if they just return one and then let toolrunner convert it automatically? So you use the same utility for both.
  • If the helpers are API specific maybe we should put them under the resource. So, for instance, Chat.zodFunctionTool()
  • Similar: we could scope the helpers. e.g. import { zodHelperTool } from "helpers/beta/chat/tools" and import { zodHelperTool } from "helpers/beta/responses/tools".
  • Alternatively we could, yes, just do something like betaChatZodFunctionTool, but that sort of sucks. We could shorten functionTool to betaChatZodFunction.

@404Wolf 404Wolf force-pushed the add-new-tool-call-streaming branch from 2db4f0a to 949d8a0 Compare December 4, 2025 18:48
@404Wolf 404Wolf changed the title [WIP] Add new tool call streaming system Add new tool call streaming system Dec 5, 2025
@404Wolf 404Wolf marked this pull request as ready for review December 5, 2025 14:52
@404Wolf 404Wolf requested a review from a team as a code owner December 5, 2025 14:52
@404Wolf 404Wolf force-pushed the add-new-tool-call-streaming branch from 2f06c89 to ec31a58 Compare December 5, 2025 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants