初始化仓库

This commit is contained in:
mmc
2025-09-15 10:04:47 +08:00
commit 87a4a76f1b
2576 changed files with 221250 additions and 0 deletions

4
node_modules/openai/src/lib/.keep generated vendored Normal file
View File

@@ -0,0 +1,4 @@
File generated from our OpenAPI spec by Stainless.
This directory can be used to store custom files to expand the SDK.
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.

View File

@@ -0,0 +1,404 @@
import { OpenAIError } from '../error';
import type OpenAI from '../index';
import type { RequestOptions } from '../internal/request-options';
import { isAutoParsableTool, parseChatCompletion } from '../lib/parser';
import type {
ChatCompletion,
ChatCompletionCreateParams,
ChatCompletionMessage,
ChatCompletionMessageFunctionToolCall,
ChatCompletionMessageParam,
ChatCompletionTool,
ParsedChatCompletion,
} from '../resources/chat/completions';
import type { CompletionUsage } from '../resources/completions';
import type { ChatCompletionToolRunnerParams } from './ChatCompletionRunner';
import type { ChatCompletionStreamingToolRunnerParams } from './ChatCompletionStreamingRunner';
import { isAssistantMessage, isToolMessage } from './chatCompletionUtils';
import { BaseEvents, EventStream } from './EventStream';
import {
isRunnableFunctionWithParse,
type BaseFunctionsArgs,
type RunnableFunction,
type RunnableToolFunction,
} from './RunnableFunction';
const DEFAULT_MAX_CHAT_COMPLETIONS = 10;
export interface RunnerOptions extends RequestOptions {
/** How many requests to make before canceling. Default 10. */
maxChatCompletions?: number;
}
export class AbstractChatCompletionRunner<
EventTypes extends AbstractChatCompletionRunnerEvents,
ParsedT,
> extends EventStream<EventTypes> {
protected _chatCompletions: ParsedChatCompletion<ParsedT>[] = [];
messages: ChatCompletionMessageParam[] = [];
protected _addChatCompletion(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
chatCompletion: ParsedChatCompletion<ParsedT>,
): ParsedChatCompletion<ParsedT> {
this._chatCompletions.push(chatCompletion);
this._emit('chatCompletion', chatCompletion);
const message = chatCompletion.choices[0]?.message;
if (message) this._addMessage(message as ChatCompletionMessageParam);
return chatCompletion;
}
protected _addMessage(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
message: ChatCompletionMessageParam,
emit = true,
) {
if (!('content' in message)) message.content = null;
this.messages.push(message);
if (emit) {
this._emit('message', message);
if (isToolMessage(message) && message.content) {
// Note, this assumes that {role: 'tool', content: …} is always the result of a call of tool of type=function.
this._emit('functionToolCallResult', message.content as string);
} else if (isAssistantMessage(message) && message.tool_calls) {
for (const tool_call of message.tool_calls) {
if (tool_call.type === 'function') {
this._emit('functionToolCall', tool_call.function);
}
}
}
}
}
/**
* @returns a promise that resolves with the final ChatCompletion, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletion.
*/
async finalChatCompletion(): Promise<ParsedChatCompletion<ParsedT>> {
await this.done();
const completion = this._chatCompletions[this._chatCompletions.length - 1];
if (!completion) throw new OpenAIError('stream ended without producing a ChatCompletion');
return completion;
}
#getFinalContent(): string | null {
return this.#getFinalMessage().content ?? null;
}
/**
* @returns a promise that resolves with the content of the final ChatCompletionMessage, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalContent(): Promise<string | null> {
await this.done();
return this.#getFinalContent();
}
#getFinalMessage(): ChatCompletionMessage {
let i = this.messages.length;
while (i-- > 0) {
const message = this.messages[i];
if (isAssistantMessage(message)) {
// TODO: support audio here
const ret: Omit<ChatCompletionMessage, 'audio'> = {
...message,
content: (message as ChatCompletionMessage).content ?? null,
refusal: (message as ChatCompletionMessage).refusal ?? null,
};
return ret;
}
}
throw new OpenAIError('stream ended without producing a ChatCompletionMessage with role=assistant');
}
/**
* @returns a promise that resolves with the the final assistant ChatCompletionMessage response,
* or rejects if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalMessage(): Promise<ChatCompletionMessage> {
await this.done();
return this.#getFinalMessage();
}
#getFinalFunctionToolCall(): ChatCompletionMessageFunctionToolCall.Function | undefined {
for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[i];
if (isAssistantMessage(message) && message?.tool_calls?.length) {
return message.tool_calls.filter((x) => x.type === 'function').at(-1)?.function;
}
}
return;
}
/**
* @returns a promise that resolves with the content of the final FunctionCall, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalFunctionToolCall(): Promise<ChatCompletionMessageFunctionToolCall.Function | undefined> {
await this.done();
return this.#getFinalFunctionToolCall();
}
#getFinalFunctionToolCallResult(): string | undefined {
for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[i];
if (
isToolMessage(message) &&
message.content != null &&
typeof message.content === 'string' &&
this.messages.some(
(x) =>
x.role === 'assistant' &&
x.tool_calls?.some((y) => y.type === 'function' && y.id === message.tool_call_id),
)
) {
return message.content;
}
}
return;
}
async finalFunctionToolCallResult(): Promise<string | undefined> {
await this.done();
return this.#getFinalFunctionToolCallResult();
}
#calculateTotalUsage(): CompletionUsage {
const total: CompletionUsage = {
completion_tokens: 0,
prompt_tokens: 0,
total_tokens: 0,
};
for (const { usage } of this._chatCompletions) {
if (usage) {
total.completion_tokens += usage.completion_tokens;
total.prompt_tokens += usage.prompt_tokens;
total.total_tokens += usage.total_tokens;
}
}
return total;
}
async totalUsage(): Promise<CompletionUsage> {
await this.done();
return this.#calculateTotalUsage();
}
allChatCompletions(): ChatCompletion[] {
return [...this._chatCompletions];
}
protected override _emitFinal(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
) {
const completion = this._chatCompletions[this._chatCompletions.length - 1];
if (completion) this._emit('finalChatCompletion', completion);
const finalMessage = this.#getFinalMessage();
if (finalMessage) this._emit('finalMessage', finalMessage);
const finalContent = this.#getFinalContent();
if (finalContent) this._emit('finalContent', finalContent);
const finalFunctionCall = this.#getFinalFunctionToolCall();
if (finalFunctionCall) this._emit('finalFunctionToolCall', finalFunctionCall);
const finalFunctionCallResult = this.#getFinalFunctionToolCallResult();
if (finalFunctionCallResult != null) this._emit('finalFunctionToolCallResult', finalFunctionCallResult);
if (this._chatCompletions.some((c) => c.usage)) {
this._emit('totalUsage', this.#calculateTotalUsage());
}
}
#validateParams(params: ChatCompletionCreateParams): void {
if (params.n != null && params.n > 1) {
throw new OpenAIError(
'ChatCompletion convenience helpers only support n=1 at this time. To use n>1, please use chat.completions.create() directly.',
);
}
}
protected async _createChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: RequestOptions,
): Promise<ParsedChatCompletion<ParsedT>> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#validateParams(params);
const chatCompletion = await client.chat.completions.create(
{ ...params, stream: false },
{ ...options, signal: this.controller.signal },
);
this._connected();
return this._addChatCompletion(parseChatCompletion(chatCompletion, params));
}
protected async _runChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: RequestOptions,
): Promise<ChatCompletion> {
for (const message of params.messages) {
this._addMessage(message, false);
}
return await this._createChatCompletion(client, params, options);
}
protected async _runTools<FunctionsArgs extends BaseFunctionsArgs>(
client: OpenAI,
params:
| ChatCompletionToolRunnerParams<FunctionsArgs>
| ChatCompletionStreamingToolRunnerParams<FunctionsArgs>,
options?: RunnerOptions,
) {
const role = 'tool' as const;
const { tool_choice = 'auto', stream, ...restParams } = params;
const singleFunctionToCall =
typeof tool_choice !== 'string' && tool_choice.type === 'function' && tool_choice?.function?.name;
const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
// TODO(someday): clean this logic up
const inputTools = params.tools.map((tool): RunnableToolFunction<any> => {
if (isAutoParsableTool(tool)) {
if (!tool.$callback) {
throw new OpenAIError('Tool given to `.runTools()` that does not have an associated function');
}
return {
type: 'function',
function: {
function: tool.$callback,
name: tool.function.name,
description: tool.function.description || '',
parameters: tool.function.parameters as any,
parse: tool.$parseRaw,
strict: true,
},
};
}
return tool as any as RunnableToolFunction<any>;
});
const functionsByName: Record<string, RunnableFunction<any>> = {};
for (const f of inputTools) {
if (f.type === 'function') {
functionsByName[f.function.name || f.function.function.name] = f.function;
}
}
const tools: ChatCompletionTool[] =
'tools' in params ?
inputTools.map((t) =>
t.type === 'function' ?
{
type: 'function',
function: {
name: t.function.name || t.function.function.name,
parameters: t.function.parameters as Record<string, unknown>,
description: t.function.description,
strict: t.function.strict,
},
}
: (t as unknown as ChatCompletionTool),
)
: (undefined as any);
for (const message of params.messages) {
this._addMessage(message, false);
}
for (let i = 0; i < maxChatCompletions; ++i) {
const chatCompletion: ChatCompletion = await this._createChatCompletion(
client,
{
...restParams,
tool_choice,
tools,
messages: [...this.messages],
},
options,
);
const message = chatCompletion.choices[0]?.message;
if (!message) {
throw new OpenAIError(`missing message in ChatCompletion response`);
}
if (!message.tool_calls?.length) {
return;
}
for (const tool_call of message.tool_calls) {
if (tool_call.type !== 'function') continue;
const tool_call_id = tool_call.id;
const { name, arguments: args } = tool_call.function;
const fn = functionsByName[name];
if (!fn) {
const content = `Invalid tool_call: ${JSON.stringify(name)}. Available options are: ${Object.keys(
functionsByName,
)
.map((name) => JSON.stringify(name))
.join(', ')}. Please try again`;
this._addMessage({ role, tool_call_id, content });
continue;
} else if (singleFunctionToCall && singleFunctionToCall !== name) {
const content = `Invalid tool_call: ${JSON.stringify(name)}. ${JSON.stringify(
singleFunctionToCall,
)} requested. Please try again`;
this._addMessage({ role, tool_call_id, content });
continue;
}
let parsed;
try {
parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args;
} catch (error) {
const content = error instanceof Error ? error.message : String(error);
this._addMessage({ role, tool_call_id, content });
continue;
}
// @ts-expect-error it can't rule out `never` type.
const rawContent = await fn.function(parsed, this);
const content = this.#stringifyFunctionCallResult(rawContent);
this._addMessage({ role, tool_call_id, content });
if (singleFunctionToCall) {
return;
}
}
}
return;
}
#stringifyFunctionCallResult(rawContent: unknown): string {
return (
typeof rawContent === 'string' ? rawContent
: rawContent === undefined ? 'undefined'
: JSON.stringify(rawContent)
);
}
}
export interface AbstractChatCompletionRunnerEvents extends BaseEvents {
functionToolCall: (functionCall: ChatCompletionMessageFunctionToolCall.Function) => void;
message: (message: ChatCompletionMessageParam) => void;
chatCompletion: (completion: ChatCompletion) => void;
finalContent: (contentSnapshot: string) => void;
finalMessage: (message: ChatCompletionMessageParam) => void;
finalChatCompletion: (completion: ChatCompletion) => void;
finalFunctionToolCall: (functionCall: ChatCompletionMessageFunctionToolCall.Function) => void;
functionToolCallResult: (content: string) => void;
finalFunctionToolCallResult: (content: string) => void;
totalUsage: (usage: CompletionUsage) => void;
}

778
node_modules/openai/src/lib/AssistantStream.ts generated vendored Normal file
View File

@@ -0,0 +1,778 @@
import {
TextContentBlock,
ImageFileContentBlock,
Message,
MessageContentDelta,
Text,
ImageFile,
TextDelta,
MessageDelta,
MessageContent,
} from '../resources/beta/threads/messages';
import { RequestOptions } from '../internal/request-options';
import {
Run,
RunCreateParamsBase,
RunCreateParamsStreaming,
Runs,
RunSubmitToolOutputsParamsBase,
RunSubmitToolOutputsParamsStreaming,
} from '../resources/beta/threads/runs/runs';
import { type ReadableStream } from '../internal/shim-types';
import { Stream } from '../streaming';
import { APIUserAbortError, OpenAIError } from '../error';
import {
AssistantStreamEvent,
MessageStreamEvent,
RunStepStreamEvent,
RunStreamEvent,
} from '../resources/beta/assistants';
import { RunStep, RunStepDelta, ToolCall, ToolCallDelta } from '../resources/beta/threads/runs/steps';
import { ThreadCreateAndRunParamsBase, Threads } from '../resources/beta/threads/threads';
import { BaseEvents, EventStream } from './EventStream';
import { isObj } from '../internal/utils';
export interface AssistantStreamEvents extends BaseEvents {
run: (run: Run) => void;
//New event structure
messageCreated: (message: Message) => void;
messageDelta: (message: MessageDelta, snapshot: Message) => void;
messageDone: (message: Message) => void;
runStepCreated: (runStep: RunStep) => void;
runStepDelta: (delta: RunStepDelta, snapshot: Runs.RunStep) => void;
runStepDone: (runStep: Runs.RunStep, snapshot: Runs.RunStep) => void;
toolCallCreated: (toolCall: ToolCall) => void;
toolCallDelta: (delta: ToolCallDelta, snapshot: ToolCall) => void;
toolCallDone: (toolCall: ToolCall) => void;
textCreated: (content: Text) => void;
textDelta: (delta: TextDelta, snapshot: Text) => void;
textDone: (content: Text, snapshot: Message) => void;
//No created or delta as this is not streamed
imageFileDone: (content: ImageFile, snapshot: Message) => void;
event: (event: AssistantStreamEvent) => void;
}
export type ThreadCreateAndRunParamsBaseStream = Omit<ThreadCreateAndRunParamsBase, 'stream'> & {
stream?: true;
};
export type RunCreateParamsBaseStream = Omit<RunCreateParamsBase, 'stream'> & {
stream?: true;
};
export type RunSubmitToolOutputsParamsStream = Omit<RunSubmitToolOutputsParamsBase, 'stream'> & {
stream?: true;
};
export class AssistantStream
extends EventStream<AssistantStreamEvents>
implements AsyncIterable<AssistantStreamEvent>
{
//Track all events in a single list for reference
#events: AssistantStreamEvent[] = [];
//Used to accumulate deltas
//We are accumulating many types so the value here is not strict
#runStepSnapshots: { [id: string]: Runs.RunStep } = {};
#messageSnapshots: { [id: string]: Message } = {};
#messageSnapshot: Message | undefined;
#finalRun: Run | undefined;
#currentContentIndex: number | undefined;
#currentContent: MessageContent | undefined;
#currentToolCallIndex: number | undefined;
#currentToolCall: ToolCall | undefined;
//For current snapshot methods
#currentEvent: AssistantStreamEvent | undefined;
#currentRunSnapshot: Run | undefined;
#currentRunStepSnapshot: Runs.RunStep | undefined;
[Symbol.asyncIterator](): AsyncIterator<AssistantStreamEvent> {
const pushQueue: AssistantStreamEvent[] = [];
const readQueue: {
resolve: (chunk: AssistantStreamEvent | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
//Catch all for passing along all events
this.on('event', (event) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(event);
} else {
pushQueue.push(event);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<AssistantStreamEvent>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<AssistantStreamEvent | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((chunk) => (chunk ? { value: chunk, done: false } : { value: undefined, done: true }));
}
const chunk = pushQueue.shift()!;
return { value: chunk, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
static fromReadableStream(stream: ReadableStream): AssistantStream {
const runner = new AssistantStream();
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
protected async _fromReadableStream(
readableStream: ReadableStream,
options?: RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this._connected();
const stream = Stream.fromReadableStream<AssistantStreamEvent>(readableStream, this.controller);
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
toReadableStream(): ReadableStream {
const stream = new Stream(this[Symbol.asyncIterator].bind(this), this.controller);
return stream.toReadableStream();
}
static createToolAssistantStream(
runId: string,
runs: Runs,
params: RunSubmitToolOutputsParamsStream,
options: RequestOptions | undefined,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._runToolAssistantStream(runId, runs, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
protected async _createToolAssistantStream(
run: Runs,
runId: string,
params: RunSubmitToolOutputsParamsStream,
options?: RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunSubmitToolOutputsParamsStreaming = { ...params, stream: true };
const stream = await run.submitToolOutputs(runId, body, {
...options,
signal: this.controller.signal,
});
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
static createThreadAssistantStream(
params: ThreadCreateAndRunParamsBaseStream,
thread: Threads,
options?: RequestOptions,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._threadAssistantStream(params, thread, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
static createAssistantStream(
threadId: string,
runs: Runs,
params: RunCreateParamsBaseStream,
options?: RequestOptions,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._runAssistantStream(threadId, runs, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
currentEvent(): AssistantStreamEvent | undefined {
return this.#currentEvent;
}
currentRun(): Run | undefined {
return this.#currentRunSnapshot;
}
currentMessageSnapshot(): Message | undefined {
return this.#messageSnapshot;
}
currentRunStepSnapshot(): Runs.RunStep | undefined {
return this.#currentRunStepSnapshot;
}
async finalRunSteps(): Promise<Runs.RunStep[]> {
await this.done();
return Object.values(this.#runStepSnapshots);
}
async finalMessages(): Promise<Message[]> {
await this.done();
return Object.values(this.#messageSnapshots);
}
async finalRun(): Promise<Run> {
await this.done();
if (!this.#finalRun) throw Error('Final run was not received.');
return this.#finalRun;
}
protected async _createThreadAssistantStream(
thread: Threads,
params: ThreadCreateAndRunParamsBase,
options?: RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunCreateParamsStreaming = { ...params, stream: true };
const stream = await thread.createAndRun(body, { ...options, signal: this.controller.signal });
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
protected async _createAssistantStream(
run: Runs,
threadId: string,
params: RunCreateParamsBase,
options?: RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunCreateParamsStreaming = { ...params, stream: true };
const stream = await run.create(threadId, body, { ...options, signal: this.controller.signal });
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
#addEvent(event: AssistantStreamEvent) {
if (this.ended) return;
this.#currentEvent = event;
this.#handleEvent(event);
switch (event.event) {
case 'thread.created':
//No action on this event.
break;
case 'thread.run.created':
case 'thread.run.queued':
case 'thread.run.in_progress':
case 'thread.run.requires_action':
case 'thread.run.completed':
case 'thread.run.incomplete':
case 'thread.run.failed':
case 'thread.run.cancelling':
case 'thread.run.cancelled':
case 'thread.run.expired':
this.#handleRun(event);
break;
case 'thread.run.step.created':
case 'thread.run.step.in_progress':
case 'thread.run.step.delta':
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
this.#handleRunStep(event);
break;
case 'thread.message.created':
case 'thread.message.in_progress':
case 'thread.message.delta':
case 'thread.message.completed':
case 'thread.message.incomplete':
this.#handleMessage(event);
break;
case 'error':
//This is included for completeness, but errors are processed in the SSE event processing so this should not occur
throw new Error(
'Encountered an error event in event processing - errors should be processed earlier',
);
default:
assertNever(event);
}
}
#endRequest(): Run {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
if (!this.#finalRun) throw Error('Final run has not been received');
return this.#finalRun;
}
#handleMessage(this: AssistantStream, event: MessageStreamEvent) {
const [accumulatedMessage, newContent] = this.#accumulateMessage(event, this.#messageSnapshot);
this.#messageSnapshot = accumulatedMessage;
this.#messageSnapshots[accumulatedMessage.id] = accumulatedMessage;
for (const content of newContent) {
const snapshotContent = accumulatedMessage.content[content.index];
if (snapshotContent?.type == 'text') {
this._emit('textCreated', snapshotContent.text);
}
}
switch (event.event) {
case 'thread.message.created':
this._emit('messageCreated', event.data);
break;
case 'thread.message.in_progress':
break;
case 'thread.message.delta':
this._emit('messageDelta', event.data.delta, accumulatedMessage);
if (event.data.delta.content) {
for (const content of event.data.delta.content) {
//If it is text delta, emit a text delta event
if (content.type == 'text' && content.text) {
let textDelta = content.text;
let snapshot = accumulatedMessage.content[content.index];
if (snapshot && snapshot.type == 'text') {
this._emit('textDelta', textDelta, snapshot.text);
} else {
throw Error('The snapshot associated with this text delta is not text or missing');
}
}
if (content.index != this.#currentContentIndex) {
//See if we have in progress content
if (this.#currentContent) {
switch (this.#currentContent.type) {
case 'text':
this._emit('textDone', this.#currentContent.text, this.#messageSnapshot);
break;
case 'image_file':
this._emit('imageFileDone', this.#currentContent.image_file, this.#messageSnapshot);
break;
}
}
this.#currentContentIndex = content.index;
}
this.#currentContent = accumulatedMessage.content[content.index];
}
}
break;
case 'thread.message.completed':
case 'thread.message.incomplete':
//We emit the latest content we were working on on completion (including incomplete)
if (this.#currentContentIndex !== undefined) {
const currentContent = event.data.content[this.#currentContentIndex];
if (currentContent) {
switch (currentContent.type) {
case 'image_file':
this._emit('imageFileDone', currentContent.image_file, this.#messageSnapshot);
break;
case 'text':
this._emit('textDone', currentContent.text, this.#messageSnapshot);
break;
}
}
}
if (this.#messageSnapshot) {
this._emit('messageDone', event.data);
}
this.#messageSnapshot = undefined;
}
}
#handleRunStep(this: AssistantStream, event: RunStepStreamEvent) {
const accumulatedRunStep = this.#accumulateRunStep(event);
this.#currentRunStepSnapshot = accumulatedRunStep;
switch (event.event) {
case 'thread.run.step.created':
this._emit('runStepCreated', event.data);
break;
case 'thread.run.step.delta':
const delta = event.data.delta;
if (
delta.step_details &&
delta.step_details.type == 'tool_calls' &&
delta.step_details.tool_calls &&
accumulatedRunStep.step_details.type == 'tool_calls'
) {
for (const toolCall of delta.step_details.tool_calls) {
if (toolCall.index == this.#currentToolCallIndex) {
this._emit(
'toolCallDelta',
toolCall,
accumulatedRunStep.step_details.tool_calls[toolCall.index] as ToolCall,
);
} else {
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall);
}
this.#currentToolCallIndex = toolCall.index;
this.#currentToolCall = accumulatedRunStep.step_details.tool_calls[toolCall.index];
if (this.#currentToolCall) this._emit('toolCallCreated', this.#currentToolCall);
}
}
}
this._emit('runStepDelta', event.data.delta, accumulatedRunStep);
break;
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
this.#currentRunStepSnapshot = undefined;
const details = event.data.step_details;
if (details.type == 'tool_calls') {
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall as ToolCall);
this.#currentToolCall = undefined;
}
}
this._emit('runStepDone', event.data, accumulatedRunStep);
break;
case 'thread.run.step.in_progress':
break;
}
}
#handleEvent(this: AssistantStream, event: AssistantStreamEvent) {
this.#events.push(event);
this._emit('event', event);
}
#accumulateRunStep(event: RunStepStreamEvent): Runs.RunStep {
switch (event.event) {
case 'thread.run.step.created':
this.#runStepSnapshots[event.data.id] = event.data;
return event.data;
case 'thread.run.step.delta':
let snapshot = this.#runStepSnapshots[event.data.id] as Runs.RunStep;
if (!snapshot) {
throw Error('Received a RunStepDelta before creation of a snapshot');
}
let data = event.data;
if (data.delta) {
const accumulated = AssistantStream.accumulateDelta(snapshot, data.delta) as Runs.RunStep;
this.#runStepSnapshots[event.data.id] = accumulated;
}
return this.#runStepSnapshots[event.data.id] as Runs.RunStep;
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
case 'thread.run.step.in_progress':
this.#runStepSnapshots[event.data.id] = event.data;
break;
}
if (this.#runStepSnapshots[event.data.id]) return this.#runStepSnapshots[event.data.id] as Runs.RunStep;
throw new Error('No snapshot available');
}
#accumulateMessage(
event: AssistantStreamEvent,
snapshot: Message | undefined,
): [Message, MessageContentDelta[]] {
let newContent: MessageContentDelta[] = [];
switch (event.event) {
case 'thread.message.created':
//On creation the snapshot is just the initial message
return [event.data, newContent];
case 'thread.message.delta':
if (!snapshot) {
throw Error(
'Received a delta with no existing snapshot (there should be one from message creation)',
);
}
let data = event.data;
//If this delta does not have content, nothing to process
if (data.delta.content) {
for (const contentElement of data.delta.content) {
if (contentElement.index in snapshot.content) {
let currentContent = snapshot.content[contentElement.index];
snapshot.content[contentElement.index] = this.#accumulateContent(
contentElement,
currentContent,
);
} else {
snapshot.content[contentElement.index] = contentElement as MessageContent;
// This is a new element
newContent.push(contentElement);
}
}
}
return [snapshot, newContent];
case 'thread.message.in_progress':
case 'thread.message.completed':
case 'thread.message.incomplete':
//No changes on other thread events
if (snapshot) {
return [snapshot, newContent];
} else {
throw Error('Received thread message event with no existing snapshot');
}
}
throw Error('Tried to accumulate a non-message event');
}
#accumulateContent(
contentElement: MessageContentDelta,
currentContent: MessageContent | undefined,
): TextContentBlock | ImageFileContentBlock {
return AssistantStream.accumulateDelta(currentContent as unknown as Record<any, any>, contentElement) as
| TextContentBlock
| ImageFileContentBlock;
}
static accumulateDelta(acc: Record<string, any>, delta: Record<string, any>): Record<string, any> {
for (const [key, deltaValue] of Object.entries(delta)) {
if (!acc.hasOwnProperty(key)) {
acc[key] = deltaValue;
continue;
}
let accValue = acc[key];
if (accValue === null || accValue === undefined) {
acc[key] = deltaValue;
continue;
}
// We don't accumulate these special properties
if (key === 'index' || key === 'type') {
acc[key] = deltaValue;
continue;
}
// Type-specific accumulation logic
if (typeof accValue === 'string' && typeof deltaValue === 'string') {
accValue += deltaValue;
} else if (typeof accValue === 'number' && typeof deltaValue === 'number') {
accValue += deltaValue;
} else if (isObj(accValue) && isObj(deltaValue)) {
accValue = this.accumulateDelta(accValue as Record<string, any>, deltaValue as Record<string, any>);
} else if (Array.isArray(accValue) && Array.isArray(deltaValue)) {
if (accValue.every((x) => typeof x === 'string' || typeof x === 'number')) {
accValue.push(...deltaValue); // Use spread syntax for efficient addition
continue;
}
for (const deltaEntry of deltaValue) {
if (!isObj(deltaEntry)) {
throw new Error(`Expected array delta entry to be an object but got: ${deltaEntry}`);
}
const index = deltaEntry['index'];
if (index == null) {
console.error(deltaEntry);
throw new Error('Expected array delta entry to have an `index` property');
}
if (typeof index !== 'number') {
throw new Error(`Expected array delta entry \`index\` property to be a number but got ${index}`);
}
const accEntry = accValue[index];
if (accEntry == null) {
accValue.push(deltaEntry);
} else {
accValue[index] = this.accumulateDelta(accEntry, deltaEntry);
}
}
continue;
} else {
throw Error(`Unhandled record type: ${key}, deltaValue: ${deltaValue}, accValue: ${accValue}`);
}
acc[key] = accValue;
}
return acc;
}
#handleRun(this: AssistantStream, event: RunStreamEvent) {
this.#currentRunSnapshot = event.data;
switch (event.event) {
case 'thread.run.created':
break;
case 'thread.run.queued':
break;
case 'thread.run.in_progress':
break;
case 'thread.run.requires_action':
case 'thread.run.cancelled':
case 'thread.run.failed':
case 'thread.run.completed':
case 'thread.run.expired':
case 'thread.run.incomplete':
this.#finalRun = event.data;
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall);
this.#currentToolCall = undefined;
}
break;
case 'thread.run.cancelling':
break;
}
}
protected _addRun(run: Run): Run {
return run;
}
protected async _threadAssistantStream(
params: ThreadCreateAndRunParamsBase,
thread: Threads,
options?: RequestOptions,
): Promise<Run> {
return await this._createThreadAssistantStream(thread, params, options);
}
protected async _runAssistantStream(
threadId: string,
runs: Runs,
params: RunCreateParamsBase,
options?: RequestOptions,
): Promise<Run> {
return await this._createAssistantStream(runs, threadId, params, options);
}
protected async _runToolAssistantStream(
runId: string,
runs: Runs,
params: RunSubmitToolOutputsParamsStream,
options?: RequestOptions,
): Promise<Run> {
return await this._createToolAssistantStream(runs, runId, params, options);
}
}
function assertNever(_x: never) {}

54
node_modules/openai/src/lib/ChatCompletionRunner.ts generated vendored Normal file
View File

@@ -0,0 +1,54 @@
import {
type ChatCompletionMessageParam,
type ChatCompletionCreateParamsNonStreaming,
} from '../resources/chat/completions';
import { type BaseFunctionsArgs, RunnableTools } from './RunnableFunction';
import {
AbstractChatCompletionRunner,
AbstractChatCompletionRunnerEvents,
RunnerOptions,
} from './AbstractChatCompletionRunner';
import { isAssistantMessage } from './chatCompletionUtils';
import OpenAI from '../index';
import { AutoParseableTool } from '../lib/parser';
export interface ChatCompletionRunnerEvents extends AbstractChatCompletionRunnerEvents {
content: (content: string) => void;
}
export type ChatCompletionToolRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsNonStreaming,
'tools'
> & {
tools: RunnableTools<FunctionsArgs> | AutoParseableTool<any, true>[];
};
export class ChatCompletionRunner<ParsedT = null> extends AbstractChatCompletionRunner<
ChatCompletionRunnerEvents,
ParsedT
> {
static runTools<ParsedT>(
client: OpenAI,
params: ChatCompletionToolRunnerParams<any[]>,
options?: RunnerOptions,
): ChatCompletionRunner<ParsedT> {
const runner = new ChatCompletionRunner<ParsedT>();
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runTools' },
};
runner._run(() => runner._runTools(client, params, opts));
return runner;
}
override _addMessage(
this: ChatCompletionRunner<ParsedT>,
message: ChatCompletionMessageParam,
emit: boolean = true,
) {
super._addMessage(message, emit);
if (isAssistantMessage(message) && message.content) {
this._emit('content', message.content as string);
}
}
}

872
node_modules/openai/src/lib/ChatCompletionStream.ts generated vendored Normal file
View File

@@ -0,0 +1,872 @@
import { partialParse } from '../_vendor/partial-json-parser/parser';
import {
APIUserAbortError,
ContentFilterFinishReasonError,
LengthFinishReasonError,
OpenAIError,
} from '../error';
import OpenAI from '../index';
import { RequestOptions } from '../internal/request-options';
import { type ReadableStream } from '../internal/shim-types';
import {
AutoParseableResponseFormat,
hasAutoParseableInput,
isAutoParsableResponseFormat,
isAutoParsableTool,
isChatCompletionFunctionTool,
maybeParseChatCompletion,
shouldParseToolCall,
} from '../lib/parser';
import { ChatCompletionFunctionTool, ParsedChatCompletion } from '../resources/chat/completions';
import {
ChatCompletionTokenLogprob,
type ChatCompletion,
type ChatCompletionChunk,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsBase,
type ChatCompletionCreateParamsStreaming,
type ChatCompletionRole,
} from '../resources/chat/completions/completions';
import { Stream } from '../streaming';
import {
AbstractChatCompletionRunner,
type AbstractChatCompletionRunnerEvents,
} from './AbstractChatCompletionRunner';
export interface ContentDeltaEvent {
delta: string;
snapshot: string;
parsed: unknown | null;
}
export interface ContentDoneEvent<ParsedT = null> {
content: string;
parsed: ParsedT | null;
}
export interface RefusalDeltaEvent {
delta: string;
snapshot: string;
}
export interface RefusalDoneEvent {
refusal: string;
}
export interface FunctionToolCallArgumentsDeltaEvent {
name: string;
index: number;
arguments: string;
parsed_arguments: unknown;
arguments_delta: string;
}
export interface FunctionToolCallArgumentsDoneEvent {
name: string;
index: number;
arguments: string;
parsed_arguments: unknown;
}
export interface LogProbsContentDeltaEvent {
content: Array<ChatCompletionTokenLogprob>;
snapshot: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsContentDoneEvent {
content: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsRefusalDeltaEvent {
refusal: Array<ChatCompletionTokenLogprob>;
snapshot: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsRefusalDoneEvent {
refusal: Array<ChatCompletionTokenLogprob>;
}
export interface ChatCompletionStreamEvents<ParsedT = null> extends AbstractChatCompletionRunnerEvents {
content: (contentDelta: string, contentSnapshot: string) => void;
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
'content.delta': (props: ContentDeltaEvent) => void;
'content.done': (props: ContentDoneEvent<ParsedT>) => void;
'refusal.delta': (props: RefusalDeltaEvent) => void;
'refusal.done': (props: RefusalDoneEvent) => void;
'tool_calls.function.arguments.delta': (props: FunctionToolCallArgumentsDeltaEvent) => void;
'tool_calls.function.arguments.done': (props: FunctionToolCallArgumentsDoneEvent) => void;
'logprobs.content.delta': (props: LogProbsContentDeltaEvent) => void;
'logprobs.content.done': (props: LogProbsContentDoneEvent) => void;
'logprobs.refusal.delta': (props: LogProbsRefusalDeltaEvent) => void;
'logprobs.refusal.done': (props: LogProbsRefusalDoneEvent) => void;
}
export type ChatCompletionStreamParams = Omit<ChatCompletionCreateParamsBase, 'stream'> & {
stream?: true;
};
interface ChoiceEventState {
content_done: boolean;
refusal_done: boolean;
logprobs_content_done: boolean;
logprobs_refusal_done: boolean;
current_tool_call_index: number | null;
done_tool_calls: Set<number>;
}
export class ChatCompletionStream<ParsedT = null>
extends AbstractChatCompletionRunner<ChatCompletionStreamEvents<ParsedT>, ParsedT>
implements AsyncIterable<ChatCompletionChunk>
{
#params: ChatCompletionCreateParams | null;
#choiceEventStates: ChoiceEventState[];
#currentChatCompletionSnapshot: ChatCompletionSnapshot | undefined;
constructor(params: ChatCompletionCreateParams | null) {
super();
this.#params = params;
this.#choiceEventStates = [];
}
get currentChatCompletionSnapshot(): ChatCompletionSnapshot | undefined {
return this.#currentChatCompletionSnapshot;
}
/**
* Intended for use on the frontend, consuming a stream produced with
* `.toReadableStream()` on the backend.
*
* Note that messages sent to the model do not appear in `.on('message')`
* in this context.
*/
static fromReadableStream(stream: ReadableStream): ChatCompletionStream<null> {
const runner = new ChatCompletionStream(null);
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
static createChatCompletion<ParsedT>(
client: OpenAI,
params: ChatCompletionStreamParams,
options?: RequestOptions,
): ChatCompletionStream<ParsedT> {
const runner = new ChatCompletionStream<ParsedT>(params as ChatCompletionCreateParamsStreaming);
runner._run(() =>
runner._runChatCompletion(
client,
{ ...params, stream: true },
{ ...options, headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' } },
),
);
return runner;
}
#beginRequest() {
if (this.ended) return;
this.#currentChatCompletionSnapshot = undefined;
}
#getChoiceEventState(choice: ChatCompletionSnapshot.Choice): ChoiceEventState {
let state = this.#choiceEventStates[choice.index];
if (state) {
return state;
}
state = {
content_done: false,
refusal_done: false,
logprobs_content_done: false,
logprobs_refusal_done: false,
done_tool_calls: new Set(),
current_tool_call_index: null,
};
this.#choiceEventStates[choice.index] = state;
return state;
}
#addChunk(this: ChatCompletionStream<ParsedT>, chunk: ChatCompletionChunk) {
if (this.ended) return;
const completion = this.#accumulateChatCompletion(chunk);
this._emit('chunk', chunk, completion);
for (const choice of chunk.choices) {
const choiceSnapshot = completion.choices[choice.index]!;
if (
choice.delta.content != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.content
) {
this._emit('content', choice.delta.content, choiceSnapshot.message.content);
this._emit('content.delta', {
delta: choice.delta.content,
snapshot: choiceSnapshot.message.content,
parsed: choiceSnapshot.message.parsed,
});
}
if (
choice.delta.refusal != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.refusal
) {
this._emit('refusal.delta', {
delta: choice.delta.refusal,
snapshot: choiceSnapshot.message.refusal,
});
}
if (choice.logprobs?.content != null && choiceSnapshot.message?.role === 'assistant') {
this._emit('logprobs.content.delta', {
content: choice.logprobs?.content,
snapshot: choiceSnapshot.logprobs?.content ?? [],
});
}
if (choice.logprobs?.refusal != null && choiceSnapshot.message?.role === 'assistant') {
this._emit('logprobs.refusal.delta', {
refusal: choice.logprobs?.refusal,
snapshot: choiceSnapshot.logprobs?.refusal ?? [],
});
}
const state = this.#getChoiceEventState(choiceSnapshot);
if (choiceSnapshot.finish_reason) {
this.#emitContentDoneEvents(choiceSnapshot);
if (state.current_tool_call_index != null) {
this.#emitToolCallDoneEvent(choiceSnapshot, state.current_tool_call_index);
}
}
for (const toolCall of choice.delta.tool_calls ?? []) {
if (state.current_tool_call_index !== toolCall.index) {
this.#emitContentDoneEvents(choiceSnapshot);
// new tool call started, the previous one is done
if (state.current_tool_call_index != null) {
this.#emitToolCallDoneEvent(choiceSnapshot, state.current_tool_call_index);
}
}
state.current_tool_call_index = toolCall.index;
}
for (const toolCallDelta of choice.delta.tool_calls ?? []) {
const toolCallSnapshot = choiceSnapshot.message.tool_calls?.[toolCallDelta.index];
if (!toolCallSnapshot?.type) {
continue;
}
if (toolCallSnapshot?.type === 'function') {
this._emit('tool_calls.function.arguments.delta', {
name: toolCallSnapshot.function?.name,
index: toolCallDelta.index,
arguments: toolCallSnapshot.function.arguments,
parsed_arguments: toolCallSnapshot.function.parsed_arguments,
arguments_delta: toolCallDelta.function?.arguments ?? '',
});
} else {
assertNever(toolCallSnapshot?.type);
}
}
}
}
#emitToolCallDoneEvent(choiceSnapshot: ChatCompletionSnapshot.Choice, toolCallIndex: number) {
const state = this.#getChoiceEventState(choiceSnapshot);
if (state.done_tool_calls.has(toolCallIndex)) {
// we've already fired the done event
return;
}
const toolCallSnapshot = choiceSnapshot.message.tool_calls?.[toolCallIndex];
if (!toolCallSnapshot) {
throw new Error('no tool call snapshot');
}
if (!toolCallSnapshot.type) {
throw new Error('tool call snapshot missing `type`');
}
if (toolCallSnapshot.type === 'function') {
const inputTool = this.#params?.tools?.find(
(tool) => isChatCompletionFunctionTool(tool) && tool.function.name === toolCallSnapshot.function.name,
) as ChatCompletionFunctionTool | undefined; // TS doesn't narrow based on isChatCompletionTool
this._emit('tool_calls.function.arguments.done', {
name: toolCallSnapshot.function.name,
index: toolCallIndex,
arguments: toolCallSnapshot.function.arguments,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCallSnapshot.function.arguments)
: inputTool?.function.strict ? JSON.parse(toolCallSnapshot.function.arguments)
: null,
});
} else {
assertNever(toolCallSnapshot.type);
}
}
#emitContentDoneEvents(choiceSnapshot: ChatCompletionSnapshot.Choice) {
const state = this.#getChoiceEventState(choiceSnapshot);
if (choiceSnapshot.message.content && !state.content_done) {
state.content_done = true;
const responseFormat = this.#getAutoParseableResponseFormat();
this._emit('content.done', {
content: choiceSnapshot.message.content,
parsed: responseFormat ? responseFormat.$parseRaw(choiceSnapshot.message.content) : (null as any),
});
}
if (choiceSnapshot.message.refusal && !state.refusal_done) {
state.refusal_done = true;
this._emit('refusal.done', { refusal: choiceSnapshot.message.refusal });
}
if (choiceSnapshot.logprobs?.content && !state.logprobs_content_done) {
state.logprobs_content_done = true;
this._emit('logprobs.content.done', { content: choiceSnapshot.logprobs.content });
}
if (choiceSnapshot.logprobs?.refusal && !state.logprobs_refusal_done) {
state.logprobs_refusal_done = true;
this._emit('logprobs.refusal.done', { refusal: choiceSnapshot.logprobs.refusal });
}
}
#endRequest(): ParsedChatCompletion<ParsedT> {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
const snapshot = this.#currentChatCompletionSnapshot;
if (!snapshot) {
throw new OpenAIError(`request ended without sending any chunks`);
}
this.#currentChatCompletionSnapshot = undefined;
this.#choiceEventStates = [];
return finalizeChatCompletion(snapshot, this.#params);
}
protected override async _createChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: RequestOptions,
): Promise<ParsedChatCompletion<ParsedT>> {
super._createChatCompletion;
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
const stream = await client.chat.completions.create(
{ ...params, stream: true },
{ ...options, signal: this.controller.signal },
);
this._connected();
for await (const chunk of stream) {
this.#addChunk(chunk);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addChatCompletion(this.#endRequest());
}
protected async _fromReadableStream(
readableStream: ReadableStream,
options?: RequestOptions,
): Promise<ChatCompletion> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
this._connected();
const stream = Stream.fromReadableStream<ChatCompletionChunk>(readableStream, this.controller);
let chatId;
for await (const chunk of stream) {
if (chatId && chatId !== chunk.id) {
// A new request has been made.
this._addChatCompletion(this.#endRequest());
}
this.#addChunk(chunk);
chatId = chunk.id;
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addChatCompletion(this.#endRequest());
}
#getAutoParseableResponseFormat(): AutoParseableResponseFormat<ParsedT> | null {
const responseFormat = this.#params?.response_format;
if (isAutoParsableResponseFormat<ParsedT>(responseFormat)) {
return responseFormat;
}
return null;
}
#accumulateChatCompletion(chunk: ChatCompletionChunk): ChatCompletionSnapshot {
let snapshot = this.#currentChatCompletionSnapshot;
const { choices, ...rest } = chunk;
if (!snapshot) {
snapshot = this.#currentChatCompletionSnapshot = {
...rest,
choices: [],
};
} else {
Object.assign(snapshot, rest);
}
for (const { delta, finish_reason, index, logprobs = null, ...other } of chunk.choices) {
let choice = snapshot.choices[index];
if (!choice) {
choice = snapshot.choices[index] = { finish_reason, index, message: {}, logprobs, ...other };
}
if (logprobs) {
if (!choice.logprobs) {
choice.logprobs = Object.assign({}, logprobs);
} else {
const { content, refusal, ...rest } = logprobs;
assertIsEmpty(rest);
Object.assign(choice.logprobs, rest);
if (content) {
choice.logprobs.content ??= [];
choice.logprobs.content.push(...content);
}
if (refusal) {
choice.logprobs.refusal ??= [];
choice.logprobs.refusal.push(...refusal);
}
}
}
if (finish_reason) {
choice.finish_reason = finish_reason;
if (this.#params && hasAutoParseableInput(this.#params)) {
if (finish_reason === 'length') {
throw new LengthFinishReasonError();
}
if (finish_reason === 'content_filter') {
throw new ContentFilterFinishReasonError();
}
}
}
Object.assign(choice, other);
if (!delta) continue; // Shouldn't happen; just in case.
const { content, refusal, function_call, role, tool_calls, ...rest } = delta;
assertIsEmpty(rest);
Object.assign(choice.message, rest);
if (refusal) {
choice.message.refusal = (choice.message.refusal || '') + refusal;
}
if (role) choice.message.role = role;
if (function_call) {
if (!choice.message.function_call) {
choice.message.function_call = function_call;
} else {
if (function_call.name) choice.message.function_call.name = function_call.name;
if (function_call.arguments) {
choice.message.function_call.arguments ??= '';
choice.message.function_call.arguments += function_call.arguments;
}
}
}
if (content) {
choice.message.content = (choice.message.content || '') + content;
if (!choice.message.refusal && this.#getAutoParseableResponseFormat()) {
choice.message.parsed = partialParse(choice.message.content);
}
}
if (tool_calls) {
if (!choice.message.tool_calls) choice.message.tool_calls = [];
for (const { index, id, type, function: fn, ...rest } of tool_calls) {
const tool_call = (choice.message.tool_calls[index] ??=
{} as ChatCompletionSnapshot.Choice.Message.ToolCall);
Object.assign(tool_call, rest);
if (id) tool_call.id = id;
if (type) tool_call.type = type;
if (fn) tool_call.function ??= { name: fn.name ?? '', arguments: '' };
if (fn?.name) tool_call.function!.name = fn.name;
if (fn?.arguments) {
tool_call.function!.arguments += fn.arguments;
if (shouldParseToolCall(this.#params, tool_call)) {
tool_call.function!.parsed_arguments = partialParse(tool_call.function!.arguments);
}
}
}
}
}
return snapshot;
}
[Symbol.asyncIterator](this: ChatCompletionStream<ParsedT>): AsyncIterator<ChatCompletionChunk> {
const pushQueue: ChatCompletionChunk[] = [];
const readQueue: {
resolve: (chunk: ChatCompletionChunk | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
this.on('chunk', (chunk) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(chunk);
} else {
pushQueue.push(chunk);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<ChatCompletionChunk>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<ChatCompletionChunk | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((chunk) => (chunk ? { value: chunk, done: false } : { value: undefined, done: true }));
}
const chunk = pushQueue.shift()!;
return { value: chunk, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
toReadableStream(): ReadableStream {
const stream = new Stream(this[Symbol.asyncIterator].bind(this), this.controller);
return stream.toReadableStream();
}
}
function finalizeChatCompletion<ParsedT>(
snapshot: ChatCompletionSnapshot,
params: ChatCompletionCreateParams | null,
): ParsedChatCompletion<ParsedT> {
const { id, choices, created, model, system_fingerprint, ...rest } = snapshot;
const completion: ChatCompletion = {
...rest,
id,
choices: choices.map(
({ message, finish_reason, index, logprobs, ...choiceRest }): ChatCompletion.Choice => {
if (!finish_reason) {
throw new OpenAIError(`missing finish_reason for choice ${index}`);
}
const { content = null, function_call, tool_calls, ...messageRest } = message;
const role = message.role as 'assistant'; // this is what we expect; in theory it could be different which would make our types a slight lie but would be fine.
if (!role) {
throw new OpenAIError(`missing role for choice ${index}`);
}
if (function_call) {
const { arguments: args, name } = function_call;
if (args == null) {
throw new OpenAIError(`missing function_call.arguments for choice ${index}`);
}
if (!name) {
throw new OpenAIError(`missing function_call.name for choice ${index}`);
}
return {
...choiceRest,
message: {
content,
function_call: { arguments: args, name },
role,
refusal: message.refusal ?? null,
},
finish_reason,
index,
logprobs,
};
}
if (tool_calls) {
return {
...choiceRest,
index,
finish_reason,
logprobs,
message: {
...messageRest,
role,
content,
refusal: message.refusal ?? null,
tool_calls: tool_calls.map((tool_call, i) => {
const { function: fn, type, id, ...toolRest } = tool_call;
const { arguments: args, name, ...fnRest } = fn || {};
if (id == null) {
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].id\n${str(snapshot)}`);
}
if (type == null) {
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].type\n${str(snapshot)}`);
}
if (name == null) {
throw new OpenAIError(
`missing choices[${index}].tool_calls[${i}].function.name\n${str(snapshot)}`,
);
}
if (args == null) {
throw new OpenAIError(
`missing choices[${index}].tool_calls[${i}].function.arguments\n${str(snapshot)}`,
);
}
return { ...toolRest, id, type, function: { ...fnRest, name, arguments: args } };
}),
},
};
}
return {
...choiceRest,
message: { ...messageRest, content, role, refusal: message.refusal ?? null },
finish_reason,
index,
logprobs,
};
},
),
created,
model,
object: 'chat.completion',
...(system_fingerprint ? { system_fingerprint } : {}),
};
return maybeParseChatCompletion(completion, params);
}
function str(x: unknown) {
return JSON.stringify(x);
}
/**
* Represents a streamed chunk of a chat completion response returned by model,
* based on the provided input.
*/
export interface ChatCompletionSnapshot {
/**
* A unique identifier for the chat completion.
*/
id: string;
/**
* A list of chat completion choices. Can be more than one if `n` is greater
* than 1.
*/
choices: Array<ChatCompletionSnapshot.Choice>;
/**
* The Unix timestamp (in seconds) of when the chat completion was created.
*/
created: number;
/**
* The model to generate the completion.
*/
model: string;
// Note we do not include an "object" type on the snapshot,
// because the object is not a valid "chat.completion" until finalized.
// object: 'chat.completion';
/**
* This fingerprint represents the backend configuration that the model runs with.
*
* Can be used in conjunction with the `seed` request parameter to understand when
* backend changes have been made that might impact determinism.
*/
system_fingerprint?: string;
}
export namespace ChatCompletionSnapshot {
export interface Choice {
/**
* A chat completion delta generated by streamed model responses.
*/
message: Choice.Message;
/**
* The reason the model stopped generating tokens. This will be `stop` if the model
* hit a natural stop point or a provided stop sequence, `length` if the maximum
* number of tokens specified in the request was reached, `content_filter` if
* content was omitted due to a flag from our content filters, or `function_call`
* if the model called a function.
*/
finish_reason: ChatCompletion.Choice['finish_reason'] | null;
/**
* Log probability information for the choice.
*/
logprobs: ChatCompletion.Choice.Logprobs | null;
/**
* The index of the choice in the list of choices.
*/
index: number;
}
export namespace Choice {
/**
* A chat completion delta generated by streamed model responses.
*/
export interface Message {
/**
* The contents of the chunk message.
*/
content?: string | null;
refusal?: string | null;
parsed?: unknown | null;
/**
* The name and arguments of a function that should be called, as generated by the
* model.
*/
function_call?: Message.FunctionCall;
tool_calls?: Array<Message.ToolCall>;
/**
* The role of the author of this message.
*/
role?: ChatCompletionRole;
}
export namespace Message {
export interface ToolCall {
/**
* The ID of the tool call.
*/
id: string;
function: ToolCall.Function;
/**
* The type of the tool.
*/
type: 'function';
}
export namespace ToolCall {
export interface Function {
/**
* The arguments to call the function with, as generated by the model in JSON
* format. Note that the model does not always generate valid JSON, and may
* hallucinate parameters not defined by your function schema. Validate the
* arguments in your code before calling your function.
*/
arguments: string;
parsed_arguments?: unknown;
/**
* The name of the function to call.
*/
name: string;
}
}
/**
* The name and arguments of a function that should be called, as generated by the
* model.
*/
export interface FunctionCall {
/**
* The arguments to call the function with, as generated by the model in JSON
* format. Note that the model does not always generate valid JSON, and may
* hallucinate parameters not defined by your function schema. Validate the
* arguments in your code before calling your function.
*/
arguments?: string;
/**
* The name of the function to call.
*/
name?: string;
}
}
}
}
type AssertIsEmpty<T extends {}> = keyof T extends never ? T : never;
/**
* Ensures the given argument is an empty object, useful for
* asserting that all known properties on an object have been
* destructured.
*/
function assertIsEmpty<T extends {}>(obj: AssertIsEmpty<T>): asserts obj is AssertIsEmpty<T> {
return;
}
function assertNever(_x: never) {}

View File

@@ -0,0 +1,50 @@
import {
type ChatCompletionChunk,
type ChatCompletionCreateParamsStreaming,
} from '../resources/chat/completions';
import { RunnerOptions, type AbstractChatCompletionRunnerEvents } from './AbstractChatCompletionRunner';
import { type ReadableStream } from '../internal/shim-types';
import { RunnableTools, type BaseFunctionsArgs } from './RunnableFunction';
import { ChatCompletionSnapshot, ChatCompletionStream } from './ChatCompletionStream';
import OpenAI from '../index';
import { AutoParseableTool } from '../lib/parser';
export interface ChatCompletionStreamEvents extends AbstractChatCompletionRunnerEvents {
content: (contentDelta: string, contentSnapshot: string) => void;
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
}
export type ChatCompletionStreamingToolRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsStreaming,
'tools'
> & {
tools: RunnableTools<FunctionsArgs> | AutoParseableTool<any, true>[];
};
export class ChatCompletionStreamingRunner<ParsedT = null>
extends ChatCompletionStream<ParsedT>
implements AsyncIterable<ChatCompletionChunk>
{
static override fromReadableStream(stream: ReadableStream): ChatCompletionStreamingRunner<null> {
const runner = new ChatCompletionStreamingRunner(null);
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
static runTools<T extends (string | object)[], ParsedT = null>(
client: OpenAI,
params: ChatCompletionStreamingToolRunnerParams<T>,
options?: RunnerOptions,
): ChatCompletionStreamingRunner<ParsedT> {
const runner = new ChatCompletionStreamingRunner<ParsedT>(
// @ts-expect-error TODO these types are incompatible
params,
);
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runTools' },
};
runner._run(() => runner._runTools(client, params, opts));
return runner;
}
}

98
node_modules/openai/src/lib/EventEmitter.ts generated vendored Normal file
View File

@@ -0,0 +1,98 @@
type EventListener<Events, EventType extends keyof Events> = Events[EventType];
type EventListeners<Events, EventType extends keyof Events> = Array<{
listener: EventListener<Events, EventType>;
once?: boolean;
}>;
export type EventParameters<Events, EventType extends keyof Events> = {
[Event in EventType]: EventListener<Events, EventType> extends (...args: infer P) => any ? P : never;
}[EventType];
export class EventEmitter<EventTypes extends Record<string, (...args: any) => any>> {
#listeners: {
[Event in keyof EventTypes]?: EventListeners<EventTypes, Event>;
} = {};
/**
* Adds the listener function to the end of the listeners array for the event.
* No checks are made to see if the listener has already been added. Multiple calls passing
* the same combination of event and listener will result in the listener being added, and
* called, multiple times.
* @returns this, so that calls can be chained
*/
on<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener });
return this;
}
/**
* Removes the specified listener from the listener array for the event.
* off() will remove, at most, one instance of a listener from the listener array. If any single
* listener has been added multiple times to the listener array for the specified event, then
* off() must be called multiple times to remove each instance.
* @returns this, so that calls can be chained
*/
off<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners = this.#listeners[event];
if (!listeners) return this;
const index = listeners.findIndex((l) => l.listener === listener);
if (index >= 0) listeners.splice(index, 1);
return this;
}
/**
* Adds a one-time listener function for the event. The next time the event is triggered,
* this listener is removed and then invoked.
* @returns this, so that calls can be chained
*/
once<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener, once: true });
return this;
}
/**
* This is similar to `.once()`, but returns a Promise that resolves the next time
* the event is triggered, instead of calling a listener callback.
* @returns a Promise that resolves the next time given event is triggered,
* or rejects if an error is emitted. (If you request the 'error' event,
* returns a promise that resolves with the error).
*
* Example:
*
* const message = await stream.emitted('message') // rejects if the stream errors
*/
emitted<Event extends keyof EventTypes>(
event: Event,
): Promise<
EventParameters<EventTypes, Event> extends [infer Param] ? Param
: EventParameters<EventTypes, Event> extends [] ? void
: EventParameters<EventTypes, Event>
> {
return new Promise((resolve, reject) => {
// TODO: handle errors
this.once(event, resolve as any);
});
}
protected _emit<Event extends keyof EventTypes>(
this: EventEmitter<EventTypes>,
event: Event,
...args: EventParameters<EventTypes, Event>
) {
const listeners: EventListeners<EventTypes, Event> | undefined = this.#listeners[event];
if (listeners) {
this.#listeners[event] = listeners.filter((l) => !l.once) as any;
listeners.forEach(({ listener }: any) => listener(...(args as any)));
}
}
protected _hasListener(event: keyof EventTypes): boolean {
const listeners = this.#listeners[event];
return listeners && listeners.length > 0;
}
}

