UNPKG

@langchain/openai

Version:
1,470 lines 61.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatOpenAI = exports._convertMessagesToOpenAIParams = exports.messageToOpenAIRole = void 0; const openai_1 = require("openai"); const messages_1 = require("@langchain/core/messages"); const outputs_1 = require("@langchain/core/outputs"); const env_1 = require("@langchain/core/utils/env"); const chat_models_1 = require("@langchain/core/language_models/chat_models"); const base_1 = require("@langchain/core/language_models/base"); const runnables_1 = require("@langchain/core/runnables"); const output_parsers_1 = require("@langchain/core/output_parsers"); const openai_tools_1 = require("@langchain/core/output_parsers/openai_tools"); const zod_to_json_schema_1 = require("zod-to-json-schema"); const zod_1 = require("openai/helpers/zod"); const azure_js_1 = require("./utils/azure.cjs"); const openai_js_1 = require("./utils/openai.cjs"); const openai_format_fndef_js_1 = require("./utils/openai-format-fndef.cjs"); const tools_js_1 = require("./utils/tools.cjs"); function extractGenericMessageCustomRole(message) { if (message.role !== "system" && message.role !== "developer" && message.role !== "assistant" && message.role !== "user" && message.role !== "function" && message.role !== "tool") { console.warn(`Unknown message role: ${message.role}`); } return message.role; } function messageToOpenAIRole(message) { const type = message._getType(); switch (type) { case "system": return "system"; case "ai": return "assistant"; case "human": return "user"; case "function": return "function"; case "tool": return "tool"; case "generic": { if (!messages_1.ChatMessage.isInstance(message)) throw new Error("Invalid generic chat message"); return extractGenericMessageCustomRole(message); } default: throw new Error(`Unknown message type: ${type}`); } } exports.messageToOpenAIRole = messageToOpenAIRole; // Used in LangSmith, export is important here function _convertMessagesToOpenAIParams(messages, model) { // TODO: Function messages do not support array content, fix cast return messages.flatMap((message) => { let role = messageToOpenAIRole(message); if (role === "system" && isReasoningModel(model)) { role = "developer"; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const completionParam = { role, content: message.content, }; if (message.name != null) { completionParam.name = message.name; } if (message.additional_kwargs.function_call != null) { completionParam.function_call = message.additional_kwargs.function_call; completionParam.content = null; } if ((0, messages_1.isAIMessage)(message) && !!message.tool_calls?.length) { completionParam.tool_calls = message.tool_calls.map(openai_tools_1.convertLangChainToolCallToOpenAI); completionParam.content = null; } else { if (message.additional_kwargs.tool_calls != null) { completionParam.tool_calls = message.additional_kwargs.tool_calls; } if (message.tool_call_id != null) { completionParam.tool_call_id = message.tool_call_id; } } if (message.additional_kwargs.audio && typeof message.additional_kwargs.audio === "object" && "id" in message.additional_kwargs.audio) { const audioMessage = { role: "assistant", audio: { id: message.additional_kwargs.audio.id, }, }; return [completionParam, audioMessage]; } return completionParam; }); } exports._convertMessagesToOpenAIParams = _convertMessagesToOpenAIParams; function _convertChatOpenAIToolTypeToOpenAITool(tool, fields) { if ((0, base_1.isOpenAITool)(tool)) { if (fields?.strict !== undefined) { return { ...tool, function: { ...tool.function, strict: fields.strict, }, }; } return tool; } return (0, tools_js_1._convertToOpenAITool)(tool, fields); } function isReasoningModel(model) { return model?.startsWith("o1") || model?.startsWith("o3"); } /** * OpenAI chat model integration. * * To use with Azure, import the `AzureChatOpenAI` class. * * Setup: * Install `@langchain/openai` and set an environment variable named `OPENAI_API_KEY`. * * ```bash * npm install @langchain/openai * export OPENAI_API_KEY="your-api-key" * ``` * * ## [Constructor args](https://api.js.langchain.com/classes/langchain_openai.ChatOpenAI.html#constructor) * * ## [Runtime args](https://api.js.langchain.com/interfaces/langchain_openai.ChatOpenAICallOptions.html) * * Runtime args can be passed as the second argument to any of the base runnable methods `.invoke`. `.stream`, `.batch`, etc. * They can also be passed via `.bind`, or the second arg in `.bindTools`, like shown in the examples below: * * ```typescript * // When calling `.bind`, call options should be passed via the first argument * const llmWithArgsBound = llm.bind({ * stop: ["\n"], * tools: [...], * }); * * // When calling `.bindTools`, call options should be passed via the second argument * const llmWithTools = llm.bindTools( * [...], * { * tool_choice: "auto", * } * ); * ``` * * ## Examples * * <details open> * <summary><strong>Instantiate</strong></summary> * * ```typescript * import { ChatOpenAI } from '@langchain/openai'; * * const llm = new ChatOpenAI({ * model: "gpt-4o", * temperature: 0, * maxTokens: undefined, * timeout: undefined, * maxRetries: 2, * // apiKey: "...", * // baseUrl: "...", * // organization: "...", * // other params... * }); * ``` * </details> * * <br /> * * <details> * <summary><strong>Invoking</strong></summary> * * ```typescript * const input = `Translate "I love programming" into French.`; * * // Models also accept a list of chat messages or a formatted prompt * const result = await llm.invoke(input); * console.log(result); * ``` * * ```txt * AIMessage { * "id": "chatcmpl-9u4Mpu44CbPjwYFkTbeoZgvzB00Tz", * "content": "J'adore la programmation.", * "response_metadata": { * "tokenUsage": { * "completionTokens": 5, * "promptTokens": 28, * "totalTokens": 33 * }, * "finish_reason": "stop", * "system_fingerprint": "fp_3aa7262c27" * }, * "usage_metadata": { * "input_tokens": 28, * "output_tokens": 5, * "total_tokens": 33 * } * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Streaming Chunks</strong></summary> * * ```typescript * for await (const chunk of await llm.stream(input)) { * console.log(chunk); * } * ``` * * ```txt * AIMessageChunk { * "id": "chatcmpl-9u4NWB7yUeHCKdLr6jP3HpaOYHTqs", * "content": "" * } * AIMessageChunk { * "content": "J" * } * AIMessageChunk { * "content": "'adore" * } * AIMessageChunk { * "content": " la" * } * AIMessageChunk { * "content": " programmation",, * } * AIMessageChunk { * "content": ".",, * } * AIMessageChunk { * "content": "", * "response_metadata": { * "finish_reason": "stop", * "system_fingerprint": "fp_c9aa9c0491" * }, * } * AIMessageChunk { * "content": "", * "usage_metadata": { * "input_tokens": 28, * "output_tokens": 5, * "total_tokens": 33 * } * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Aggregate Streamed Chunks</strong></summary> * * ```typescript * import { AIMessageChunk } from '@langchain/core/messages'; * import { concat } from '@langchain/core/utils/stream'; * * const stream = await llm.stream(input); * let full: AIMessageChunk | undefined; * for await (const chunk of stream) { * full = !full ? chunk : concat(full, chunk); * } * console.log(full); * ``` * * ```txt * AIMessageChunk { * "id": "chatcmpl-9u4PnX6Fy7OmK46DASy0bH6cxn5Xu", * "content": "J'adore la programmation.", * "response_metadata": { * "prompt": 0, * "completion": 0, * "finish_reason": "stop", * }, * "usage_metadata": { * "input_tokens": 28, * "output_tokens": 5, * "total_tokens": 33 * } * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Bind tools</strong></summary> * * ```typescript * import { z } from 'zod'; * * const GetWeather = { * name: "GetWeather", * description: "Get the current weather in a given location", * schema: z.object({ * location: z.string().describe("The city and state, e.g. San Francisco, CA") * }), * } * * const GetPopulation = { * name: "GetPopulation", * description: "Get the current population in a given location", * schema: z.object({ * location: z.string().describe("The city and state, e.g. San Francisco, CA") * }), * } * * const llmWithTools = llm.bindTools( * [GetWeather, GetPopulation], * { * // strict: true // enforce tool args schema is respected * } * ); * const aiMsg = await llmWithTools.invoke( * "Which city is hotter today and which is bigger: LA or NY?" * ); * console.log(aiMsg.tool_calls); * ``` * * ```txt * [ * { * name: 'GetWeather', * args: { location: 'Los Angeles, CA' }, * type: 'tool_call', * id: 'call_uPU4FiFzoKAtMxfmPnfQL6UK' * }, * { * name: 'GetWeather', * args: { location: 'New York, NY' }, * type: 'tool_call', * id: 'call_UNkEwuQsHrGYqgDQuH9nPAtX' * }, * { * name: 'GetPopulation', * args: { location: 'Los Angeles, CA' }, * type: 'tool_call', * id: 'call_kL3OXxaq9OjIKqRTpvjaCH14' * }, * { * name: 'GetPopulation', * args: { location: 'New York, NY' }, * type: 'tool_call', * id: 'call_s9KQB1UWj45LLGaEnjz0179q' * } * ] * ``` * </details> * * <br /> * * <details> * <summary><strong>Structured Output</strong></summary> * * ```typescript * import { z } from 'zod'; * * const Joke = z.object({ * setup: z.string().describe("The setup of the joke"), * punchline: z.string().describe("The punchline to the joke"), * rating: z.number().nullable().describe("How funny the joke is, from 1 to 10") * }).describe('Joke to tell user.'); * * const structuredLlm = llm.withStructuredOutput(Joke, { * name: "Joke", * strict: true, // Optionally enable OpenAI structured outputs * }); * const jokeResult = await structuredLlm.invoke("Tell me a joke about cats"); * console.log(jokeResult); * ``` * * ```txt * { * setup: 'Why was the cat sitting on the computer?', * punchline: 'Because it wanted to keep an eye on the mouse!', * rating: 7 * } * ``` * </details> * * <br /> * * <details> * <summary><strong>JSON Object Response Format</strong></summary> * * ```typescript * const jsonLlm = llm.bind({ response_format: { type: "json_object" } }); * const jsonLlmAiMsg = await jsonLlm.invoke( * "Return a JSON object with key 'randomInts' and a value of 10 random ints in [0-99]" * ); * console.log(jsonLlmAiMsg.content); * ``` * * ```txt * { * "randomInts": [23, 87, 45, 12, 78, 34, 56, 90, 11, 67] * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Multimodal</strong></summary> * * ```typescript * import { HumanMessage } from '@langchain/core/messages'; * * const imageUrl = "https://example.com/image.jpg"; * const imageData = await fetch(imageUrl).then(res => res.arrayBuffer()); * const base64Image = Buffer.from(imageData).toString('base64'); * * const message = new HumanMessage({ * content: [ * { type: "text", text: "describe the weather in this image" }, * { * type: "image_url", * image_url: { url: `data:image/jpeg;base64,${base64Image}` }, * }, * ] * }); * * const imageDescriptionAiMsg = await llm.invoke([message]); * console.log(imageDescriptionAiMsg.content); * ``` * * ```txt * The weather in the image appears to be clear and sunny. The sky is mostly blue with a few scattered white clouds, indicating fair weather. The bright sunlight is casting shadows on the green, grassy hill, suggesting it is a pleasant day with good visibility. There are no signs of rain or stormy conditions. * ``` * </details> * * <br /> * * <details> * <summary><strong>Usage Metadata</strong></summary> * * ```typescript * const aiMsgForMetadata = await llm.invoke(input); * console.log(aiMsgForMetadata.usage_metadata); * ``` * * ```txt * { input_tokens: 28, output_tokens: 5, total_tokens: 33 } * ``` * </details> * * <br /> * * <details> * <summary><strong>Logprobs</strong></summary> * * ```typescript * const logprobsLlm = new ChatOpenAI({ logprobs: true }); * const aiMsgForLogprobs = await logprobsLlm.invoke(input); * console.log(aiMsgForLogprobs.response_metadata.logprobs); * ``` * * ```txt * { * content: [ * { * token: 'J', * logprob: -0.000050616763, * bytes: [Array], * top_logprobs: [] * }, * { * token: "'", * logprob: -0.01868736, * bytes: [Array], * top_logprobs: [] * }, * { * token: 'ad', * logprob: -0.0000030545007, * bytes: [Array], * top_logprobs: [] * }, * { token: 'ore', logprob: 0, bytes: [Array], top_logprobs: [] }, * { * token: ' la', * logprob: -0.515404, * bytes: [Array], * top_logprobs: [] * }, * { * token: ' programm', * logprob: -0.0000118755715, * bytes: [Array], * top_logprobs: [] * }, * { token: 'ation', logprob: 0, bytes: [Array], top_logprobs: [] }, * { * token: '.', * logprob: -0.0000037697225, * bytes: [Array], * top_logprobs: [] * } * ], * refusal: null * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Response Metadata</strong></summary> * * ```typescript * const aiMsgForResponseMetadata = await llm.invoke(input); * console.log(aiMsgForResponseMetadata.response_metadata); * ``` * * ```txt * { * tokenUsage: { completionTokens: 5, promptTokens: 28, totalTokens: 33 }, * finish_reason: 'stop', * system_fingerprint: 'fp_3aa7262c27' * } * ``` * </details> * * <br /> * * <details> * <summary><strong>JSON Schema Structured Output</strong></summary> * * ```typescript * const llmForJsonSchema = new ChatOpenAI({ * model: "gpt-4o-2024-08-06", * }).withStructuredOutput( * z.object({ * command: z.string().describe("The command to execute"), * expectedOutput: z.string().describe("The expected output of the command"), * options: z * .array(z.string()) * .describe("The options you can pass to the command"), * }), * { * method: "jsonSchema", * strict: true, // Optional when using the `jsonSchema` method * } * ); * * const jsonSchemaRes = await llmForJsonSchema.invoke( * "What is the command to list files in a directory?" * ); * console.log(jsonSchemaRes); * ``` * * ```txt * { * command: 'ls', * expectedOutput: 'A list of files and subdirectories within the specified directory.', * options: [ * '-a: include directory entries whose names begin with a dot (.).', * '-l: use a long listing format.', * '-h: with -l, print sizes in human readable format (e.g., 1K, 234M, 2G).', * '-t: sort by time, newest first.', * '-r: reverse order while sorting.', * '-S: sort by file size, largest first.', * '-R: list subdirectories recursively.' * ] * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Audio Outputs</strong></summary> * * ```typescript * import { ChatOpenAI } from "@langchain/openai"; * * const modelWithAudioOutput = new ChatOpenAI({ * model: "gpt-4o-audio-preview", * // You may also pass these fields to `.bind` as a call argument. * modalities: ["text", "audio"], // Specifies that the model should output audio. * audio: { * voice: "alloy", * format: "wav", * }, * }); * * const audioOutputResult = await modelWithAudioOutput.invoke("Tell me a joke about cats."); * const castMessageContent = audioOutputResult.content[0] as Record<string, any>; * * console.log({ * ...castMessageContent, * data: castMessageContent.data.slice(0, 100) // Sliced for brevity * }) * ``` * * ```txt * { * id: 'audio_67117718c6008190a3afad3e3054b9b6', * data: 'UklGRqYwBgBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGFg', * expires_at: 1729201448, * transcript: 'Sure! Why did the cat sit on the computer? Because it wanted to keep an eye on the mouse!' * } * ``` * </details> * * <br /> * * <details> * <summary><strong>Audio Outputs</strong></summary> * * ```typescript * import { ChatOpenAI } from "@langchain/openai"; * * const modelWithAudioOutput = new ChatOpenAI({ * model: "gpt-4o-audio-preview", * // You may also pass these fields to `.bind` as a call argument. * modalities: ["text", "audio"], // Specifies that the model should output audio. * audio: { * voice: "alloy", * format: "wav", * }, * }); * * const audioOutputResult = await modelWithAudioOutput.invoke("Tell me a joke about cats."); * const castAudioContent = audioOutputResult.additional_kwargs.audio as Record<string, any>; * * console.log({ * ...castAudioContent, * data: castAudioContent.data.slice(0, 100) // Sliced for brevity * }) * ``` * * ```txt * { * id: 'audio_67117718c6008190a3afad3e3054b9b6', * data: 'UklGRqYwBgBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGFg', * expires_at: 1729201448, * transcript: 'Sure! Why did the cat sit on the computer? Because it wanted to keep an eye on the mouse!' * } * ``` * </details> * * <br /> */ class ChatOpenAI extends chat_models_1.BaseChatModel { static lc_name() { return "ChatOpenAI"; } get callKeys() { return [ ...super.callKeys, "options", "function_call", "functions", "tools", "tool_choice", "promptIndex", "response_format", "seed", "reasoning_effort", ]; } get lc_secrets() { return { openAIApiKey: "OPENAI_API_KEY", apiKey: "OPENAI_API_KEY", organization: "OPENAI_ORGANIZATION", }; } get lc_aliases() { return { modelName: "model", openAIApiKey: "openai_api_key", apiKey: "openai_api_key", }; } get lc_serializable_keys() { return [ "configuration", "logprobs", "topLogprobs", "prefixMessages", "supportsStrictToolCalling", "modalities", "audio", "reasoningEffort", "temperature", "maxTokens", "topP", "frequencyPenalty", "presencePenalty", "n", "logitBias", "user", "streaming", "streamUsage", "modelName", "model", "modelKwargs", "stop", "stopSequences", "timeout", "openAIApiKey", "apiKey", "cache", "maxConcurrency", "maxRetries", "verbose", "callbacks", "tags", "metadata", "disableStreaming", ]; } constructor(fields) { super(fields ?? {}); Object.defineProperty(this, "lc_serializable", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "temperature", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "topP", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "frequencyPenalty", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "presencePenalty", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "n", { enumerable: true, configurable: true, writable: true, value: 1 }); Object.defineProperty(this, "logitBias", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** @deprecated Use "model" instead */ Object.defineProperty(this, "modelName", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "model", { enumerable: true, configurable: true, writable: true, value: "gpt-3.5-turbo" }); Object.defineProperty(this, "modelKwargs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "stop", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "stopSequences", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "user", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "timeout", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "streaming", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "streamUsage", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "maxTokens", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "logprobs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "topLogprobs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "openAIApiKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "apiKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "organization", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "__includeRawResponse", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "client", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "clientConfig", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Whether the model supports the `strict` argument when passing in tools. * If `undefined` the `strict` argument will not be passed to OpenAI. */ Object.defineProperty(this, "supportsStrictToolCalling", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "audio", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "modalities", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "reasoningEffort", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.openAIApiKey = fields?.apiKey ?? fields?.openAIApiKey ?? fields?.configuration?.apiKey ?? (0, env_1.getEnvironmentVariable)("OPENAI_API_KEY"); this.apiKey = this.openAIApiKey; this.organization = fields?.configuration?.organization ?? (0, env_1.getEnvironmentVariable)("OPENAI_ORGANIZATION"); this.model = fields?.model ?? fields?.modelName ?? this.model; this.modelName = this.model; this.modelKwargs = fields?.modelKwargs ?? {}; this.timeout = fields?.timeout; this.temperature = fields?.temperature ?? this.temperature; this.topP = fields?.topP ?? this.topP; this.frequencyPenalty = fields?.frequencyPenalty ?? this.frequencyPenalty; this.presencePenalty = fields?.presencePenalty ?? this.presencePenalty; this.logprobs = fields?.logprobs; this.topLogprobs = fields?.topLogprobs; this.n = fields?.n ?? this.n; this.logitBias = fields?.logitBias; this.stop = fields?.stopSequences ?? fields?.stop; this.stopSequences = this?.stop; this.user = fields?.user; this.__includeRawResponse = fields?.__includeRawResponse; this.audio = fields?.audio; this.modalities = fields?.modalities; this.reasoningEffort = fields?.reasoningEffort; this.maxTokens = fields?.maxCompletionTokens ?? fields?.maxTokens; if (this.model === "o1") { this.disableStreaming = true; } this.streaming = fields?.streaming ?? false; this.streamUsage = fields?.streamUsage ?? this.streamUsage; this.clientConfig = { apiKey: this.apiKey, organization: this.organization, dangerouslyAllowBrowser: true, ...fields?.configuration, }; // If `supportsStrictToolCalling` is explicitly set, use that value. // Else leave undefined so it's not passed to OpenAI. if (fields?.supportsStrictToolCalling !== undefined) { this.supportsStrictToolCalling = fields.supportsStrictToolCalling; } } getLsParams(options) { const params = this.invocationParams(options); return { ls_provider: "openai", ls_model_name: this.model, ls_model_type: "chat", ls_temperature: params.temperature ?? undefined, ls_max_tokens: params.max_tokens ?? undefined, ls_stop: options.stop, }; } bindTools(tools, kwargs) { let strict; if (kwargs?.strict !== undefined) { strict = kwargs.strict; } else if (this.supportsStrictToolCalling !== undefined) { strict = this.supportsStrictToolCalling; } return this.bind({ tools: tools.map((tool) => _convertChatOpenAIToolTypeToOpenAITool(tool, { strict })), ...kwargs, }); } createResponseFormat(resFormat) { if (resFormat && resFormat.type === "json_schema" && resFormat.json_schema.schema && isZodSchema(resFormat.json_schema.schema)) { return (0, zod_1.zodResponseFormat)(resFormat.json_schema.schema, resFormat.json_schema.name, { description: resFormat.json_schema.description, }); } return resFormat; } /** * Get the parameters used to invoke the model */ invocationParams(options, extra) { let strict; if (options?.strict !== undefined) { strict = options.strict; } else if (this.supportsStrictToolCalling !== undefined) { strict = this.supportsStrictToolCalling; } let streamOptionsConfig = {}; if (options?.stream_options !== undefined) { streamOptionsConfig = { stream_options: options.stream_options }; } else if (this.streamUsage && (this.streaming || extra?.streaming)) { streamOptionsConfig = { stream_options: { include_usage: true } }; } const params = { model: this.model, temperature: this.temperature, top_p: this.topP, frequency_penalty: this.frequencyPenalty, presence_penalty: this.presencePenalty, logprobs: this.logprobs, top_logprobs: this.topLogprobs, n: this.n, logit_bias: this.logitBias, stop: options?.stop ?? this.stopSequences, user: this.user, // if include_usage is set or streamUsage then stream must be set to true. stream: this.streaming, functions: options?.functions, function_call: options?.function_call, tools: options?.tools?.length ? options.tools.map((tool) => _convertChatOpenAIToolTypeToOpenAITool(tool, { strict })) : undefined, tool_choice: (0, openai_js_1.formatToOpenAIToolChoice)(options?.tool_choice), response_format: this.createResponseFormat(options?.response_format), seed: options?.seed, ...streamOptionsConfig, parallel_tool_calls: options?.parallel_tool_calls, ...(this.audio || options?.audio ? { audio: this.audio || options?.audio } : {}), ...(this.modalities || options?.modalities ? { modalities: this.modalities || options?.modalities } : {}), ...this.modelKwargs, }; if (options?.prediction !== undefined) { params.prediction = options.prediction; } const reasoningEffort = options?.reasoning_effort ?? this.reasoningEffort; if (reasoningEffort !== undefined) { params.reasoning_effort = reasoningEffort; } if (isReasoningModel(params.model)) { params.max_completion_tokens = this.maxTokens === -1 ? undefined : this.maxTokens; } else { params.max_tokens = this.maxTokens === -1 ? undefined : this.maxTokens; } return params; } _convertOpenAIChatCompletionMessageToBaseMessage(message, rawResponse) { const rawToolCalls = message.tool_calls; switch (message.role) { case "assistant": { const toolCalls = []; const invalidToolCalls = []; for (const rawToolCall of rawToolCalls ?? []) { try { toolCalls.push((0, openai_tools_1.parseToolCall)(rawToolCall, { returnId: true })); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { invalidToolCalls.push((0, openai_tools_1.makeInvalidToolCall)(rawToolCall, e.message)); } } const additional_kwargs = { function_call: message.function_call, tool_calls: rawToolCalls, }; if (this.__includeRawResponse !== undefined) { additional_kwargs.__raw_response = rawResponse; } const response_metadata = { model_name: rawResponse.model, ...(rawResponse.system_fingerprint ? { usage: { ...rawResponse.usage }, system_fingerprint: rawResponse.system_fingerprint, } : {}), }; if (message.audio) { additional_kwargs.audio = message.audio; } return new messages_1.AIMessage({ content: message.content || "", tool_calls: toolCalls, invalid_tool_calls: invalidToolCalls, additional_kwargs, response_metadata, id: rawResponse.id, }); } default: return new messages_1.ChatMessage(message.content || "", message.role ?? "unknown"); } } _convertOpenAIDeltaToBaseMessageChunk( // eslint-disable-next-line @typescript-eslint/no-explicit-any delta, rawResponse, defaultRole) { const role = delta.role ?? defaultRole; const content = delta.content ?? ""; let additional_kwargs; if (delta.function_call) { additional_kwargs = { function_call: delta.function_call, }; } else if (delta.tool_calls) { additional_kwargs = { tool_calls: delta.tool_calls, }; } else { additional_kwargs = {}; } if (this.__includeRawResponse) { additional_kwargs.__raw_response = rawResponse; } if (delta.audio) { additional_kwargs.audio = { ...delta.audio, index: rawResponse.choices[0].index, }; } const response_metadata = { usage: { ...rawResponse.usage } }; if (role === "user") { return new messages_1.HumanMessageChunk({ content, response_metadata }); } else if (role === "assistant") { const toolCallChunks = []; if (Array.isArray(delta.tool_calls)) { for (const rawToolCall of delta.tool_calls) { toolCallChunks.push({ name: rawToolCall.function?.name, args: rawToolCall.function?.arguments, id: rawToolCall.id, index: rawToolCall.index, type: "tool_call_chunk", }); } } return new messages_1.AIMessageChunk({ content, tool_call_chunks: toolCallChunks, additional_kwargs, id: rawResponse.id, response_metadata, }); } else if (role === "system") { return new messages_1.SystemMessageChunk({ content, response_metadata }); } else if (role === "developer") { return new messages_1.SystemMessageChunk({ content, response_metadata, additional_kwargs: { __openai_role__: "developer", }, }); } else if (role === "function") { return new messages_1.FunctionMessageChunk({ content, additional_kwargs, name: delta.name, response_metadata, }); } else if (role === "tool") { return new messages_1.ToolMessageChunk({ content, additional_kwargs, tool_call_id: delta.tool_call_id, response_metadata, }); } else { return new messages_1.ChatMessageChunk({ content, role, response_metadata }); } } /** @ignore */ _identifyingParams() { return { model_name: this.model, ...this.invocationParams(), ...this.clientConfig, }; } async *_streamResponseChunks(messages, options, runManager) { const messagesMapped = _convertMessagesToOpenAIParams(messages, this.model); const params = { ...this.invocationParams(options, { streaming: true, }), messages: messagesMapped, stream: true, }; let defaultRole; const streamIterable = await this.completionWithRetry(params, options); let usage; for await (const data of streamIterable) { const choice = data?.choices?.[0]; if (data.usage) { usage = data.usage; } if (!choice) { continue; } const { delta } = choice; if (!delta) { continue; } const chunk = this._convertOpenAIDeltaToBaseMessageChunk(delta, data, defaultRole); defaultRole = delta.role ?? defaultRole; const newTokenIndices = { prompt: options.promptIndex ?? 0, completion: choice.index ?? 0, }; if (typeof chunk.content !== "string") { console.log("[WARNING]: Received non-string content from OpenAI. This is currently not supported."); continue; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const generationInfo = { ...newTokenIndices }; if (choice.finish_reason != null) { generationInfo.finish_reason = choice.finish_reason; // Only include system fingerprint in the last chunk for now // to avoid concatenation issues generationInfo.system_fingerprint = data.system_fingerprint; generationInfo.model_name = data.model; } if (this.logprobs) { generationInfo.logprobs = choice.logprobs; } const generationChunk = new outputs_1.ChatGenerationChunk({ message: chunk, text: chunk.content, generationInfo, }); yield generationChunk; await runManager?.handleLLMNewToken(generationChunk.text ?? "", newTokenIndices, undefined, undefined, undefined, { chunk: generationChunk }); } if (usage) { const inputTokenDetails = { ...(usage.prompt_tokens_details?.audio_tokens !== null && { audio: usage.prompt_tokens_details?.audio_tokens, }), ...(usage.prompt_tokens_details?.cached_tokens !== null && { cache_read: usage.prompt_tokens_details?.cached_tokens, }), }; const outputTokenDetails = { ...(usage.completion_tokens_details?.audio_tokens !== null && { audio: usage.completion_tokens_details?.audio_tokens, }), ...(usage.completion_tokens_details?.reasoning_tokens !== null && { reasoning: usage.completion_tokens_details?.reasoning_tokens, }), }; const generationChunk = new outputs_1.ChatGenerationChunk({ message: new messages_1.AIMessageChunk({ content: "", response_metadata: { usage: { ...usage }, }, usage_metadata: { input_tokens: usage.prompt_tokens, output_tokens: usage.completion_tokens, total_tokens: usage.total_tokens, ...(Object.keys(inputTokenDetails).length > 0 && { input_token_details: inputTokenDetails, }), ...(Object.keys(outputTokenDetails).length > 0 && { output_token_details: outputTokenDetails, }), }, }), text: "", }); yield generationChunk; } if (options.signal?.aborted) { throw new Error("AbortError"); } } /** * Get the identifying parameters for the model * */ identifyingParams() { return this._identifyingParams(); } /** @ignore */ async _generate(messages, options, runManager) { const usageMetadata = {}; const params = this.invocationParams(options); const messagesMapped = _convertMessagesToOpenAIParams(messages, this.model); if (params.stream) { const stream = this._streamResponseChunks(messages, options, runManager); const finalChunks = {}; for await (const chunk of stream) { chunk.message.response_metadata = { ...chunk.generationInfo, ...chunk.message.response_metadata, }; const index = chunk.generationInfo?.completion ?? 0; if (finalChunks[index] === undefined) { finalChunks[index] = chunk; } else { finalChunks[index] = finalChunks[index].concat(chunk); } } const generations = Object.entries(finalChunks) .sort(([aKey], [bKey]) => parseInt(aKey, 10) - parseInt(bKey, 10)) .map(([_, value]) => value); const { functions, function_call } = this.invocationParams(options); // OpenAI does not support token usage report under stream mode, // fallback to estimation. const promptTokenUsage = await this.getEstimatedTokenCountFromPrompt(messages, functions, function_call); const completionTokenUsage = await this.getNumTokensFromGenerations(generations); usageMetadata.input_tokens = promptTokenUsage; usageMetadata.output_tokens = completionTokenUsage; usageMetadata.total_tokens = promptTokenUsage + completionTokenUsage; return { generations, llmOutput: { estimatedTokenUsage: { promptTokens: usageMetadata.input_tokens, completionTokens: usageMetadata.output_tokens, totalTokens: usageMetadata.total_tokens, }, }, }; } else { let data; if (options.response_format && options.response_format.type === "json_schema") { data = await this.betaParsedCompletionWithRetry({ ...params, stream: false, messages: messagesMapped, }, { signal: options?.signal, ...options?.options, }); } else { data = await this.completionWithRetry({ ...params, stream: false, messages: messagesMapped, }, { signal: options?.signal, ...options?.options, }); } const { completion_tokens: completionTokens, prompt_tokens: promptTokens, total_tokens: totalTokens, prompt_tokens_details: promptTokensDetails, completion_tokens_details: completionTokensDetails, } = data?.usage ?? {}; if (completionTokens) { usageMetadata.output_tokens = (usageMetadata.output_tokens ?? 0) + completionTokens; } if (promptTokens) { usageMetadata.input_tokens = (usageMetadata.input_tokens ?? 0) + promptTokens; } if (totalTokens) { usageMetadata.total_tokens = (usageMetadata.total_tokens ?? 0) + totalTokens; } if (promptTokensDetails?.audio_tokens !== null || promptTokensDetails?.cached_tokens !== null) { usageMetadata.input_token_details = { ...(promptTokensDetails?.audio_tokens !== null && { audio: promptTokensDetails?.audio_tokens, }), ...(promptTokensDetails?.cached_tokens !== null && { cache_read: promptTokensDetails?.cached_tokens, }), }; } if (completionTokensDetails?.audio_tokens !== null || completionTokensDetails?.reasoning_tokens !== null) { usageMetadata.output_token_details = { ...(completionTokensDetails?.audio_tokens !== null && { audio: completionTokensDetails?.audio_tokens, }), ...(completionTokensDetails?.reasoning_tokens !== null && { reasoning: completionTokensDetails?.reasoning_tokens, }), }; } const generations = []; for (const part of data?.choices ?? []) { const text = part.message?.content ?? ""; const generation = { text, message: this._convertOpenAIChatCompletionMessageToBaseMessage(part.message ?? { role: "assistant" }, data), }; generation.generationInfo = { ...(part.finish_reason ? { finish_reason: part.finish_reason } : {}), ...(part.logprobs ? { logprobs: part.logprobs } : {}), }; if ((0, messages_1.isAIMessage)(generation.message)) { generation.message.usage_metadata = usageMetadata; } // Fields are not serialized unless passed to the constructor // Doing this ensures all fields on the message are serialized generation.message = new messages_1.AIMessage(Object.fromEntries(Object.entries(generation.message).filter(([key]) => !key.startsWith("lc_")))); generations.push(generation); } return { generations, llmOutput: { tokenUsage: { promptTokens: usageMetadata.input_tokens, completionTokens: usageMetadata.output_tokens, totalTokens: usageMetadata.total_tokens, }, }, }; } } /** * Estimate the number of tokens a prompt will use. * Modified from: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts */ async getEstimatedTokenCountFromPrompt(messages, functions, function_call) { // It appears that if functions are present, the first system message is padded with a trailing newline. This // was inferred by trying lots of combinations of messages and functions and seeing what the token counts were. let tokens = (await this.getNumTokensFromMessages(messages)).totalCount; // If there are functions, add the function definitions as they count towards token usage if (functions && function_call !== "auto") { const promptDefinitions = (0, openai_format_fndef_js_1.formatFunctionDefinitions)(functions); tokens += await this.getNumTokens(promptDefinitions); tokens += 9; // Add nine per completion } // If there's a system message _and_ functions are present, subtract four tokens. I assume this is because // functions typically add a system message, but reuse the first one if it's already there. This offsets // the extra 9 tokens added by the function definitions. if (functions && messages.find((m) => m._getType() === "system")) { tokens -= 4; } // If function_call is 'none', add one token. // If it's a FunctionCall object, add 4 + the number of tokens in the function name. // If it's undefined or 'auto', don't add anything. if (function_call === "none") { tokens += 1; } else if (typeof function_call === "object") { tokens += (await this.getNumTokens(function_call.name)) + 4; } return tokens; } /** * Estimate the number of tokens an array of generations have used. */ async getNumTokensFromGenerations(generations) { const generationUsages = await Promise.all(generations.map(async (generation) => { if (generation.message.additional_kwargs?.function_call) { return (await this.getNumTokensFromMessages([generation.message])) .countPerMessage[0]; } else {