( provider: ChatProvider, systemPrompt: string, tools: Tool[], history: Message[], callbacks?: GenerateCallbacks, options?: GenerateOptions, )
| 80 | * no tool calls, or only thinking content without any text or tool calls. |
| 81 | */ |
| 82 | export async function generate( |
| 83 | provider: ChatProvider, |
| 84 | systemPrompt: string, |
| 85 | tools: Tool[], |
| 86 | history: Message[], |
| 87 | callbacks?: GenerateCallbacks, |
| 88 | options?: GenerateOptions, |
| 89 | ): Promise<GenerateResult> { |
| 90 | const message: Message = { role: 'assistant', content: [], toolCalls: [] }; |
| 91 | let pendingPart: StreamedMessagePart | null = null; |
| 92 | |
| 93 | // Map from provider streaming index (e.g. OpenAI Chat `index`, Responses |
| 94 | // `item_id`) to the position inside `message.toolCalls`. Used to route |
| 95 | // interleaved argument deltas from parallel tool calls to the correct call. |
| 96 | const toolCallIndexMap = new Map<number | string, number>(); |
| 97 | |
| 98 | // Pre-flight abort check: if the caller's signal is already aborted, we |
| 99 | // must not issue the provider request at all. Providers that do not |
| 100 | // themselves honor `signal` would otherwise emit a network call that the |
| 101 | // caller has explicitly cancelled. |
| 102 | if (options?.signal?.aborted) { |
| 103 | throwAbortError(); |
| 104 | } |
| 105 | |
| 106 | options?.onRequestStart?.(); |
| 107 | const stream = await provider.generate(systemPrompt, tools, history, options); |
| 108 | |
| 109 | // Post-await abort check: `provider.generate()` may have resolved before |
| 110 | // noticing a mid-flight abort. Reject immediately rather than draining |
| 111 | // the stream. |
| 112 | await throwIfAborted(options?.signal, stream); |
| 113 | |
| 114 | // Decode-phase accounting. We split the window from the first streamed part |
| 115 | // to stream end into time spent awaiting the next part (server + network) vs. |
| 116 | // time spent processing each part in-process (deep copy, host callback, part |
| 117 | // merge). `lastResumeAt` marks the end of the previous part's processing, so |
| 118 | // the gap until the next part arrives is attributed to the server. The |
| 119 | // per-part processing is wrapped in try/finally so the accounting stays |
| 120 | // correct across `continue` and thrown aborts. |
| 121 | let serverDecodeMs = 0; |
| 122 | let clientConsumeMs = 0; |
| 123 | let firstPartAt: number | undefined; |
| 124 | let lastResumeAt = 0; |
| 125 | |
| 126 | for await (const part of stream) { |
| 127 | const arrivedAt = Date.now(); |
| 128 | if (firstPartAt === undefined) { |
| 129 | firstPartAt = arrivedAt; |
| 130 | } else { |
| 131 | serverDecodeMs += arrivedAt - lastResumeAt; |
| 132 | } |
| 133 | |
| 134 | try { |
| 135 | await throwIfAborted(options?.signal, stream); |
| 136 | |
| 137 | // Notify raw part callback (deep copy to avoid aliasing mutations). |
| 138 | if (callbacks?.onMessagePart !== undefined) { |
| 139 | await callbacks.onMessagePart(deepCopyPart(part)); |
no test coverage detected