239
node_modules/openai/src/lib/EventStream.ts generated vendored Normal file
View File

@@ -0,0 +1,239 @@
import { APIUserAbortError, OpenAIError } from '../error';
export class EventStream<EventTypes extends BaseEvents> {
controller: AbortController = new AbortController();
#connectedPromise: Promise<void>;
#resolveConnectedPromise: () => void = () => {};
#rejectConnectedPromise: (error: OpenAIError) => void = () => {};
#endPromise: Promise<void>;
#resolveEndPromise: () => void = () => {};
#rejectEndPromise: (error: OpenAIError) => void = () => {};
#listeners: {
[Event in keyof EventTypes]?: EventListeners<EventTypes, Event>;
} = {};
#ended = false;
#errored = false;
#aborted = false;
#catchingPromiseCreated = false;
constructor() {
this.#connectedPromise = new Promise<void>((resolve, reject) => {
this.#resolveConnectedPromise = resolve;
this.#rejectConnectedPromise = reject;
});
this.#endPromise = new Promise<void>((resolve, reject) => {
this.#resolveEndPromise = resolve;
this.#rejectEndPromise = reject;
});
// Don't let these promises cause unhandled rejection errors.
// we will manually cause an unhandled rejection error later
// if the user hasn't registered any error listener or called
// any promise-returning method.
this.#connectedPromise.catch(() => {});
this.#endPromise.catch(() => {});
}
protected _run(this: EventStream<EventTypes>, executor: () => Promise<any>) {
// Unfortunately if we call `executor()` immediately we get runtime errors about
// references to `this` before the `super()` constructor call returns.
setTimeout(() => {
executor().then(() => {
this._emitFinal();
this._emit('end');
}, this.#handleError.bind(this));
}, 0);
}
protected _connected(this: EventStream<EventTypes>) {
if (this.ended) return;
this.#resolveConnectedPromise();
this._emit('connect');
}
get ended(): boolean {
return this.#ended;
}
get errored(): boolean {
return this.#errored;
}
get aborted(): boolean {
return this.#aborted;
}
abort() {
this.controller.abort();
}
/**
* Adds the listener function to the end of the listeners array for the event.
* No checks are made to see if the listener has already been added. Multiple calls passing
* the same combination of event and listener will result in the listener being added, and
* called, multiple times.
* @returns this ChatCompletionStream, so that calls can be chained
*/
on<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener });
return this;
}
/**
* Removes the specified listener from the listener array for the event.
* off() will remove, at most, one instance of a listener from the listener array. If any single
* listener has been added multiple times to the listener array for the specified event, then
* off() must be called multiple times to remove each instance.
* @returns this ChatCompletionStream, so that calls can be chained
*/
off<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners = this.#listeners[event];
if (!listeners) return this;
const index = listeners.findIndex((l) => l.listener === listener);
if (index >= 0) listeners.splice(index, 1);
return this;
}
/**
* Adds a one-time listener function for the event. The next time the event is triggered,
* this listener is removed and then invoked.
* @returns this ChatCompletionStream, so that calls can be chained
*/
once<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener, once: true });
return this;
}
/**
* This is similar to `.once()`, but returns a Promise that resolves the next time
* the event is triggered, instead of calling a listener callback.
* @returns a Promise that resolves the next time given event is triggered,
* or rejects if an error is emitted. (If you request the 'error' event,
* returns a promise that resolves with the error).
*
* Example:
*
* const message = await stream.emitted('message') // rejects if the stream errors
*/
emitted<Event extends keyof EventTypes>(
event: Event,
): Promise<
EventParameters<EventTypes, Event> extends [infer Param] ? Param
: EventParameters<EventTypes, Event> extends [] ? void
: EventParameters<EventTypes, Event>
> {
return new Promise((resolve, reject) => {
this.#catchingPromiseCreated = true;
if (event !== 'error') this.once('error', reject);
this.once(event, resolve as any);
});
}
async done(): Promise<void> {
this.#catchingPromiseCreated = true;
await this.#endPromise;
}
#handleError(this: EventStream<EventTypes>, error: unknown) {
this.#errored = true;
if (error instanceof Error && error.name === 'AbortError') {
error = new APIUserAbortError();
}
if (error instanceof APIUserAbortError) {
this.#aborted = true;
return this._emit('abort', error);
}
if (error instanceof OpenAIError) {
return this._emit('error', error);
}
if (error instanceof Error) {
const openAIError: OpenAIError = new OpenAIError(error.message);
// @ts-ignore
openAIError.cause = error;
return this._emit('error', openAIError);
}
return this._emit('error', new OpenAIError(String(error)));
}
_emit<Event extends keyof BaseEvents>(event: Event, ...args: EventParameters<BaseEvents, Event>): void;
_emit<Event extends keyof EventTypes>(event: Event, ...args: EventParameters<EventTypes, Event>): void;
_emit<Event extends keyof EventTypes>(
this: EventStream<EventTypes>,
event: Event,
...args: EventParameters<EventTypes, Event>
) {
// make sure we don't emit any events after end
if (this.#ended) {
return;
}
if (event === 'end') {
this.#ended = true;
this.#resolveEndPromise();
}
const listeners: EventListeners<EventTypes, Event> | undefined = this.#listeners[event];
if (listeners) {
this.#listeners[event] = listeners.filter((l) => !l.once) as any;
listeners.forEach(({ listener }: any) => listener(...(args as any)));
}
if (event === 'abort') {
const error = args[0] as APIUserAbortError;
if (!this.#catchingPromiseCreated && !listeners?.length) {
Promise.reject(error);
}
this.#rejectConnectedPromise(error);
this.#rejectEndPromise(error);
this._emit('end');
return;
}
if (event === 'error') {
// NOTE: _emit('error', error) should only be called from #handleError().
const error = args[0] as OpenAIError;
if (!this.#catchingPromiseCreated && !listeners?.length) {
// Trigger an unhandled rejection if the user hasn't registered any error handlers.
// If you are seeing stack traces here, make sure to handle errors via either:
// - runner.on('error', () => ...)
// - await runner.done()
// - await runner.finalChatCompletion()
// - etc.
Promise.reject(error);
}
this.#rejectConnectedPromise(error);
this.#rejectEndPromise(error);
this._emit('end');
}
}
protected _emitFinal(): void {}
}
type EventListener<Events, EventType extends keyof Events> = Events[EventType];
type EventListeners<Events, EventType extends keyof Events> = Array<{
listener: EventListener<Events, EventType>;
once?: boolean;
}>;
export type EventParameters<Events, EventType extends keyof Events> = {
[Event in EventType]: EventListener<Events, EventType> extends (...args: infer P) => any ? P : never;
}[EventType];
export interface BaseEvents {
connect: () => void;
error: (error: OpenAIError) => void;
abort: (error: APIUserAbortError) => void;
end: () => void;
}

