初始化仓库
This commit is contained in:
4
node_modules/openai/src/lib/.keep
generated
vendored
Normal file
4
node_modules/openai/src/lib/.keep
generated
vendored
Normal 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.
|
||||
404
node_modules/openai/src/lib/AbstractChatCompletionRunner.ts
generated
vendored
Normal file
404
node_modules/openai/src/lib/AbstractChatCompletionRunner.ts
generated
vendored
Normal 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
778
node_modules/openai/src/lib/AssistantStream.ts
generated
vendored
Normal 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
54
node_modules/openai/src/lib/ChatCompletionRunner.ts
generated
vendored
Normal 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
872
node_modules/openai/src/lib/ChatCompletionStream.ts
generated
vendored
Normal 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) {}
|
||||
50
node_modules/openai/src/lib/ChatCompletionStreamingRunner.ts
generated
vendored
Normal file
50
node_modules/openai/src/lib/ChatCompletionStreamingRunner.ts
generated
vendored
Normal 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
98
node_modules/openai/src/lib/EventEmitter.ts
generated
vendored
Normal 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
239
node_modules/openai/src/lib/EventStream.ts
generated
vendored
Normal 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
265
node_modules/openai/src/lib/ResponsesParser.ts
generated
vendored
Normal 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
114
node_modules/openai/src/lib/RunnableFunction.ts
generated
vendored
Normal 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
23
node_modules/openai/src/lib/Util.ts
generated
vendored
Normal 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
21
node_modules/openai/src/lib/chatCompletionUtils.ts
generated
vendored
Normal 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
148
node_modules/openai/src/lib/jsonschema.ts
generated
vendored
Normal 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
311
node_modules/openai/src/lib/parser.ts
generated
vendored
Normal 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
74
node_modules/openai/src/lib/responses/EventTypes.ts
generated
vendored
Normal 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
344
node_modules/openai/src/lib/responses/ResponseStream.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user