Skip to content

Support tool calling while streaming OpenAI responses - #7

Merged
ActuallyTaylor merged 2 commits into
mainfrom
feature/openai-streaming-tool-calling
Aug 3, 2026
Merged

Support tool calling while streaming OpenAI responses#7
ActuallyTaylor merged 2 commits into
mainfrom
feature/openai-streaming-tool-calling

Conversation

@ActuallyTaylor

Copy link
Copy Markdown
Collaborator

Neither streaming variant handled tool calls. The Responses path had literal placeholders — case .toolCallCreated and case .toolCallDelta with comments saying tool call events are ignored — and Chat Completions never decoded tool_calls deltas at all.

Both variants now accumulate the fragments OpenAI streams: an opening event carries the call id and function name, later events append chunks of the arguments JSON. Fragments are keyed by index, and by item_id for the Responses variant since its argument deltas are keyed by item rather than position. A call is surfaced only once its fragments parse as JSON. Assembled calls run through the shared resolveToolCalls, then the assistant turn and its tool results are appended and another request starts, until a turn ends without calls.

response.completed now breaks the event loop rather than finishing the continuation, which would otherwise end the stream on the first tool-calling turn instead of starting the next request.

Failures are no longer silent. The Responses endpoint answers with HTTP 200 and reports errors as error or response.failed events. Both previously fell through to .ignored, so a failed request produced an empty response indistinguishable from a model that said nothing. They now throw OpenAILanguageModelError.streamFailed carrying the API's own message — found when an exhausted-quota account reported empty output instead of "You have no credits remaining."

Verified live: tool tests pass for both variants.

Based on #4; merge that first.

🤖 Generated with Claude Code

@ActuallyTaylor
ActuallyTaylor force-pushed the feature/openai-streaming-tool-calling branch from 71c3da7 to 012a2c2 Compare August 3, 2026 19:36
@ActuallyTaylor
ActuallyTaylor force-pushed the feature/openai-streaming-tool-calling branch 2 times, most recently from a5003e9 to 7f169ce Compare August 3, 2026 19:47
@ActuallyTaylor
ActuallyTaylor changed the base branch from feature/central-tool-resolution to feature/ollama-streaming-tool-calling August 3, 2026 19:48
@ActuallyTaylor
ActuallyTaylor force-pushed the feature/openai-streaming-tool-calling branch 2 times, most recently from fe38569 to c5ed57b Compare August 3, 2026 20:07
Base automatically changed from feature/ollama-streaming-tool-calling to main August 3, 2026 20:09
Neither streaming variant handled tool calls. The Responses path had literal placeholders — `case .toolCallCreated` and `case .toolCallDelta` with comments saying tool call events are ignored — and the Chat Completions path never decoded `tool_calls` deltas at all, so tools silently never fired when streaming.

Both variants now accumulate the fragments OpenAI streams: an opening event carries the call id and function name, later events append chunks of the arguments JSON. Fragments are keyed by index, and by `item_id` for the Responses variant since its argument deltas are keyed by item rather than position. A call is only surfaced once its fragments parse as JSON. Assembled calls run through the shared `resolveToolCalls`, the assistant turn and its tool results are appended, and another request starts until a turn ends without tool calls.

`response.completed` now breaks the event loop rather than finishing the continuation, which would otherwise end the stream on the first tool-calling turn instead of starting the next request.

Failures are no longer silent: the Responses endpoint answers with HTTP 200 and reports errors as `error` or `response.failed` events, both of which previously fell through to `.ignored`, so a failed request produced an empty response with no error. They now throw `OpenAILanguageModelError.streamFailed` carrying the API's message.

Verified against the live API: tool tests pass for both variants.
@ActuallyTaylor
ActuallyTaylor force-pushed the feature/openai-streaming-tool-calling branch from c5ed57b to 0e7e7de Compare August 3, 2026 20:09
`.init` leaves the reader to infer the type from context, which is hardest to do exactly where this file used it most: inside nested literals and behind a `Content.Type` metatype.

Every site now names its type — `OpenAIMessage(...)` in the transcript conversion, `Transcript.TextSegment(...)` in the refusal path, `LanguageModelSession.ResponseStream<Content>.Snapshot(...)` for streamed snapshots, and `Content(...)` in place of `type.init(...)`. The doc comment example names `CustomGenerationOptions` too, so it can be copied without the surrounding assignment.

No behavior change; the live OpenAI tool tests pass for both API variants.

@ActuallyTaylor ActuallyTaylor left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Looks good

@ActuallyTaylor
ActuallyTaylor merged commit 1bb4c61 into main Aug 3, 2026
7 checks passed
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.

2 participants