265
node_modules/openai/src/lib/ResponsesParser.ts generated vendored Normal file
View File

@@ -0,0 +1,265 @@
import { OpenAIError } from '../error';
import type { ChatCompletionTool } from '../resources/chat/completions';
import {
ResponseTextConfig,
type FunctionTool,
type ParsedContent,
type ParsedResponse,
type ParsedResponseFunctionToolCall,
type ParsedResponseOutputItem,
type Response,
type ResponseCreateParamsBase,
type ResponseCreateParamsNonStreaming,
type ResponseFunctionToolCall,
type Tool,
} from '../resources/responses/responses';
import { type AutoParseableTextFormat, isAutoParsableResponseFormat } from '../lib/parser';
export type ParseableToolsParams = Array<Tool> | ChatCompletionTool | null;
export type ResponseCreateParamsWithTools = ResponseCreateParamsBase & {
tools?: ParseableToolsParams;
};
type TextConfigParams = { text?: ResponseTextConfig };
export type ExtractParsedContentFromParams<Params extends TextConfigParams> =
NonNullable<Params['text']>['format'] extends AutoParseableTextFormat<infer P> ? P : null;
export function maybeParseResponse<
Params extends ResponseCreateParamsBase | null,
ParsedT = Params extends null ? null : ExtractParsedContentFromParams<NonNullable<Params>>,
>(response: Response, params: Params): ParsedResponse<ParsedT> {
if (!params || !hasAutoParseableInput(params)) {
return {
...response,
output_parsed: null,
output: response.output.map((item) => {
if (item.type === 'function_call') {
return {
...item,
parsed_arguments: null,
};
}
if (item.type === 'message') {
return {
...item,
content: item.content.map((content) => ({
...content,
parsed: null,
})),
};
} else {
return item;
}
}),
};
}
return parseResponse(response, params);
}
export function parseResponse<
Params extends ResponseCreateParamsBase,
ParsedT = ExtractParsedContentFromParams<Params>,
>(response: Response, params: Params): ParsedResponse<ParsedT> {
const output: Array<ParsedResponseOutputItem<ParsedT>> = response.output.map(
(item): ParsedResponseOutputItem<ParsedT> => {
if (item.type === 'function_call') {
return {
...item,
parsed_arguments: parseToolCall(params, item),
};
}
if (item.type === 'message') {
const content: Array<ParsedContent<ParsedT>> = item.content.map((content) => {
if (content.type === 'output_text') {
return {
...content,
parsed: parseTextFormat(params, content.text),
};
}
return content;
});
return {
...item,
content,
};
}
return item;
},
);
const parsed: Omit<ParsedResponse<ParsedT>, 'output_parsed'> = Object.assign({}, response, { output });
if (!Object.getOwnPropertyDescriptor(response, 'output_text')) {
addOutputText(parsed);
}
Object.defineProperty(parsed, 'output_parsed', {
enumerable: true,
get() {
for (const output of parsed.output) {
if (output.type !== 'message') {
continue;
}
for (const content of output.content) {
if (content.type === 'output_text' && content.parsed !== null) {
return content.parsed;
}
}
}
return null;
},
});
return parsed as ParsedResponse<ParsedT>;
}
function parseTextFormat<
Params extends ResponseCreateParamsBase,
ParsedT = ExtractParsedContentFromParams<Params>,
>(params: Params, content: string): ParsedT | null {
if (params.text?.format?.type !== 'json_schema') {
return null;
}
if ('$parseRaw' in params.text?.format) {
const text_format = params.text?.format as unknown as AutoParseableTextFormat<ParsedT>;
return text_format.$parseRaw(content);
}
return JSON.parse(content);
}
export function hasAutoParseableInput(params: ResponseCreateParamsWithTools): boolean {
if (isAutoParsableResponseFormat(params.text?.format)) {
return true;
}
return false;
}
type ToolOptions = {
name: string;
arguments: any;
function?: ((args: any) => any) | undefined;
};
export type AutoParseableResponseTool<
OptionsT extends ToolOptions,
HasFunction = OptionsT['function'] extends Function ? true : false,
> = FunctionTool & {
__arguments: OptionsT['arguments']; // type-level only
__name: OptionsT['name']; // type-level only
$brand: 'auto-parseable-tool';
$callback: ((args: OptionsT['arguments']) => any) | undefined;
$parseRaw(args: string): OptionsT['arguments'];
};
export function makeParseableResponseTool<OptionsT extends ToolOptions>(
tool: FunctionTool,
{
parser,
callback,
}: {
parser: (content: string) => OptionsT['arguments'];
callback: ((args: any) => any) | undefined;
},
): AutoParseableResponseTool<OptionsT['arguments']> {
const obj = { ...tool };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-tool',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
$callback: {
value: callback,
enumerable: false,
},
});
return obj as AutoParseableResponseTool<OptionsT['arguments']>;
}
export function isAutoParsableTool(tool: any): tool is AutoParseableResponseTool<any> {
return tool?.['$brand'] === 'auto-parseable-tool';
}
function getInputToolByName(input_tools: Array<Tool>, name: string): FunctionTool | undefined {
return input_tools.find((tool) => tool.type === 'function' && tool.name === name) as
| FunctionTool
| undefined;
}
function parseToolCall<Params extends ResponseCreateParamsBase>(
params: Params,
toolCall: ResponseFunctionToolCall,
): ParsedResponseFunctionToolCall {
const inputTool = getInputToolByName(params.tools ?? [], toolCall.name);
return {
...toolCall,
...toolCall,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.arguments)
: inputTool?.strict ? JSON.parse(toolCall.arguments)
: null,
};
}
export function shouldParseToolCall(
params: ResponseCreateParamsNonStreaming | null | undefined,
toolCall: ResponseFunctionToolCall,
): boolean {
if (!params) {
return false;
}
const inputTool = getInputToolByName(params.tools ?? [], toolCall.name);
return isAutoParsableTool(inputTool) || inputTool?.strict || false;
}
export function validateInputTools(tools: ChatCompletionTool[] | undefined) {
for (const tool of tools ?? []) {
if (tool.type !== 'function') {
throw new OpenAIError(
`Currently only \`function\` tool types support auto-parsing; Received \`${tool.type}\``,
);
}
if (tool.function.strict !== true) {
throw new OpenAIError(
`The \`${tool.function.name}\` tool is not marked with \`strict: true\`. Only strict function tools can be auto-parsed`,
);
}
}
}
export function addOutputText(rsp: Response): void {
const texts: string[] = [];
for (const output of rsp.output) {
if (output.type !== 'message') {
continue;
}
for (const content of output.content) {
if (content.type === 'output_text') {
texts.push(content.text);
}
}
}
rsp.output_text = texts.join('');
}

114
node_modules/openai/src/lib/RunnableFunction.ts generated vendored Normal file
View File

@@ -0,0 +1,114 @@
import { type ChatCompletionRunner } from './ChatCompletionRunner';
import { type ChatCompletionStreamingRunner } from './ChatCompletionStreamingRunner';
import { JSONSchema } from './jsonschema';
type PromiseOrValue<T> = T | Promise<T>;
export type RunnableFunctionWithParse<Args extends object> = {
/**
* @param args the return value from `parse`.
* @param runner the runner evaluating this callback.
* @returns a string to send back to OpenAI.
*/
function: (
args: Args,
runner: ChatCompletionRunner<unknown> | ChatCompletionStreamingRunner<unknown>,
) => PromiseOrValue<unknown>;
/**
* @param input the raw args from the OpenAI function call.
* @returns the parsed arguments to pass to `function`
*/
parse: (input: string) => PromiseOrValue<Args>;
/**
* The parameters the function accepts, describes as a JSON Schema object.
*/
parameters: JSONSchema;
/**
* A description of what the function does, used by the model to choose when and how to call the function.
*/
description: string;
/**
* The name of the function to be called. Will default to function.name if omitted.
*/
name?: string | undefined;
strict?: boolean | undefined;
};
export type RunnableFunctionWithoutParse = {
/**
* @param args the raw args from the OpenAI function call.
* @returns a string to send back to OpenAI
*/
function: (
args: string,
runner: ChatCompletionRunner<unknown> | ChatCompletionStreamingRunner<unknown>,
) => PromiseOrValue<unknown>;
/**
* The parameters the function accepts, describes as a JSON Schema object.
*/
parameters: JSONSchema;
/**
* A description of what the function does, used by the model to choose when and how to call the function.
*/
description: string;
/**
* The name of the function to be called. Will default to function.name if omitted.
*/
name?: string | undefined;
strict?: boolean | undefined;
};
export type RunnableFunction<Args extends object | string> =
Args extends string ? RunnableFunctionWithoutParse
: Args extends object ? RunnableFunctionWithParse<Args>
: never;
export type RunnableToolFunction<Args extends object | string> =
Args extends string ? RunnableToolFunctionWithoutParse
: Args extends object ? RunnableToolFunctionWithParse<Args>
: never;
export type RunnableToolFunctionWithoutParse = {
type: 'function';
function: RunnableFunctionWithoutParse;
};
export type RunnableToolFunctionWithParse<Args extends object> = {
type: 'function';
function: RunnableFunctionWithParse<Args>;
};
export function isRunnableFunctionWithParse<Args extends object>(
fn: any,
): fn is RunnableFunctionWithParse<Args> {
return typeof (fn as any).parse === 'function';
}
export type BaseFunctionsArgs = readonly (object | string)[];
export type RunnableFunctions<FunctionsArgs extends BaseFunctionsArgs> =
[any[]] extends [FunctionsArgs] ? readonly RunnableFunction<any>[]
: {
[Index in keyof FunctionsArgs]: Index extends number ? RunnableFunction<FunctionsArgs[Index]>
: FunctionsArgs[Index];
};
export type RunnableTools<FunctionsArgs extends BaseFunctionsArgs> =
[any[]] extends [FunctionsArgs] ? readonly RunnableToolFunction<any>[]
: {
[Index in keyof FunctionsArgs]: Index extends number ? RunnableToolFunction<FunctionsArgs[Index]>
: FunctionsArgs[Index];
};
/**
* This is helper class for passing a `function` and `parse` where the `function`
* argument type matches the `parse` return type.
*/
export class ParsingToolFunction<Args extends object> {
type: 'function';
function: RunnableFunctionWithParse<Args>;
constructor(input: RunnableFunctionWithParse<Args>) {
this.type = 'function';
this.function = input;
}
}

23
node_modules/openai/src/lib/Util.ts generated vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* Like `Promise.allSettled()` but throws an error if any promises are rejected.
*/
export const allSettledWithThrow = async <R>(promises: Promise<R>[]): Promise<R[]> => {
const results = await Promise.allSettled(promises);
const rejected = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
if (rejected.length) {
for (const result of rejected) {
console.error(result.reason);
}
throw new Error(`${rejected.length} promise(s) failed - see the above errors`);
}
// Note: TS was complaining about using `.filter().map()` here for some reason
const values: R[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
values.push(result.value);
}
}
return values;
};

21
node_modules/openai/src/lib/chatCompletionUtils.ts generated vendored Normal file
View File

@@ -0,0 +1,21 @@
import {
type ChatCompletionAssistantMessageParam,
type ChatCompletionMessageParam,
type ChatCompletionToolMessageParam,
} from '../resources';
export const isAssistantMessage = (
message: ChatCompletionMessageParam | null | undefined,
): message is ChatCompletionAssistantMessageParam => {
return message?.role === 'assistant';
};
export const isToolMessage = (
message: ChatCompletionMessageParam | null | undefined,
): message is ChatCompletionToolMessageParam => {
return message?.role === 'tool';
};
export function isPresent<T>(obj: T | null | undefined): obj is T {
return obj != null;
}

148
node_modules/openai/src/lib/jsonschema.ts generated vendored Normal file
View File

@@ -0,0 +1,148 @@
// File mostly copied from @types/json-schema, but stripped down a bit for brevity
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/817274f3280152ba2929a6067c93df8b34c4c9aa/types/json-schema/index.d.ts
//
// ==================================================================================================
// JSON Schema Draft 07
// ==================================================================================================
// https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
// --------------------------------------------------------------------------------------------------
/**
* Primitive type
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
*/
export type JSONSchemaTypeName =
| ({} & string)
| 'string'
| 'number'
| 'integer'
| 'boolean'
| 'object'
| 'array'
| 'null';
/**
* Primitive type
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
*/
export type JSONSchemaType =
| string //
| number
| boolean
| JSONSchemaObject
| JSONSchemaArray
| null;
// Workaround for infinite type recursion
export interface JSONSchemaObject {
[key: string]: JSONSchemaType;
}
// Workaround for infinite type recursion
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
export interface JSONSchemaArray extends Array<JSONSchemaType> {}
/**
* Meta schema
*
* Recommended values:
* - 'http://json-schema.org/schema#'
* - 'http://json-schema.org/hyper-schema#'
* - 'http://json-schema.org/draft-07/schema#'
* - 'http://json-schema.org/draft-07/hyper-schema#'
*
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5
*/
export type JSONSchemaVersion = string;
/**
* JSON Schema v7
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
*/
export type JSONSchemaDefinition = JSONSchema | boolean;
export interface JSONSchema {
$id?: string | undefined;
$comment?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1
*/
type?: JSONSchemaTypeName | JSONSchemaTypeName[] | undefined;
enum?: JSONSchemaType[] | undefined;
const?: JSONSchemaType | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2
*/
multipleOf?: number | undefined;
maximum?: number | undefined;
exclusiveMaximum?: number | undefined;
minimum?: number | undefined;
exclusiveMinimum?: number | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3
*/
maxLength?: number | undefined;
minLength?: number | undefined;
pattern?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4
*/
items?: JSONSchemaDefinition | JSONSchemaDefinition[] | undefined;
additionalItems?: JSONSchemaDefinition | undefined;
maxItems?: number | undefined;
minItems?: number | undefined;
uniqueItems?: boolean | undefined;
contains?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5
*/
maxProperties?: number | undefined;
minProperties?: number | undefined;
required?: string[] | undefined;
properties?:
| {
[key: string]: JSONSchemaDefinition;
}
| undefined;
patternProperties?:
| {
[key: string]: JSONSchemaDefinition;
}
| undefined;
additionalProperties?: JSONSchemaDefinition | undefined;
propertyNames?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6
*/
if?: JSONSchemaDefinition | undefined;
then?: JSONSchemaDefinition | undefined;
else?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7
*/
allOf?: JSONSchemaDefinition[] | undefined;
anyOf?: JSONSchemaDefinition[] | undefined;
oneOf?: JSONSchemaDefinition[] | undefined;
not?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7
*/
format?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10
*/
title?: string | undefined;
description?: string | undefined;
default?: JSONSchemaType | undefined;
readOnly?: boolean | undefined;
writeOnly?: boolean | undefined;
examples?: JSONSchemaType | undefined;
}

311
node_modules/openai/src/lib/parser.ts generated vendored Normal file
View File

@@ -0,0 +1,311 @@
import { ContentFilterFinishReasonError, LengthFinishReasonError, OpenAIError } from '../error';
import {
ChatCompletion,
ChatCompletionCreateParams,
ChatCompletionCreateParamsBase,
ChatCompletionFunctionTool,
ChatCompletionMessage,
ChatCompletionMessageFunctionToolCall,
ChatCompletionStreamingToolRunnerParams,
ChatCompletionStreamParams,
ChatCompletionToolRunnerParams,
ParsedChatCompletion,
ParsedChoice,
ParsedFunctionToolCall,
} from '../resources/chat/completions';
import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses';
import { ResponseFormatJSONSchema } from '../resources/shared';
type AnyChatCompletionCreateParams =
| ChatCompletionCreateParams
| ChatCompletionToolRunnerParams<any>
| ChatCompletionStreamingToolRunnerParams<any>
| ChatCompletionStreamParams;
type Unpacked<T> = T extends (infer U)[] ? U : T;
type ToolCall = Unpacked<ChatCompletionCreateParamsBase['tools']>;
export function isChatCompletionFunctionTool(tool: ToolCall): tool is ChatCompletionFunctionTool {
return tool !== undefined && 'function' in tool && tool.function !== undefined;
}
export type ExtractParsedContentFromParams<Params extends AnyChatCompletionCreateParams> =
Params['response_format'] extends AutoParseableResponseFormat<infer P> ? P : null;
export type AutoParseableResponseFormat<ParsedT> = ResponseFormatJSONSchema & {
__output: ParsedT; // type-level only
$brand: 'auto-parseable-response-format';
$parseRaw(content: string): ParsedT;
};
export function makeParseableResponseFormat<ParsedT>(
response_format: ResponseFormatJSONSchema,
parser: (content: string) => ParsedT,
): AutoParseableResponseFormat<ParsedT> {
const obj = { ...response_format };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-response-format',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
});
return obj as AutoParseableResponseFormat<ParsedT>;
}
export type AutoParseableTextFormat<ParsedT> = ResponseFormatTextJSONSchemaConfig & {
__output: ParsedT; // type-level only
$brand: 'auto-parseable-response-format';
$parseRaw(content: string): ParsedT;
};
export function makeParseableTextFormat<ParsedT>(
response_format: ResponseFormatTextJSONSchemaConfig,
parser: (content: string) => ParsedT,
): AutoParseableTextFormat<ParsedT> {
const obj = { ...response_format };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-response-format',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
});
return obj as AutoParseableTextFormat<ParsedT>;
}
export function isAutoParsableResponseFormat<ParsedT>(
response_format: any,
): response_format is AutoParseableResponseFormat<ParsedT> {
return response_format?.['$brand'] === 'auto-parseable-response-format';
}
type ToolOptions = {
name: string;
arguments: any;
function?: ((args: any) => any) | undefined;
};
export type AutoParseableTool<
OptionsT extends ToolOptions,
HasFunction = OptionsT['function'] extends Function ? true : false,
> = ChatCompletionFunctionTool & {
__arguments: OptionsT['arguments']; // type-level only
__name: OptionsT['name']; // type-level only
__hasFunction: HasFunction; // type-level only
$brand: 'auto-parseable-tool';
$callback: ((args: OptionsT['arguments']) => any) | undefined;
$parseRaw(args: string): OptionsT['arguments'];
};
export function makeParseableTool<OptionsT extends ToolOptions>(
tool: ChatCompletionFunctionTool,
{
parser,
callback,
}: {
parser: (content: string) => OptionsT['arguments'];
callback: ((args: any) => any) | undefined;
},
): AutoParseableTool<OptionsT['arguments']> {
const obj = { ...tool };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-tool',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
$callback: {
value: callback,
enumerable: false,
},
});
return obj as AutoParseableTool<OptionsT['arguments']>;
}
export function isAutoParsableTool(tool: any): tool is AutoParseableTool<any> {
return tool?.['$brand'] === 'auto-parseable-tool';
}
export function maybeParseChatCompletion<
Params extends ChatCompletionCreateParams | null,
ParsedT = Params extends null ? null : ExtractParsedContentFromParams<NonNullable<Params>>,
>(completion: ChatCompletion, params: Params): ParsedChatCompletion<ParsedT> {
if (!params || !hasAutoParseableInput(params)) {
return {
...completion,
choices: completion.choices.map((choice) => {
assertToolCallsAreChatCompletionFunctionToolCalls(choice.message.tool_calls);
return {
...choice,
message: {
...choice.message,
parsed: null,
...(choice.message.tool_calls ?
{
tool_calls: choice.message.tool_calls,
}
: undefined),
},
};
}),
} as ParsedChatCompletion<ParsedT>;
}
return parseChatCompletion(completion, params);
}
export function parseChatCompletion<
Params extends ChatCompletionCreateParams,
ParsedT = ExtractParsedContentFromParams<Params>,
>(completion: ChatCompletion, params: Params): ParsedChatCompletion<ParsedT> {
const choices: Array<ParsedChoice<ParsedT>> = completion.choices.map((choice): ParsedChoice<ParsedT> => {
if (choice.finish_reason === 'length') {
throw new LengthFinishReasonError();
}
if (choice.finish_reason === 'content_filter') {
throw new ContentFilterFinishReasonError();
}
assertToolCallsAreChatCompletionFunctionToolCalls(choice.message.tool_calls);
return {
...choice,
message: {
...choice.message,
...(choice.message.tool_calls ?
{
tool_calls:
choice.message.tool_calls?.map((toolCall) => parseToolCall(params, toolCall)) ?? undefined,
}
: undefined),
parsed:
choice.message.content && !choice.message.refusal ?
parseResponseFormat(params, choice.message.content)
: null,
},
} as ParsedChoice<ParsedT>;
});
return { ...completion, choices };
}
function parseResponseFormat<
Params extends ChatCompletionCreateParams,
ParsedT = ExtractParsedContentFromParams<Params>,
>(params: Params, content: string): ParsedT | null {
if (params.response_format?.type !== 'json_schema') {
return null;
}
if (params.response_format?.type === 'json_schema') {
if ('$parseRaw' in params.response_format) {
const response_format = params.response_format as AutoParseableResponseFormat<ParsedT>;
return response_format.$parseRaw(content);
}
return JSON.parse(content);
}
return null;
}
function parseToolCall<Params extends ChatCompletionCreateParams>(
params: Params,
toolCall: ChatCompletionMessageFunctionToolCall,
): ParsedFunctionToolCall {
const inputTool = params.tools?.find(
(inputTool) =>
isChatCompletionFunctionTool(inputTool) && inputTool.function?.name === toolCall.function.name,
) as ChatCompletionFunctionTool | undefined; // TS doesn't narrow based on isChatCompletionTool
return {
...toolCall,
function: {
...toolCall.function,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.function.arguments)
: inputTool?.function.strict ? JSON.parse(toolCall.function.arguments)
: null,
},
};
}
export function shouldParseToolCall(
params: ChatCompletionCreateParams | null | undefined,
toolCall: ChatCompletionMessageFunctionToolCall,
): boolean {
if (!params || !('tools' in params) || !params.tools) {
return false;
}
const inputTool = params.tools?.find(
(inputTool) =>
isChatCompletionFunctionTool(inputTool) && inputTool.function?.name === toolCall.function.name,
);
return (
isChatCompletionFunctionTool(inputTool) &&
(isAutoParsableTool(inputTool) || inputTool?.function.strict || false)
);
}
export function hasAutoParseableInput(params: AnyChatCompletionCreateParams): boolean {
if (isAutoParsableResponseFormat(params.response_format)) {
return true;
}
return (
params.tools?.some(
(t) => isAutoParsableTool(t) || (t.type === 'function' && t.function.strict === true),
) ?? false
);
}
export function assertToolCallsAreChatCompletionFunctionToolCalls(
toolCalls: ChatCompletionMessage['tool_calls'],
): asserts toolCalls is ChatCompletionMessageFunctionToolCall[] {
for (const toolCall of toolCalls || []) {
if (toolCall.type !== 'function') {
throw new OpenAIError(
`Currently only \`function\` tool calls are supported; Received \`${toolCall.type}\``,
);
}
}
}
export function validateInputTools(tools: ChatCompletionCreateParamsBase['tools']) {
for (const tool of tools ?? []) {
if (tool.type !== 'function') {
throw new OpenAIError(
`Currently only \`function\` tool types support auto-parsing; Received \`${tool.type}\``,
);
}
if (tool.function.strict !== true) {
throw new OpenAIError(
`The \`${tool.function.name}\` tool is not marked with \`strict: true\`. Only strict function tools can be auto-parsed`,
);
}
}
}

74
node_modules/openai/src/lib/responses/EventTypes.ts generated vendored Normal file
View File

@@ -0,0 +1,74 @@
import {
ResponseAudioDeltaEvent,
ResponseAudioDoneEvent,
ResponseAudioTranscriptDeltaEvent,
ResponseAudioTranscriptDoneEvent,
ResponseCodeInterpreterCallCodeDeltaEvent,
ResponseCodeInterpreterCallCodeDoneEvent,
ResponseCodeInterpreterCallCompletedEvent,
ResponseCodeInterpreterCallInProgressEvent,
ResponseCodeInterpreterCallInterpretingEvent,
ResponseCompletedEvent,
ResponseContentPartAddedEvent,
ResponseContentPartDoneEvent,
ResponseCreatedEvent,
ResponseErrorEvent,
ResponseFailedEvent,
ResponseFileSearchCallCompletedEvent,
ResponseFileSearchCallInProgressEvent,
ResponseFileSearchCallSearchingEvent,
ResponseFunctionCallArgumentsDeltaEvent as RawResponseFunctionCallArgumentsDeltaEvent,
ResponseFunctionCallArgumentsDoneEvent,
ResponseInProgressEvent,
ResponseOutputItemAddedEvent,
ResponseOutputItemDoneEvent,
ResponseRefusalDeltaEvent,
ResponseRefusalDoneEvent,
ResponseTextDeltaEvent as RawResponseTextDeltaEvent,
ResponseTextDoneEvent,
ResponseIncompleteEvent,
ResponseWebSearchCallCompletedEvent,
ResponseWebSearchCallInProgressEvent,
ResponseWebSearchCallSearchingEvent,
} from '../../resources/responses/responses';
export type ResponseFunctionCallArgumentsDeltaEvent = RawResponseFunctionCallArgumentsDeltaEvent & {
snapshot: string;
};
export type ResponseTextDeltaEvent = RawResponseTextDeltaEvent & {
snapshot: string;
};
export type ParsedResponseStreamEvent =
| ResponseAudioDeltaEvent
| ResponseAudioDoneEvent
| ResponseAudioTranscriptDeltaEvent
| ResponseAudioTranscriptDoneEvent
| ResponseCodeInterpreterCallCodeDeltaEvent
| ResponseCodeInterpreterCallCodeDoneEvent
| ResponseCodeInterpreterCallCompletedEvent
| ResponseCodeInterpreterCallInProgressEvent
| ResponseCodeInterpreterCallInterpretingEvent
| ResponseCompletedEvent
| ResponseContentPartAddedEvent
| ResponseContentPartDoneEvent
| ResponseCreatedEvent
| ResponseErrorEvent
| ResponseFileSearchCallCompletedEvent
| ResponseFileSearchCallInProgressEvent
| ResponseFileSearchCallSearchingEvent
| ResponseFunctionCallArgumentsDeltaEvent
| ResponseFunctionCallArgumentsDoneEvent
| ResponseInProgressEvent
| ResponseFailedEvent
| ResponseIncompleteEvent
| ResponseOutputItemAddedEvent
| ResponseOutputItemDoneEvent
| ResponseRefusalDeltaEvent
| ResponseRefusalDoneEvent
| ResponseTextDeltaEvent
| ResponseTextDoneEvent
| ResponseWebSearchCallCompletedEvent
| ResponseWebSearchCallInProgressEvent
| ResponseWebSearchCallSearchingEvent;

344
node_modules/openai/src/lib/responses/ResponseStream.ts generated vendored Normal file
View File

@@ -0,0 +1,344 @@
import {
ResponseTextConfig,
type ParsedResponse,
type Response,
type ResponseCreateParamsBase,
type ResponseCreateParamsStreaming,
type ResponseStreamEvent,
} from '../../resources/responses/responses';
import { RequestOptions } from '../../internal/request-options';
import { APIUserAbortError, OpenAIError } from '../../error';
import OpenAI from '../../index';
import { type BaseEvents, EventStream } from '../EventStream';
import { type ResponseFunctionCallArgumentsDeltaEvent, type ResponseTextDeltaEvent } from './EventTypes';
import { maybeParseResponse, ParseableToolsParams } from '../ResponsesParser';
import { Stream } from '../../streaming';
export type ResponseStreamParams = ResponseCreateAndStreamParams | ResponseStreamByIdParams;
export type ResponseCreateAndStreamParams = Omit<ResponseCreateParamsBase, 'stream'> & {
stream?: true;
};
export type ResponseStreamByIdParams = {
/**
* The ID of the response to stream.
*/
response_id: string;
/**
* If provided, the stream will start after the event with the given sequence number.
*/
starting_after?: number;
/**
* Configuration options for a text response from the model. Can be plain text or
* structured JSON data. Learn more:
*
* - [Text inputs and outputs](https://platform.openai.com/docs/guides/text)
* - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)
*/
text?: ResponseTextConfig;
/**
* An array of tools the model may call while generating a response. When continuing a stream, provide
* the same tools as the original request.
*/
tools?: ParseableToolsParams;
};
type ResponseEvents = BaseEvents &
Omit<
{
[K in ResponseStreamEvent['type']]: (event: Extract<ResponseStreamEvent, { type: K }>) => void;
},
'response.output_text.delta' | 'response.function_call_arguments.delta'
> & {
event: (event: ResponseStreamEvent) => void;
'response.output_text.delta': (event: ResponseTextDeltaEvent) => void;
'response.function_call_arguments.delta': (event: ResponseFunctionCallArgumentsDeltaEvent) => void;
};
export type ResponseStreamingParams = Omit<ResponseCreateParamsBase, 'stream'> & {
stream?: true;
};
export class ResponseStream<ParsedT = null>
extends EventStream<ResponseEvents>
implements AsyncIterable<ResponseStreamEvent>
{
#params: ResponseStreamingParams | null;
#currentResponseSnapshot: Response | undefined;
#finalResponse: ParsedResponse<ParsedT> | undefined;
constructor(params: ResponseStreamingParams | null) {
super();
this.#params = params;
}
static createResponse<ParsedT>(
client: OpenAI,
params: ResponseStreamParams,
options?: RequestOptions,
): ResponseStream<ParsedT> {
const runner = new ResponseStream<ParsedT>(params as ResponseCreateParamsStreaming);
runner._run(() =>
runner._createOrRetrieveResponse(client, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
#beginRequest() {
if (this.ended) return;
this.#currentResponseSnapshot = undefined;
}
#addEvent(this: ResponseStream<ParsedT>, event: ResponseStreamEvent, starting_after: number | null) {
if (this.ended) return;
const maybeEmit = (name: string, event: ResponseStreamEvent & { snapshot?: string }) => {
if (starting_after == null || event.sequence_number > starting_after) {
this._emit(name as any, event);
}
};
const response = this.#accumulateResponse(event);
maybeEmit('event', event);
switch (event.type) {
case 'response.output_text.delta': {
const output = response.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
const content = output.content[event.content_index];
if (!content) {
throw new OpenAIError(`missing content at index ${event.content_index}`);
}
if (content.type !== 'output_text') {
throw new OpenAIError(`expected content to be 'output_text', got ${content.type}`);
}
maybeEmit('response.output_text.delta', {
...event,
snapshot: content.text,
});
}
break;
}
case 'response.function_call_arguments.delta': {
const output = response.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'function_call') {
maybeEmit('response.function_call_arguments.delta', {
...event,
snapshot: output.arguments,
});
}
break;
}
default:
maybeEmit(event.type, event);
break;
}
}
#endRequest(): ParsedResponse<ParsedT> {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
const snapshot = this.#currentResponseSnapshot;
if (!snapshot) {
throw new OpenAIError(`request ended without sending any events`);
}
this.#currentResponseSnapshot = undefined;
const parsedResponse = finalizeResponse<ParsedT>(snapshot, this.#params);
this.#finalResponse = parsedResponse;
return parsedResponse;
}
protected async _createOrRetrieveResponse(
client: OpenAI,
params: ResponseStreamParams,
options?: RequestOptions,
): Promise<ParsedResponse<ParsedT>> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
let stream: Stream<ResponseStreamEvent> | undefined;
let starting_after: number | null = null;
if ('response_id' in params) {
stream = await client.responses.retrieve(
params.response_id,
{ stream: true },
{ ...options, signal: this.controller.signal, stream: true },
);
starting_after = params.starting_after ?? null;
} else {
stream = await client.responses.create(
{ ...params, stream: true },
{ ...options, signal: this.controller.signal },
);
}
this._connected();
for await (const event of stream) {
this.#addEvent(event, starting_after);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this.#endRequest();
}
#accumulateResponse(event: ResponseStreamEvent): Response {
let snapshot = this.#currentResponseSnapshot;
if (!snapshot) {
if (event.type !== 'response.created') {
throw new OpenAIError(
`When snapshot hasn't been set yet, expected 'response.created' event, got ${event.type}`,
);
}
snapshot = this.#currentResponseSnapshot = event.response;
return snapshot;
}
switch (event.type) {
case 'response.output_item.added': {
snapshot.output.push(event.item);
break;
}
case 'response.content_part.added': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
output.content.push(event.part);
}
break;
}
case 'response.output_text.delta': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
const content = output.content[event.content_index];
if (!content) {
throw new OpenAIError(`missing content at index ${event.content_index}`);
}
if (content.type !== 'output_text') {
throw new OpenAIError(`expected content to be 'output_text', got ${content.type}`);
}
content.text += event.delta;
}
break;
}
case 'response.function_call_arguments.delta': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'function_call') {
output.arguments += event.delta;
}
break;
}
case 'response.completed': {
this.#currentResponseSnapshot = event.response;
break;
}
}
return snapshot;
}
[Symbol.asyncIterator](this: ResponseStream<ParsedT>): AsyncIterator<ResponseStreamEvent> {
const pushQueue: ResponseStreamEvent[] = [];
const readQueue: {
resolve: (event: ResponseStreamEvent | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
this.on('event', (event) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(event);
} else {
pushQueue.push(event);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<ResponseStreamEvent>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<ResponseStreamEvent | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((event) => (event ? { value: event, done: false } : { value: undefined, done: true }));
}
const event = pushQueue.shift()!;
return { value: event, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
/**
* @returns a promise that resolves with the final Response, or rejects
* if an error occurred or the stream ended prematurely without producing a REsponse.
*/
async finalResponse(): Promise<ParsedResponse<ParsedT>> {
await this.done();
const response = this.#finalResponse;
if (!response) throw new OpenAIError('stream ended without producing a ChatCompletion');
return response;
}
}
function finalizeResponse<ParsedT>(
snapshot: Response,
params: ResponseStreamingParams | null,
): ParsedResponse<ParsedT> {
return maybeParseResponse(snapshot, params);
}