diff --git a/langchain-core/README.md b/langchain-core/README.md index e2530a61fe5e..c69646b77823 100644 --- a/langchain-core/README.md +++ b/langchain-core/README.md @@ -1,5 +1,9 @@ # 🦜️🔗 @langchain/core +:::note +This package is experimental at the moment and may change. +::: + `@langchain/core` contains the core abstractions and schemas of LangChain.js, including base classes for language models, chat models, vectorstores, retrievers, and runnables. diff --git a/langchain/package.json b/langchain/package.json index 628381783036..8cde8ae0ecfe 100644 --- a/langchain/package.json +++ b/langchain/package.json @@ -865,9 +865,7 @@ "@google-cloud/storage": "^6.10.1", "@huggingface/inference": "^2.6.4", "@jest/globals": "^29.5.0", - "@langchain/anthropic": "workspace:*", "@langchain/core": "workspace:*", - "@langchain/openai": "workspace:*", "@mozilla/readability": "^0.4.4", "@notionhq/client": "^2.2.10", "@opensearch-project/opensearch": "^2.2.0", @@ -1380,9 +1378,8 @@ } }, "dependencies": { - "@langchain/anthropic": "^0.0.3", + "@anthropic-ai/sdk": "^0.9.1", "@langchain/core": "^0.0.1", - "@langchain/openai": "^0.0.1", "binary-extensions": "^2.2.0", "expr-eval": "^2.0.2", "flat": "^5.0.2", @@ -1392,6 +1389,7 @@ "langchainhub": "~0.0.6", "langsmith": "~0.0.48", "ml-distance": "^4.0.0", + "openai": "^4.19.0", "openapi-types": "^12.1.3", "p-retry": "4", "uuid": "^9.0.0", diff --git a/langchain/src/agents/openai/output_parser.ts b/langchain/src/agents/openai/output_parser.ts index fc14b6a4f160..960f6db84e47 100644 --- a/langchain/src/agents/openai/output_parser.ts +++ b/langchain/src/agents/openai/output_parser.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import { AgentAction, AgentFinish, diff --git a/langchain/src/chains/openai_functions/openapi.ts b/langchain/src/chains/openai_functions/openapi.ts index 5306eef626db..06eb92ad4995 100644 --- a/langchain/src/chains/openai_functions/openapi.ts +++ b/langchain/src/chains/openai_functions/openapi.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import { JsonSchema7ObjectType } from "zod-to-json-schema/src/parsers/object.js"; import { JsonSchema7ArrayType } from "zod-to-json-schema/src/parsers/array.js"; import { JsonSchema7Type } from "zod-to-json-schema/src/parseDef.js"; diff --git a/langchain/src/chains/openai_moderation.ts b/langchain/src/chains/openai_moderation.ts index 2474baaab0ba..862552099405 100644 --- a/langchain/src/chains/openai_moderation.ts +++ b/langchain/src/chains/openai_moderation.ts @@ -1,4 +1,4 @@ -import { type ClientOptions, OpenAIClient } from "@langchain/openai"; +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; import { BaseChain, ChainInputs } from "./base.js"; import { ChainValues } from "../schema/index.js"; import { AsyncCaller, AsyncCallerParams } from "../util/async_caller.js"; diff --git a/langchain/src/chat_models/anthropic.ts b/langchain/src/chat_models/anthropic.ts index e2bac02693b3..b386dea10fd1 100644 --- a/langchain/src/chat_models/anthropic.ts +++ b/langchain/src/chat_models/anthropic.ts @@ -1 +1,449 @@ -export * from "@langchain/anthropic"; +import { + Anthropic, + AI_PROMPT, + HUMAN_PROMPT, + ClientOptions, +} from "@anthropic-ai/sdk"; +import type { CompletionCreateParams } from "@anthropic-ai/sdk/resources/completions"; +import type { Stream } from "@anthropic-ai/sdk/streaming"; + +import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; +import { + AIMessage, + AIMessageChunk, + type BaseMessage, + ChatMessage, +} from "@langchain/core/messages"; +import { + type ChatGeneration, + ChatGenerationChunk, + type ChatResult, +} from "@langchain/core/outputs"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { + BaseChatModel, + type BaseChatModelParams, +} from "@langchain/core/language_models/chat_models"; +import { type BaseLanguageModelCallOptions } from "@langchain/core/language_models/base"; + +export { AI_PROMPT, HUMAN_PROMPT }; + +/** + * Extracts the custom role of a generic chat message. + * @param message The chat message from which to extract the custom role. + * @returns The custom role of the chat message. + */ +function extractGenericMessageCustomRole(message: ChatMessage) { + if ( + message.role !== AI_PROMPT && + message.role !== HUMAN_PROMPT && + message.role !== "" + ) { + console.warn(`Unknown message role: ${message.role}`); + } + + return message.role; +} + +/** + * Gets the Anthropic prompt from a base message. + * @param message The base message from which to get the Anthropic prompt. + * @returns The Anthropic prompt from the base message. + */ +function getAnthropicPromptFromMessage(message: BaseMessage): string { + const type = message._getType(); + switch (type) { + case "ai": + return AI_PROMPT; + case "human": + return HUMAN_PROMPT; + case "system": + return ""; + case "generic": { + if (!ChatMessage.isInstance(message)) + throw new Error("Invalid generic chat message"); + return extractGenericMessageCustomRole(message); + } + default: + throw new Error(`Unknown message type: ${type}`); + } +} + +export const DEFAULT_STOP_SEQUENCES = [HUMAN_PROMPT]; + +/** + * Input to AnthropicChat class. + */ +export interface AnthropicInput { + /** Amount of randomness injected into the response. Ranges + * from 0 to 1. Use temp closer to 0 for analytical / + * multiple choice, and temp closer to 1 for creative + * and generative tasks. + */ + temperature?: number; + + /** Only sample from the top K options for each subsequent + * token. Used to remove "long tail" low probability + * responses. Defaults to -1, which disables it. + */ + topK?: number; + + /** Does nucleus sampling, in which we compute the + * cumulative distribution over all the options for each + * subsequent token in decreasing probability order and + * cut it off once it reaches a particular probability + * specified by top_p. Defaults to -1, which disables it. + * Note that you should either alter temperature or top_p, + * but not both. + */ + topP?: number; + + /** A maximum number of tokens to generate before stopping. */ + maxTokensToSample: number; + + /** A list of strings upon which to stop generating. + * You probably want `["\n\nHuman:"]`, as that's the cue for + * the next turn in the dialog agent. + */ + stopSequences?: string[]; + + /** Whether to stream the results or not */ + streaming?: boolean; + + /** Anthropic API key */ + anthropicApiKey?: string; + + /** Anthropic API URL */ + anthropicApiUrl?: string; + + /** Model name to use */ + modelName: string; + + /** Overridable Anthropic ClientOptions */ + clientOptions: ClientOptions; + + /** Holds any additional parameters that are valid to pass to {@link + * https://console.anthropic.com/docs/api/reference | + * `anthropic.complete`} that are not explicitly specified on this class. + */ + invocationKwargs?: Kwargs; +} + +/** + * A type representing additional parameters that can be passed to the + * Anthropic API. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Kwargs = Record; + +/** + * Wrapper around Anthropic large language models. + * + * To use you should have the `@anthropic-ai/sdk` package installed, with the + * `ANTHROPIC_API_KEY` environment variable set. + * + * @remarks + * Any parameters that are valid to be passed to {@link + * https://console.anthropic.com/docs/api/reference | + * `anthropic.complete`} can be passed through {@link invocationKwargs}, + * even if not explicitly available on this class. + * @example + * ```typescript + * const model = new ChatAnthropic({ + * temperature: 0.9, + * anthropicApiKey: 'YOUR-API-KEY', + * }); + * const res = await model.invoke({ input: 'Hello!' }); + * console.log(res); + * ``` + */ +export class ChatAnthropic< + CallOptions extends BaseLanguageModelCallOptions = BaseLanguageModelCallOptions + > + extends BaseChatModel + implements AnthropicInput +{ + static lc_name() { + return "ChatAnthropic"; + } + + get lc_secrets(): { [key: string]: string } | undefined { + return { + anthropicApiKey: "ANTHROPIC_API_KEY", + }; + } + + get lc_aliases(): Record { + return { + modelName: "model", + }; + } + + lc_serializable = true; + + anthropicApiKey?: string; + + apiUrl?: string; + + temperature = 1; + + topK = -1; + + topP = -1; + + maxTokensToSample = 2048; + + modelName = "claude-2"; + + invocationKwargs?: Kwargs; + + stopSequences?: string[]; + + streaming = false; + + clientOptions: ClientOptions; + + // Used for non-streaming requests + protected batchClient: Anthropic; + + // Used for streaming requests + protected streamingClient: Anthropic; + + constructor(fields?: Partial & BaseChatModelParams) { + super(fields ?? {}); + + this.anthropicApiKey = + fields?.anthropicApiKey ?? getEnvironmentVariable("ANTHROPIC_API_KEY"); + if (!this.anthropicApiKey) { + throw new Error("Anthropic API key not found"); + } + + // Support overriding the default API URL (i.e., https://api.anthropic.com) + this.apiUrl = fields?.anthropicApiUrl; + + this.modelName = fields?.modelName ?? this.modelName; + this.invocationKwargs = fields?.invocationKwargs ?? {}; + + this.temperature = fields?.temperature ?? this.temperature; + this.topK = fields?.topK ?? this.topK; + this.topP = fields?.topP ?? this.topP; + this.maxTokensToSample = + fields?.maxTokensToSample ?? this.maxTokensToSample; + this.stopSequences = fields?.stopSequences ?? this.stopSequences; + + this.streaming = fields?.streaming ?? false; + this.clientOptions = fields?.clientOptions ?? {}; + } + + /** + * Get the parameters used to invoke the model + */ + invocationParams( + options?: this["ParsedCallOptions"] + ): Omit & Kwargs { + return { + model: this.modelName, + temperature: this.temperature, + top_k: this.topK, + top_p: this.topP, + stop_sequences: + options?.stop?.concat(DEFAULT_STOP_SEQUENCES) ?? + this.stopSequences ?? + DEFAULT_STOP_SEQUENCES, + max_tokens_to_sample: this.maxTokensToSample, + stream: this.streaming, + ...this.invocationKwargs, + }; + } + + /** @ignore */ + _identifyingParams() { + return { + model_name: this.modelName, + ...this.invocationParams(), + }; + } + + /** + * Get the identifying parameters for the model + */ + identifyingParams() { + return { + model_name: this.modelName, + ...this.invocationParams(), + }; + } + + async *_streamResponseChunks( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const params = this.invocationParams(options); + const stream = await this.createStreamWithRetry({ + ...params, + prompt: this.formatMessagesAsPrompt(messages), + }); + let modelSent = false; + let stopReasonSent = false; + for await (const data of stream) { + if (options.signal?.aborted) { + stream.controller.abort(); + throw new Error("AbortError: User aborted the request."); + } + const additional_kwargs: Record = {}; + if (data.model && !modelSent) { + additional_kwargs.model = data.model; + modelSent = true; + } else if (data.stop_reason && !stopReasonSent) { + additional_kwargs.stop_reason = data.stop_reason; + stopReasonSent = true; + } + const delta = data.completion ?? ""; + yield new ChatGenerationChunk({ + message: new AIMessageChunk({ + content: delta, + additional_kwargs, + }), + text: delta, + }); + await runManager?.handleLLMNewToken(delta); + if (data.stop_reason) { + break; + } + } + } + + /** + * Formats messages as a prompt for the model. + * @param messages The base messages to format as a prompt. + * @returns The formatted prompt. + */ + protected formatMessagesAsPrompt(messages: BaseMessage[]): string { + return ( + messages + .map((message) => { + const messagePrompt = getAnthropicPromptFromMessage(message); + return `${messagePrompt} ${message.content}`; + }) + .join("") + AI_PROMPT + ); + } + + /** @ignore */ + async _generate( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): Promise { + if (this.stopSequences && options.stop) { + throw new Error( + `"stopSequence" parameter found in input and default params` + ); + } + + const params = this.invocationParams(options); + let response; + if (params.stream) { + response = { + completion: "", + model: "", + stop_reason: "", + }; + const stream = await this._streamResponseChunks( + messages, + options, + runManager + ); + for await (const chunk of stream) { + response.completion += chunk.message.content; + response.model = + (chunk.message.additional_kwargs.model as string) ?? response.model; + response.stop_reason = + (chunk.message.additional_kwargs.stop_reason as string) ?? + response.stop_reason; + } + } else { + response = await this.completionWithRetry( + { + ...params, + prompt: this.formatMessagesAsPrompt(messages), + }, + { signal: options.signal } + ); + } + + const generations: ChatGeneration[] = (response.completion ?? "") + .split(AI_PROMPT) + .map((message) => ({ + text: message, + message: new AIMessage(message), + })); + + return { + generations, + }; + } + + /** + * Creates a streaming request with retry. + * @param request The parameters for creating a completion. + * @returns A streaming request. + */ + protected async createStreamWithRetry( + request: CompletionCreateParams & Kwargs + ): Promise> { + if (!this.streamingClient) { + const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; + this.streamingClient = new Anthropic({ + ...this.clientOptions, + ...options, + apiKey: this.anthropicApiKey, + maxRetries: 0, + }); + } + const makeCompletionRequest = async () => + this.streamingClient.completions.create( + { ...request, stream: true }, + { headers: request.headers } + ); + return this.caller.call(makeCompletionRequest); + } + + /** @ignore */ + protected async completionWithRetry( + request: CompletionCreateParams & Kwargs, + options: { signal?: AbortSignal } + ): Promise { + if (!this.anthropicApiKey) { + throw new Error("Missing Anthropic API key."); + } + if (!this.batchClient) { + const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; + this.batchClient = new Anthropic({ + ...this.clientOptions, + ...options, + apiKey: this.anthropicApiKey, + maxRetries: 0, + }); + } + const makeCompletionRequest = async () => + this.batchClient.completions.create( + { ...request, stream: false }, + { headers: request.headers } + ); + return this.caller.callWithOptions( + { signal: options.signal }, + makeCompletionRequest + ); + } + + _llmType() { + return "anthropic"; + } + + /** @ignore */ + _combineLLMOutput() { + return []; + } +} diff --git a/langchain/src/chat_models/fireworks.ts b/langchain/src/chat_models/fireworks.ts index cde5c7af72c0..29e12cc34ea2 100644 --- a/langchain/src/chat_models/fireworks.ts +++ b/langchain/src/chat_models/fireworks.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import type { ChatOpenAICallOptions, OpenAIChatInput } from "./openai.js"; import type { OpenAICoreRequestOptions } from "../types/openai-types.js"; import type { BaseChatModelParams } from "./base.js"; diff --git a/langchain/src/chat_models/openai.ts b/langchain/src/chat_models/openai.ts index cf822cb30a8d..c8e99c525246 100644 --- a/langchain/src/chat_models/openai.ts +++ b/langchain/src/chat_models/openai.ts @@ -1,20 +1,836 @@ -import { - ChatOpenAI, - type ChatOpenAICallOptions, - messageToOpenAIRole, -} from "@langchain/openai"; +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; import { CallbackManagerForLLMRun } from "../callbacks/manager.js"; -import { BaseMessage, ChatMessage, ChatResult } from "../schema/index.js"; +import { + AIMessage, + AIMessageChunk, + BaseMessage, + ChatGeneration, + ChatGenerationChunk, + ChatMessage, + ChatMessageChunk, + ChatResult, + FunctionMessageChunk, + HumanMessageChunk, + SystemMessageChunk, + ToolMessage, + ToolMessageChunk, +} from "../schema/index.js"; +import { StructuredTool } from "../tools/base.js"; +import { formatToOpenAITool } from "../tools/convert_to_openai.js"; +import { + AzureOpenAIInput, + OpenAICallOptions, + OpenAIChatInput, + OpenAICoreRequestOptions, + LegacyOpenAIInput, +} from "../types/openai-types.js"; +import { OpenAIEndpointConfig, getEndpoint } from "../util/azure.js"; +import { getEnvironmentVariable } from "../util/env.js"; import { promptLayerTrackRequest } from "../util/prompt-layer.js"; +import { BaseChatModel, BaseChatModelParams } from "./base.js"; +import { BaseFunctionCallOptions } from "../base_language/index.js"; +import { NewTokenIndices } from "../callbacks/base.js"; +import { wrapOpenAIClientError } from "../util/openai.js"; +import { + FunctionDef, + formatFunctionDefinitions, +} from "../util/openai-format-fndef.js"; + +export type { AzureOpenAIInput, OpenAICallOptions, OpenAIChatInput }; + +interface TokenUsage { + completionTokens?: number; + promptTokens?: number; + totalTokens?: number; +} + +interface OpenAILLMOutput { + tokenUsage: TokenUsage; +} + +// TODO import from SDK when available +type OpenAIRoleEnum = "system" | "assistant" | "user" | "function" | "tool"; + +type OpenAICompletionParam = + OpenAIClient.Chat.Completions.ChatCompletionMessageParam; +type OpenAIFnDef = OpenAIClient.Chat.ChatCompletionCreateParams.Function; +type OpenAIFnCallOption = OpenAIClient.Chat.ChatCompletionFunctionCallOption; + +function extractGenericMessageCustomRole(message: ChatMessage) { + if ( + message.role !== "system" && + message.role !== "assistant" && + message.role !== "user" && + message.role !== "function" && + message.role !== "tool" + ) { + console.warn(`Unknown message role: ${message.role}`); + } + + return message.role as OpenAIRoleEnum; +} + +function messageToOpenAIRole(message: BaseMessage): OpenAIRoleEnum { + 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 (!ChatMessage.isInstance(message)) + throw new Error("Invalid generic chat message"); + return extractGenericMessageCustomRole(message); + } + default: + throw new Error(`Unknown message type: ${type}`); + } +} + +function openAIResponseToChatMessage( + message: OpenAIClient.Chat.Completions.ChatCompletionMessage +): BaseMessage { + switch (message.role) { + case "assistant": + return new AIMessage(message.content || "", { + function_call: message.function_call, + tool_calls: message.tool_calls, + }); + default: + return new ChatMessage(message.content || "", message.role ?? "unknown"); + } +} + +function _convertDeltaToMessageChunk( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delta: Record, + defaultRole?: OpenAIRoleEnum +) { + 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 (role === "user") { + return new HumanMessageChunk({ content }); + } else if (role === "assistant") { + return new AIMessageChunk({ content, additional_kwargs }); + } else if (role === "system") { + return new SystemMessageChunk({ content }); + } else if (role === "function") { + return new FunctionMessageChunk({ + content, + additional_kwargs, + name: delta.name, + }); + } else if (role === "tool") { + return new ToolMessageChunk({ + content, + additional_kwargs, + tool_call_id: delta.tool_call_id, + }); + } else { + return new ChatMessageChunk({ content, role }); + } +} + +function convertMessagesToOpenAIParams(messages: BaseMessage[]) { + // TODO: Function messages do not support array content, fix cast + return messages.map( + (message) => + ({ + role: messageToOpenAIRole(message), + content: message.content, + name: message.name, + function_call: message.additional_kwargs.function_call, + tool_calls: message.additional_kwargs.tool_calls, + tool_call_id: (message as ToolMessage).tool_call_id, + } as OpenAICompletionParam) + ); +} + +export interface ChatOpenAICallOptions + extends OpenAICallOptions, + BaseFunctionCallOptions { + tools?: StructuredTool[] | OpenAIClient.ChatCompletionTool[]; + tool_choice?: OpenAIClient.ChatCompletionToolChoiceOption; + promptIndex?: number; + response_format?: { type: "json_object" }; + seed?: number; +} + +/** + * Wrapper around OpenAI large language models that use the Chat endpoint. + * + * To use you should have the `openai` package installed, with the + * `OPENAI_API_KEY` environment variable set. + * + * To use with Azure you should have the `openai` package installed, with the + * `AZURE_OPENAI_API_KEY`, + * `AZURE_OPENAI_API_INSTANCE_NAME`, + * `AZURE_OPENAI_API_DEPLOYMENT_NAME` + * and `AZURE_OPENAI_API_VERSION` environment variable set. + * `AZURE_OPENAI_BASE_PATH` is optional and will override `AZURE_OPENAI_API_INSTANCE_NAME` if you need to use a custom endpoint. + * + * @remarks + * Any parameters that are valid to be passed to {@link + * https://platform.openai.com/docs/api-reference/chat/create | + * `openai.createChatCompletion`} can be passed through {@link modelKwargs}, even + * if not explicitly available on this class. + * @example + * ```typescript + * // Create a new instance of ChatOpenAI with specific temperature and model name settings + * const model = new ChatOpenAI({ + * temperature: 0.9, + * modelName: "ft:gpt-3.5-turbo-0613:{ORG_NAME}::{MODEL_ID}", + * }); + * + * // Invoke the model with a message and await the response + * const message = await model.invoke("Hi there!"); + * + * // Log the response to the console + * console.log(message); + * + * ``` + */ +export class ChatOpenAI< + CallOptions extends ChatOpenAICallOptions = ChatOpenAICallOptions + > + extends BaseChatModel + implements OpenAIChatInput, AzureOpenAIInput +{ + static lc_name() { + return "ChatOpenAI"; + } + + get callKeys() { + return [ + ...super.callKeys, + "options", + "function_call", + "functions", + "tools", + "tool_choice", + "promptIndex", + "response_format", + "seed", + ]; + } + + lc_serializable = true; + + get lc_secrets(): { [key: string]: string } | undefined { + return { + openAIApiKey: "OPENAI_API_KEY", + azureOpenAIApiKey: "AZURE_OPENAI_API_KEY", + organization: "OPENAI_ORGANIZATION", + }; + } + + get lc_aliases(): Record { + return { + modelName: "model", + openAIApiKey: "openai_api_key", + azureOpenAIApiVersion: "azure_openai_api_version", + azureOpenAIApiKey: "azure_openai_api_key", + azureOpenAIApiInstanceName: "azure_openai_api_instance_name", + azureOpenAIApiDeploymentName: "azure_openai_api_deployment_name", + }; + } + + temperature = 1; + + topP = 1; + + frequencyPenalty = 0; + + presencePenalty = 0; + + n = 1; + + logitBias?: Record; + + modelName = "gpt-3.5-turbo"; -export { - type AzureOpenAIInput, - type OpenAICallOptions, - type OpenAIChatInput, -} from "@langchain/openai"; + modelKwargs?: OpenAIChatInput["modelKwargs"]; + + stop?: string[]; + + user?: string; + + timeout?: number; + + streaming = false; + + maxTokens?: number; + + openAIApiKey?: string; + + azureOpenAIApiVersion?: string; + + azureOpenAIApiKey?: string; + + azureOpenAIApiInstanceName?: string; + + azureOpenAIApiDeploymentName?: string; + + azureOpenAIBasePath?: string; + + organization?: string; + + private client: OpenAIClient; + + private clientConfig: ClientOptions; + + constructor( + fields?: Partial & + Partial & + BaseChatModelParams & { + configuration?: ClientOptions & LegacyOpenAIInput; + }, + /** @deprecated */ + configuration?: ClientOptions & LegacyOpenAIInput + ) { + super(fields ?? {}); + + this.openAIApiKey = + fields?.openAIApiKey ?? getEnvironmentVariable("OPENAI_API_KEY"); + + this.azureOpenAIApiKey = + fields?.azureOpenAIApiKey ?? + getEnvironmentVariable("AZURE_OPENAI_API_KEY"); + + if (!this.azureOpenAIApiKey && !this.openAIApiKey) { + throw new Error("OpenAI or Azure OpenAI API key not found"); + } + + this.azureOpenAIApiInstanceName = + fields?.azureOpenAIApiInstanceName ?? + getEnvironmentVariable("AZURE_OPENAI_API_INSTANCE_NAME"); + + this.azureOpenAIApiDeploymentName = + fields?.azureOpenAIApiDeploymentName ?? + getEnvironmentVariable("AZURE_OPENAI_API_DEPLOYMENT_NAME"); + + this.azureOpenAIApiVersion = + fields?.azureOpenAIApiVersion ?? + getEnvironmentVariable("AZURE_OPENAI_API_VERSION"); + + this.azureOpenAIBasePath = + fields?.azureOpenAIBasePath ?? + getEnvironmentVariable("AZURE_OPENAI_BASE_PATH"); + + this.organization = + fields?.configuration?.organization ?? + getEnvironmentVariable("OPENAI_ORGANIZATION"); + + this.modelName = fields?.modelName ?? this.modelName; + 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.maxTokens = fields?.maxTokens; + this.n = fields?.n ?? this.n; + this.logitBias = fields?.logitBias; + this.stop = fields?.stop; + this.user = fields?.user; + + this.streaming = fields?.streaming ?? false; + + if (this.azureOpenAIApiKey) { + if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { + throw new Error("Azure OpenAI API instance name not found"); + } + if (!this.azureOpenAIApiDeploymentName) { + throw new Error("Azure OpenAI API deployment name not found"); + } + if (!this.azureOpenAIApiVersion) { + throw new Error("Azure OpenAI API version not found"); + } + this.openAIApiKey = this.openAIApiKey ?? ""; + } + + this.clientConfig = { + apiKey: this.openAIApiKey, + organization: this.organization, + baseURL: configuration?.basePath ?? fields?.configuration?.basePath, + dangerouslyAllowBrowser: true, + defaultHeaders: + configuration?.baseOptions?.headers ?? + fields?.configuration?.baseOptions?.headers, + defaultQuery: + configuration?.baseOptions?.params ?? + fields?.configuration?.baseOptions?.params, + ...configuration, + ...fields?.configuration, + }; + } + + /** + * Get the parameters used to invoke the model + */ + invocationParams( + options?: this["ParsedCallOptions"] + ): Omit { + function isStructuredToolArray( + tools?: unknown[] + ): tools is StructuredTool[] { + return ( + tools !== undefined && + tools.every((tool) => + Array.isArray((tool as StructuredTool).lc_namespace) + ) + ); + } + const params: Omit< + OpenAIClient.Chat.ChatCompletionCreateParams, + "messages" + > = { + model: this.modelName, + temperature: this.temperature, + top_p: this.topP, + frequency_penalty: this.frequencyPenalty, + presence_penalty: this.presencePenalty, + max_tokens: this.maxTokens === -1 ? undefined : this.maxTokens, + n: this.n, + logit_bias: this.logitBias, + stop: options?.stop ?? this.stop, + user: this.user, + stream: this.streaming, + functions: options?.functions, + function_call: options?.function_call, + tools: isStructuredToolArray(options?.tools) + ? options?.tools.map(formatToOpenAITool) + : options?.tools, + tool_choice: options?.tool_choice, + response_format: options?.response_format, + seed: options?.seed, + ...this.modelKwargs, + }; + return params; + } + + /** @ignore */ + _identifyingParams(): Omit< + OpenAIClient.Chat.ChatCompletionCreateParams, + "messages" + > & { + model_name: string; + } & ClientOptions { + return { + model_name: this.modelName, + ...this.invocationParams(), + ...this.clientConfig, + }; + } + + async *_streamResponseChunks( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const messagesMapped: OpenAICompletionParam[] = + convertMessagesToOpenAIParams(messages); + const params = { + ...this.invocationParams(options), + messages: messagesMapped, + stream: true as const, + }; + let defaultRole: OpenAIRoleEnum | undefined; + const streamIterable = await this.completionWithRetry(params, options); + for await (const data of streamIterable) { + const choice = data?.choices[0]; + if (!choice) { + continue; + } -export { type ChatOpenAICallOptions, ChatOpenAI }; + const { delta } = choice; + if (!delta) { + continue; + } + const chunk = _convertDeltaToMessageChunk(delta, 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; + } + const generationChunk = new ChatGenerationChunk({ + message: chunk, + text: chunk.content, + generationInfo: newTokenIndices, + }); + yield generationChunk; + // eslint-disable-next-line no-void + void runManager?.handleLLMNewToken( + generationChunk.text ?? "", + newTokenIndices, + undefined, + undefined, + undefined, + { chunk: generationChunk } + ); + } + if (options.signal?.aborted) { + throw new Error("AbortError"); + } + } + + /** + * Get the identifying parameters for the model + * + */ + identifyingParams() { + return this._identifyingParams(); + } + + /** @ignore */ + async _generate( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): Promise { + const tokenUsage: TokenUsage = {}; + const params = this.invocationParams(options); + const messagesMapped: OpenAICompletionParam[] = + convertMessagesToOpenAIParams(messages); + + if (params.stream) { + const stream = this._streamResponseChunks(messages, options, runManager); + const finalChunks: Record = {}; + for await (const chunk of stream) { + const index = + (chunk.generationInfo as NewTokenIndices)?.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 + ); + + tokenUsage.promptTokens = promptTokenUsage; + tokenUsage.completionTokens = completionTokenUsage; + tokenUsage.totalTokens = promptTokenUsage + completionTokenUsage; + return { generations, llmOutput: { estimatedTokenUsage: tokenUsage } }; + } else { + const data = await this.completionWithRetry( + { + ...params, + stream: false, + messages: messagesMapped, + }, + { + signal: options?.signal, + ...options?.options, + } + ); + const { + completion_tokens: completionTokens, + prompt_tokens: promptTokens, + total_tokens: totalTokens, + } = data?.usage ?? {}; + + if (completionTokens) { + tokenUsage.completionTokens = + (tokenUsage.completionTokens ?? 0) + completionTokens; + } + + if (promptTokens) { + tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens; + } + + if (totalTokens) { + tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens; + } + + const generations: ChatGeneration[] = []; + for (const part of data?.choices ?? []) { + const text = part.message?.content ?? ""; + const generation: ChatGeneration = { + text, + message: openAIResponseToChatMessage( + part.message ?? { role: "assistant" } + ), + }; + if (part.finish_reason) { + generation.generationInfo = { finish_reason: part.finish_reason }; + } + generations.push(generation); + } + return { + generations, + llmOutput: { tokenUsage }, + }; + } + } + + /** + * Estimate the number of tokens a prompt will use. + * Modified from: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts + */ + private async getEstimatedTokenCountFromPrompt( + messages: BaseMessage[], + functions?: OpenAIFnDef[], + function_call?: "none" | "auto" | OpenAIFnCallOption + ): Promise { + // 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 = formatFunctionDefinitions( + functions as unknown as FunctionDef[] + ); + 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. + */ + private async getNumTokensFromGenerations(generations: ChatGeneration[]) { + 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 { + return await this.getNumTokens(generation.message.content); + } + }) + ); + + return generationUsages.reduce((a, b) => a + b, 0); + } + + async getNumTokensFromMessages(messages: BaseMessage[]) { + let totalCount = 0; + let tokensPerMessage = 0; + let tokensPerName = 0; + + // From: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb + if (this.modelName === "gpt-3.5-turbo-0301") { + tokensPerMessage = 4; + tokensPerName = -1; + } else { + tokensPerMessage = 3; + tokensPerName = 1; + } + + const countPerMessage = await Promise.all( + messages.map(async (message) => { + const textCount = await this.getNumTokens(message.content); + const roleCount = await this.getNumTokens(messageToOpenAIRole(message)); + const nameCount = + message.name !== undefined + ? tokensPerName + (await this.getNumTokens(message.name)) + : 0; + let count = textCount + tokensPerMessage + roleCount + nameCount; + + // From: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts messageTokenEstimate + const openAIMessage = message; + if (openAIMessage._getType() === "function") { + count -= 2; + } + if (openAIMessage.additional_kwargs?.function_call) { + count += 3; + } + if (openAIMessage?.additional_kwargs.function_call?.name) { + count += await this.getNumTokens( + openAIMessage.additional_kwargs.function_call?.name + ); + } + if (openAIMessage.additional_kwargs.function_call?.arguments) { + count += await this.getNumTokens( + // Remove newlines and spaces + JSON.stringify( + JSON.parse( + openAIMessage.additional_kwargs.function_call?.arguments + ) + ) + ); + } + + totalCount += count; + return count; + }) + ); + + totalCount += 3; // every reply is primed with <|start|>assistant<|message|> + + return { totalCount, countPerMessage }; + } + + /** + * Calls the OpenAI API with retry logic in case of failures. + * @param request The request to send to the OpenAI API. + * @param options Optional configuration for the API call. + * @returns The response from the OpenAI API. + */ + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming, + options?: OpenAICoreRequestOptions + ): Promise>; + + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise; + + async completionWithRetry( + request: + | OpenAIClient.Chat.ChatCompletionCreateParamsStreaming + | OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise< + | AsyncIterable + | OpenAIClient.Chat.Completions.ChatCompletion + > { + const requestOptions = this._getClientOptions(options); + return this.caller.call(async () => { + try { + const res = await this.client.chat.completions.create( + request, + requestOptions + ); + return res; + } catch (e) { + const error = wrapOpenAIClientError(e); + throw error; + } + }); + } + + private _getClientOptions(options: OpenAICoreRequestOptions | undefined) { + if (!this.client) { + const openAIEndpointConfig: OpenAIEndpointConfig = { + azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName, + azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName, + azureOpenAIApiKey: this.azureOpenAIApiKey, + azureOpenAIBasePath: this.azureOpenAIBasePath, + baseURL: this.clientConfig.baseURL, + }; + + const endpoint = getEndpoint(openAIEndpointConfig); + const params = { + ...this.clientConfig, + baseURL: endpoint, + timeout: this.timeout, + maxRetries: 0, + }; + if (!params.baseURL) { + delete params.baseURL; + } + + this.client = new OpenAIClient(params); + } + const requestOptions = { + ...this.clientConfig, + ...options, + } as OpenAICoreRequestOptions; + if (this.azureOpenAIApiKey) { + requestOptions.headers = { + "api-key": this.azureOpenAIApiKey, + ...requestOptions.headers, + }; + requestOptions.query = { + "api-version": this.azureOpenAIApiVersion, + ...requestOptions.query, + }; + } + return requestOptions; + } + + _llmType() { + return "openai"; + } + + /** @ignore */ + _combineLLMOutput(...llmOutputs: OpenAILLMOutput[]): OpenAILLMOutput { + return llmOutputs.reduce<{ + [key in keyof OpenAILLMOutput]: Required; + }>( + (acc, llmOutput) => { + if (llmOutput && llmOutput.tokenUsage) { + acc.tokenUsage.completionTokens += + llmOutput.tokenUsage.completionTokens ?? 0; + acc.tokenUsage.promptTokens += llmOutput.tokenUsage.promptTokens ?? 0; + acc.tokenUsage.totalTokens += llmOutput.tokenUsage.totalTokens ?? 0; + } + return acc; + }, + { + tokenUsage: { + completionTokens: 0, + promptTokens: 0, + totalTokens: 0, + }, + } + ); + } +} export class PromptLayerChatOpenAI extends ChatOpenAI { promptLayerApiKey?: string; diff --git a/langchain/src/chat_models/tests/chatanthropic.int.test.ts b/langchain/src/chat_models/tests/chatanthropic.int.test.ts index 05470df53d0d..cfc572c9584d 100644 --- a/langchain/src/chat_models/tests/chatanthropic.int.test.ts +++ b/langchain/src/chat_models/tests/chatanthropic.int.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-process-env */ import { expect, test } from "@jest/globals"; -import { HUMAN_PROMPT } from "@langchain/anthropic"; +import { HUMAN_PROMPT } from "@anthropic-ai/sdk"; import { ChatMessage, HumanMessage } from "../../schema/index.js"; import { ChatPromptValue } from "../../prompts/chat.js"; import { diff --git a/langchain/src/document_loaders/fs/openai_whisper_audio.ts b/langchain/src/document_loaders/fs/openai_whisper_audio.ts index 468eba28c1e5..fbbdbef6f25b 100644 --- a/langchain/src/document_loaders/fs/openai_whisper_audio.ts +++ b/langchain/src/document_loaders/fs/openai_whisper_audio.ts @@ -1,4 +1,4 @@ -import { type ClientOptions, OpenAIClient, toFile } from "@langchain/openai"; +import { type ClientOptions, OpenAI as OpenAIClient, toFile } from "openai"; import { Document } from "../../document.js"; import { BufferLoader } from "./buffer.js"; diff --git a/langchain/src/embeddings/openai.ts b/langchain/src/embeddings/openai.ts index 9c5af9d81995..0cc726d03ca3 100644 --- a/langchain/src/embeddings/openai.ts +++ b/langchain/src/embeddings/openai.ts @@ -1,4 +1,269 @@ -export { - type OpenAIEmbeddingsParams, - OpenAIEmbeddings, -} from "@langchain/openai"; +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; +import { getEnvironmentVariable } from "../util/env.js"; +import { + AzureOpenAIInput, + OpenAICoreRequestOptions, + LegacyOpenAIInput, +} from "../types/openai-types.js"; +import { chunkArray } from "../util/chunk.js"; +import { Embeddings, EmbeddingsParams } from "./base.js"; +import { getEndpoint, OpenAIEndpointConfig } from "../util/azure.js"; +import { wrapOpenAIClientError } from "../util/openai.js"; + +/** + * Interface for OpenAIEmbeddings parameters. Extends EmbeddingsParams and + * defines additional parameters specific to the OpenAIEmbeddings class. + */ +export interface OpenAIEmbeddingsParams extends EmbeddingsParams { + /** Model name to use */ + modelName: string; + + /** + * Timeout to use when making requests to OpenAI. + */ + timeout?: number; + + /** + * The maximum number of documents to embed in a single request. This is + * limited by the OpenAI API to a maximum of 2048. + */ + batchSize?: number; + + /** + * Whether to strip new lines from the input text. This is recommended by + * OpenAI, but may not be suitable for all use cases. + */ + stripNewLines?: boolean; +} + +/** + * Class for generating embeddings using the OpenAI API. Extends the + * Embeddings class and implements OpenAIEmbeddingsParams and + * AzureOpenAIInput. + * @example + * ```typescript + * // Embed a query using OpenAIEmbeddings to generate embeddings for a given text + * const model = new OpenAIEmbeddings(); + * const res = await model.embedQuery( + * "What would be a good company name for a company that makes colorful socks?", + * ); + * console.log({ res }); + * + * ``` + */ +export class OpenAIEmbeddings + extends Embeddings + implements OpenAIEmbeddingsParams, AzureOpenAIInput +{ + modelName = "text-embedding-ada-002"; + + batchSize = 512; + + stripNewLines = true; + + timeout?: number; + + azureOpenAIApiVersion?: string; + + azureOpenAIApiKey?: string; + + azureOpenAIApiInstanceName?: string; + + azureOpenAIApiDeploymentName?: string; + + azureOpenAIBasePath?: string; + + organization?: string; + + private client: OpenAIClient; + + private clientConfig: ClientOptions; + + constructor( + fields?: Partial & + Partial & { + verbose?: boolean; + openAIApiKey?: string; + configuration?: ClientOptions; + }, + configuration?: ClientOptions & LegacyOpenAIInput + ) { + const fieldsWithDefaults = { maxConcurrency: 2, ...fields }; + + super(fieldsWithDefaults); + + let apiKey = + fieldsWithDefaults?.openAIApiKey ?? + getEnvironmentVariable("OPENAI_API_KEY"); + + const azureApiKey = + fieldsWithDefaults?.azureOpenAIApiKey ?? + getEnvironmentVariable("AZURE_OPENAI_API_KEY"); + if (!azureApiKey && !apiKey) { + throw new Error("OpenAI or Azure OpenAI API key not found"); + } + + const azureApiInstanceName = + fieldsWithDefaults?.azureOpenAIApiInstanceName ?? + getEnvironmentVariable("AZURE_OPENAI_API_INSTANCE_NAME"); + + const azureApiDeploymentName = + (fieldsWithDefaults?.azureOpenAIApiEmbeddingsDeploymentName || + fieldsWithDefaults?.azureOpenAIApiDeploymentName) ?? + (getEnvironmentVariable("AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME") || + getEnvironmentVariable("AZURE_OPENAI_API_DEPLOYMENT_NAME")); + + const azureApiVersion = + fieldsWithDefaults?.azureOpenAIApiVersion ?? + getEnvironmentVariable("AZURE_OPENAI_API_VERSION"); + + this.azureOpenAIBasePath = + fieldsWithDefaults?.azureOpenAIBasePath ?? + getEnvironmentVariable("AZURE_OPENAI_BASE_PATH"); + + this.organization = + fieldsWithDefaults?.configuration?.organization ?? + getEnvironmentVariable("OPENAI_ORGANIZATION"); + + this.modelName = fieldsWithDefaults?.modelName ?? this.modelName; + this.batchSize = + fieldsWithDefaults?.batchSize ?? (azureApiKey ? 1 : this.batchSize); + this.stripNewLines = + fieldsWithDefaults?.stripNewLines ?? this.stripNewLines; + this.timeout = fieldsWithDefaults?.timeout; + + this.azureOpenAIApiVersion = azureApiVersion; + this.azureOpenAIApiKey = azureApiKey; + this.azureOpenAIApiInstanceName = azureApiInstanceName; + this.azureOpenAIApiDeploymentName = azureApiDeploymentName; + + if (this.azureOpenAIApiKey) { + if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { + throw new Error("Azure OpenAI API instance name not found"); + } + if (!this.azureOpenAIApiDeploymentName) { + throw new Error("Azure OpenAI API deployment name not found"); + } + if (!this.azureOpenAIApiVersion) { + throw new Error("Azure OpenAI API version not found"); + } + apiKey = apiKey ?? ""; + } + + this.clientConfig = { + apiKey, + organization: this.organization, + baseURL: configuration?.basePath, + dangerouslyAllowBrowser: true, + defaultHeaders: configuration?.baseOptions?.headers, + defaultQuery: configuration?.baseOptions?.params, + ...configuration, + ...fields?.configuration, + }; + } + + /** + * Method to generate embeddings for an array of documents. Splits the + * documents into batches and makes requests to the OpenAI API to generate + * embeddings. + * @param texts Array of documents to generate embeddings for. + * @returns Promise that resolves to a 2D array of embeddings for each document. + */ + async embedDocuments(texts: string[]): Promise { + const batches = chunkArray( + this.stripNewLines ? texts.map((t) => t.replace(/\n/g, " ")) : texts, + this.batchSize + ); + + const batchRequests = batches.map((batch) => + this.embeddingWithRetry({ + model: this.modelName, + input: batch, + }) + ); + const batchResponses = await Promise.all(batchRequests); + + const embeddings: number[][] = []; + for (let i = 0; i < batchResponses.length; i += 1) { + const batch = batches[i]; + const { data: batchResponse } = batchResponses[i]; + for (let j = 0; j < batch.length; j += 1) { + embeddings.push(batchResponse[j].embedding); + } + } + return embeddings; + } + + /** + * Method to generate an embedding for a single document. Calls the + * embeddingWithRetry method with the document as the input. + * @param text Document to generate an embedding for. + * @returns Promise that resolves to an embedding for the document. + */ + async embedQuery(text: string): Promise { + const { data } = await this.embeddingWithRetry({ + model: this.modelName, + input: this.stripNewLines ? text.replace(/\n/g, " ") : text, + }); + return data[0].embedding; + } + + /** + * Private method to make a request to the OpenAI API to generate + * embeddings. Handles the retry logic and returns the response from the + * API. + * @param request Request to send to the OpenAI API. + * @returns Promise that resolves to the response from the API. + */ + private async embeddingWithRetry( + request: OpenAIClient.EmbeddingCreateParams + ) { + if (!this.client) { + const openAIEndpointConfig: OpenAIEndpointConfig = { + azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName, + azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName, + azureOpenAIApiKey: this.azureOpenAIApiKey, + azureOpenAIBasePath: this.azureOpenAIBasePath, + baseURL: this.clientConfig.baseURL, + }; + + const endpoint = getEndpoint(openAIEndpointConfig); + + const params = { + ...this.clientConfig, + baseURL: endpoint, + timeout: this.timeout, + maxRetries: 0, + }; + + if (!params.baseURL) { + delete params.baseURL; + } + + this.client = new OpenAIClient(params); + } + const requestOptions: OpenAICoreRequestOptions = {}; + if (this.azureOpenAIApiKey) { + requestOptions.headers = { + "api-key": this.azureOpenAIApiKey, + ...requestOptions.headers, + }; + requestOptions.query = { + "api-version": this.azureOpenAIApiVersion, + ...requestOptions.query, + }; + } + return this.caller.call(async () => { + try { + const res = await this.client.embeddings.create( + request, + requestOptions + ); + return res; + } catch (e) { + const error = wrapOpenAIClientError(e); + throw error; + } + }); + } +} diff --git a/langchain/src/experimental/openai_assistant/index.ts b/langchain/src/experimental/openai_assistant/index.ts index 83f1330c8803..60f95757f778 100644 --- a/langchain/src/experimental/openai_assistant/index.ts +++ b/langchain/src/experimental/openai_assistant/index.ts @@ -1,4 +1,4 @@ -import { type ClientOptions, OpenAIClient } from "@langchain/openai"; +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; import { Runnable } from "../../schema/runnable/base.js"; import { sleep } from "../../util/time.js"; import type { RunnableConfig } from "../../schema/runnable/config.js"; diff --git a/langchain/src/experimental/openai_assistant/schema.ts b/langchain/src/experimental/openai_assistant/schema.ts index a74ca1fce69f..10d4ce2658f4 100644 --- a/langchain/src/experimental/openai_assistant/schema.ts +++ b/langchain/src/experimental/openai_assistant/schema.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import type { AgentFinish, AgentAction } from "../../schema/index.js"; export type OpenAIAssistantFinish = AgentFinish & { diff --git a/langchain/src/experimental/openai_assistant/tests/openai_assistant.int.test.ts b/langchain/src/experimental/openai_assistant/tests/openai_assistant.int.test.ts index 30afea5a00c3..8f206ceba79f 100644 --- a/langchain/src/experimental/openai_assistant/tests/openai_assistant.int.test.ts +++ b/langchain/src/experimental/openai_assistant/tests/openai_assistant.int.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { z } from "zod"; -import { OpenAIClient } from "@langchain/openai"; +import { OpenAI as OpenAIClient } from "openai"; import { AgentExecutor } from "../../../agents/executor.js"; import { StructuredTool } from "../../../tools/base.js"; import { OpenAIAssistantRunnable } from "../index.js"; diff --git a/langchain/src/experimental/openai_files/index.ts b/langchain/src/experimental/openai_files/index.ts index 62f35dadf438..6522e906fd29 100644 --- a/langchain/src/experimental/openai_files/index.ts +++ b/langchain/src/experimental/openai_files/index.ts @@ -1,4 +1,4 @@ -import { OpenAIClient, type ClientOptions } from "@langchain/openai"; +import { OpenAI as OpenAIClient, type ClientOptions } from "openai"; import { Serializable } from "../../load/serializable.js"; export type OpenAIFilesInput = { diff --git a/langchain/src/llms/fireworks.ts b/langchain/src/llms/fireworks.ts index 3477bd85ecd4..8dd5981aad69 100644 --- a/langchain/src/llms/fireworks.ts +++ b/langchain/src/llms/fireworks.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import type { BaseLLMParams } from "./base.js"; import type { OpenAICallOptions, OpenAIInput } from "./openai.js"; diff --git a/langchain/src/llms/openai-chat.ts b/langchain/src/llms/openai-chat.ts index 06cb7faf0f1d..4077a5efde89 100644 --- a/langchain/src/llms/openai-chat.ts +++ b/langchain/src/llms/openai-chat.ts @@ -1,18 +1,471 @@ -import { OpenAIChat } from "@langchain/openai"; - +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; import { CallbackManagerForLLMRun } from "../callbacks/manager.js"; -import type { Generation, LLMResult } from "../schema/index.js"; +import { Generation, GenerationChunk, LLMResult } from "../schema/index.js"; +import { + AzureOpenAIInput, + OpenAICallOptions, + OpenAIChatInput, + OpenAICoreRequestOptions, + LegacyOpenAIInput, +} from "../types/openai-types.js"; +import { OpenAIEndpointConfig, getEndpoint } from "../util/azure.js"; import { getEnvironmentVariable } from "../util/env.js"; import { promptLayerTrackRequest } from "../util/prompt-layer.js"; +import { BaseLLMParams, LLM } from "./base.js"; +import { wrapOpenAIClientError } from "../util/openai.js"; + +export { type AzureOpenAIInput, type OpenAIChatInput }; +/** + * Interface that extends the OpenAICallOptions interface and includes an + * optional promptIndex property. It represents the options that can be + * passed when making a call to the OpenAI Chat API. + */ +export interface OpenAIChatCallOptions extends OpenAICallOptions { + promptIndex?: number; +} + +/** + * Wrapper around OpenAI large language models that use the Chat endpoint. + * + * To use you should have the `openai` package installed, with the + * `OPENAI_API_KEY` environment variable set. + * + * To use with Azure you should have the `openai` package installed, with the + * `AZURE_OPENAI_API_KEY`, + * `AZURE_OPENAI_API_INSTANCE_NAME`, + * `AZURE_OPENAI_API_DEPLOYMENT_NAME` + * and `AZURE_OPENAI_API_VERSION` environment variable set. + * + * @remarks + * Any parameters that are valid to be passed to {@link + * https://platform.openai.com/docs/api-reference/chat/create | + * `openai.createCompletion`} can be passed through {@link modelKwargs}, even + * if not explicitly available on this class. + * + * @augments BaseLLM + * @augments OpenAIInput + * @augments AzureOpenAIChatInput + * @example + * ```typescript + * const model = new OpenAIChat({ + * prefixMessages: [ + * { + * role: "system", + * content: "You are a helpful assistant that answers in pirate language", + * }, + * ], + * maxTokens: 50, + * }); + * + * const res = await model.call( + * "What would be a good company name for a company that makes colorful socks?" + * ); + * console.log({ res }); + * ``` + */ +export class OpenAIChat + extends LLM + implements OpenAIChatInput, AzureOpenAIInput +{ + static lc_name() { + return "OpenAIChat"; + } + + get callKeys() { + return [...super.callKeys, "options", "promptIndex"]; + } + + lc_serializable = true; + + get lc_secrets(): { [key: string]: string } | undefined { + return { + openAIApiKey: "OPENAI_API_KEY", + azureOpenAIApiKey: "AZURE_OPENAI_API_KEY", + organization: "OPENAI_ORGANIZATION", + }; + } + + get lc_aliases(): Record { + return { + modelName: "model", + openAIApiKey: "openai_api_key", + azureOpenAIApiVersion: "azure_openai_api_version", + azureOpenAIApiKey: "azure_openai_api_key", + azureOpenAIApiInstanceName: "azure_openai_api_instance_name", + azureOpenAIApiDeploymentName: "azure_openai_api_deployment_name", + }; + } + + temperature = 1; + + topP = 1; + + frequencyPenalty = 0; + + presencePenalty = 0; + + n = 1; + + logitBias?: Record; + + maxTokens?: number; + + modelName = "gpt-3.5-turbo"; + + prefixMessages?: OpenAIClient.Chat.ChatCompletionMessageParam[]; + + modelKwargs?: OpenAIChatInput["modelKwargs"]; + + timeout?: number; + + stop?: string[]; + + user?: string; + + streaming = false; + + openAIApiKey?: string; + + azureOpenAIApiVersion?: string; + + azureOpenAIApiKey?: string; + + azureOpenAIApiInstanceName?: string; + + azureOpenAIApiDeploymentName?: string; + + azureOpenAIBasePath?: string; + + organization?: string; + + private client: OpenAIClient; + + private clientConfig: ClientOptions; + + constructor( + fields?: Partial & + Partial & + BaseLLMParams & { + configuration?: ClientOptions & LegacyOpenAIInput; + }, + /** @deprecated */ + configuration?: ClientOptions & LegacyOpenAIInput + ) { + super(fields ?? {}); + + this.openAIApiKey = + fields?.openAIApiKey ?? getEnvironmentVariable("OPENAI_API_KEY"); + + this.azureOpenAIApiKey = + fields?.azureOpenAIApiKey ?? + getEnvironmentVariable("AZURE_OPENAI_API_KEY"); + + if (!this.azureOpenAIApiKey && !this.openAIApiKey) { + throw new Error("OpenAI or Azure OpenAI API key not found"); + } + + this.azureOpenAIApiInstanceName = + fields?.azureOpenAIApiInstanceName ?? + getEnvironmentVariable("AZURE_OPENAI_API_INSTANCE_NAME"); + + this.azureOpenAIApiDeploymentName = + (fields?.azureOpenAIApiCompletionsDeploymentName || + fields?.azureOpenAIApiDeploymentName) ?? + (getEnvironmentVariable("AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME") || + getEnvironmentVariable("AZURE_OPENAI_API_DEPLOYMENT_NAME")); + + this.azureOpenAIApiVersion = + fields?.azureOpenAIApiVersion ?? + getEnvironmentVariable("AZURE_OPENAI_API_VERSION"); + + this.azureOpenAIBasePath = + fields?.azureOpenAIBasePath ?? + getEnvironmentVariable("AZURE_OPENAI_BASE_PATH"); -export { - type AzureOpenAIInput, - type OpenAICallOptions, - type OpenAIInput, - type OpenAIChatCallOptions, -} from "@langchain/openai"; + this.organization = + fields?.configuration?.organization ?? + getEnvironmentVariable("OPENAI_ORGANIZATION"); -export { OpenAIChat }; + this.modelName = fields?.modelName ?? this.modelName; + this.prefixMessages = fields?.prefixMessages ?? this.prefixMessages; + 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.n = fields?.n ?? this.n; + this.logitBias = fields?.logitBias; + this.maxTokens = fields?.maxTokens; + this.stop = fields?.stop; + this.user = fields?.user; + + this.streaming = fields?.streaming ?? false; + + if (this.n > 1) { + throw new Error( + "Cannot use n > 1 in OpenAIChat LLM. Use ChatOpenAI Chat Model instead." + ); + } + + if (this.azureOpenAIApiKey) { + if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { + throw new Error("Azure OpenAI API instance name not found"); + } + if (!this.azureOpenAIApiDeploymentName) { + throw new Error("Azure OpenAI API deployment name not found"); + } + if (!this.azureOpenAIApiVersion) { + throw new Error("Azure OpenAI API version not found"); + } + this.openAIApiKey = this.openAIApiKey ?? ""; + } + + this.clientConfig = { + apiKey: this.openAIApiKey, + organization: this.organization, + baseURL: configuration?.basePath ?? fields?.configuration?.basePath, + dangerouslyAllowBrowser: true, + defaultHeaders: + configuration?.baseOptions?.headers ?? + fields?.configuration?.baseOptions?.headers, + defaultQuery: + configuration?.baseOptions?.params ?? + fields?.configuration?.baseOptions?.params, + ...configuration, + ...fields?.configuration, + }; + } + + /** + * Get the parameters used to invoke the model + */ + invocationParams( + options?: this["ParsedCallOptions"] + ): Omit { + return { + model: this.modelName, + temperature: this.temperature, + top_p: this.topP, + frequency_penalty: this.frequencyPenalty, + presence_penalty: this.presencePenalty, + n: this.n, + logit_bias: this.logitBias, + max_tokens: this.maxTokens === -1 ? undefined : this.maxTokens, + stop: options?.stop ?? this.stop, + user: this.user, + stream: this.streaming, + ...this.modelKwargs, + }; + } + + /** @ignore */ + _identifyingParams(): Omit< + OpenAIClient.Chat.ChatCompletionCreateParams, + "messages" + > & { + model_name: string; + } & ClientOptions { + return { + model_name: this.modelName, + ...this.invocationParams(), + ...this.clientConfig, + }; + } + + /** + * Get the identifying parameters for the model + */ + identifyingParams(): Omit< + OpenAIClient.Chat.ChatCompletionCreateParams, + "messages" + > & { + model_name: string; + } & ClientOptions { + return { + model_name: this.modelName, + ...this.invocationParams(), + ...this.clientConfig, + }; + } + + /** + * Formats the messages for the OpenAI API. + * @param prompt The prompt to be formatted. + * @returns Array of formatted messages. + */ + private formatMessages( + prompt: string + ): OpenAIClient.Chat.ChatCompletionMessageParam[] { + const message: OpenAIClient.Chat.ChatCompletionMessageParam = { + role: "user", + content: prompt, + }; + return this.prefixMessages ? [...this.prefixMessages, message] : [message]; + } + + async *_streamResponseChunks( + prompt: string, + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const params = { + ...this.invocationParams(options), + messages: this.formatMessages(prompt), + stream: true as const, + }; + const stream = await this.completionWithRetry(params, options); + for await (const data of stream) { + const choice = data?.choices[0]; + if (!choice) { + continue; + } + const { delta } = choice; + const generationChunk = new GenerationChunk({ + text: delta.content ?? "", + }); + yield generationChunk; + const newTokenIndices = { + prompt: options.promptIndex ?? 0, + completion: choice.index ?? 0, + }; + // eslint-disable-next-line no-void + void runManager?.handleLLMNewToken( + generationChunk.text ?? "", + newTokenIndices + ); + } + if (options.signal?.aborted) { + throw new Error("AbortError"); + } + } + + /** @ignore */ + async _call( + prompt: string, + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): Promise { + const params = this.invocationParams(options); + + if (params.stream) { + const stream = await this._streamResponseChunks( + prompt, + options, + runManager + ); + let finalChunk: GenerationChunk | undefined; + for await (const chunk of stream) { + if (finalChunk === undefined) { + finalChunk = chunk; + } else { + finalChunk = finalChunk.concat(chunk); + } + } + return finalChunk?.text ?? ""; + } else { + const response = await this.completionWithRetry( + { + ...params, + stream: false, + messages: this.formatMessages(prompt), + }, + { + signal: options.signal, + ...options.options, + } + ); + return response?.choices[0]?.message?.content ?? ""; + } + } + + /** + * Calls the OpenAI API with retry logic in case of failures. + * @param request The request to send to the OpenAI API. + * @param options Optional configuration for the API call. + * @returns The response from the OpenAI API. + */ + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming, + options?: OpenAICoreRequestOptions + ): Promise>; + + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise; + + async completionWithRetry( + request: + | OpenAIClient.Chat.ChatCompletionCreateParamsStreaming + | OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise< + | AsyncIterable + | OpenAIClient.Chat.Completions.ChatCompletion + > { + const requestOptions = this._getClientOptions(options); + return this.caller.call(async () => { + try { + const res = await this.client.chat.completions.create( + request, + requestOptions + ); + return res; + } catch (e) { + const error = wrapOpenAIClientError(e); + throw error; + } + }); + } + + /** @ignore */ + private _getClientOptions(options: OpenAICoreRequestOptions | undefined) { + if (!this.client) { + const openAIEndpointConfig: OpenAIEndpointConfig = { + azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName, + azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName, + azureOpenAIApiKey: this.azureOpenAIApiKey, + azureOpenAIBasePath: this.azureOpenAIBasePath, + baseURL: this.clientConfig.baseURL, + }; + + const endpoint = getEndpoint(openAIEndpointConfig); + + const params = { + ...this.clientConfig, + baseURL: endpoint, + timeout: this.timeout, + maxRetries: 0, + }; + if (!params.baseURL) { + delete params.baseURL; + } + + this.client = new OpenAIClient(params); + } + const requestOptions = { + ...this.clientConfig, + ...options, + } as OpenAICoreRequestOptions; + if (this.azureOpenAIApiKey) { + requestOptions.headers = { + "api-key": this.azureOpenAIApiKey, + ...requestOptions.headers, + }; + requestOptions.query = { + "api-version": this.azureOpenAIApiVersion, + ...requestOptions.query, + }; + } + return requestOptions; + } + + _llmType() { + return "openai"; + } +} /** * PromptLayer wrapper to OpenAIChat diff --git a/langchain/src/llms/openai.ts b/langchain/src/llms/openai.ts index edfb429272b5..c962f30c096f 100644 --- a/langchain/src/llms/openai.ts +++ b/langchain/src/llms/openai.ts @@ -1,17 +1,558 @@ -import { OpenAI } from "@langchain/openai"; - +import type { TiktokenModel } from "js-tiktoken/lite"; +import { type ClientOptions, OpenAI as OpenAIClient } from "openai"; +import { calculateMaxTokens } from "../base_language/count_tokens.js"; import { CallbackManagerForLLMRun } from "../callbacks/manager.js"; -import type { LLMResult } from "../schema/index.js"; +import { GenerationChunk, LLMResult } from "../schema/index.js"; +import { + AzureOpenAIInput, + OpenAICallOptions, + OpenAICoreRequestOptions, + OpenAIInput, + LegacyOpenAIInput, +} from "../types/openai-types.js"; +import { OpenAIEndpointConfig, getEndpoint } from "../util/azure.js"; +import { chunkArray } from "../util/chunk.js"; import { getEnvironmentVariable } from "../util/env.js"; import { promptLayerTrackRequest } from "../util/prompt-layer.js"; +import { BaseLLM, BaseLLMParams } from "./base.js"; +import { OpenAIChat } from "./openai-chat.js"; +import { wrapOpenAIClientError } from "../util/openai.js"; + +export type { AzureOpenAIInput, OpenAICallOptions, OpenAIInput }; + +/** + * Interface for tracking token usage in OpenAI calls. + */ +interface TokenUsage { + completionTokens?: number; + promptTokens?: number; + totalTokens?: number; +} + +/** + * Wrapper around OpenAI large language models. + * + * To use you should have the `openai` package installed, with the + * `OPENAI_API_KEY` environment variable set. + * + * To use with Azure you should have the `openai` package installed, with the + * `AZURE_OPENAI_API_KEY`, + * `AZURE_OPENAI_API_INSTANCE_NAME`, + * `AZURE_OPENAI_API_DEPLOYMENT_NAME` + * and `AZURE_OPENAI_API_VERSION` environment variable set. + * + * @remarks + * Any parameters that are valid to be passed to {@link + * https://platform.openai.com/docs/api-reference/completions/create | + * `openai.createCompletion`} can be passed through {@link modelKwargs}, even + * if not explicitly available on this class. + * @example + * ```typescript + * const model = new OpenAI({ + * modelName: "gpt-4", + * temperature: 0.7, + * maxTokens: 1000, + * maxRetries: 5, + * }); + * + * const res = await model.call( + * "Question: What would be a good company name for a company that makes colorful socks?\nAnswer:" + * ); + * console.log({ res }); + * ``` + */ +export class OpenAI + extends BaseLLM + implements OpenAIInput, AzureOpenAIInput +{ + static lc_name() { + return "OpenAI"; + } + + get callKeys() { + return [...super.callKeys, "options"]; + } + + lc_serializable = true; + + get lc_secrets(): { [key: string]: string } | undefined { + return { + openAIApiKey: "OPENAI_API_KEY", + azureOpenAIApiKey: "AZURE_OPENAI_API_KEY", + organization: "OPENAI_ORGANIZATION", + }; + } + + get lc_aliases(): Record { + return { + modelName: "model", + openAIApiKey: "openai_api_key", + azureOpenAIApiVersion: "azure_openai_api_version", + azureOpenAIApiKey: "azure_openai_api_key", + azureOpenAIApiInstanceName: "azure_openai_api_instance_name", + azureOpenAIApiDeploymentName: "azure_openai_api_deployment_name", + }; + } + + temperature = 0.7; + + maxTokens = 256; + + topP = 1; + + frequencyPenalty = 0; + + presencePenalty = 0; + + n = 1; + + bestOf?: number; + + logitBias?: Record; + + modelName = "gpt-3.5-turbo-instruct"; + + modelKwargs?: OpenAIInput["modelKwargs"]; + + batchSize = 20; + + timeout?: number; -export { - type AzureOpenAIInput, - type OpenAICallOptions, - type OpenAIInput, -} from "@langchain/openai"; + stop?: string[]; -export { OpenAI }; + user?: string; + + streaming = false; + + openAIApiKey?: string; + + azureOpenAIApiVersion?: string; + + azureOpenAIApiKey?: string; + + azureOpenAIApiInstanceName?: string; + + azureOpenAIApiDeploymentName?: string; + + azureOpenAIBasePath?: string; + + organization?: string; + + private client: OpenAIClient; + + private clientConfig: ClientOptions; + + constructor( + fields?: Partial & + Partial & + BaseLLMParams & { + configuration?: ClientOptions & LegacyOpenAIInput; + }, + /** @deprecated */ + configuration?: ClientOptions & LegacyOpenAIInput + ) { + if ( + (fields?.modelName?.startsWith("gpt-3.5-turbo") || + fields?.modelName?.startsWith("gpt-4")) && + !fields?.modelName?.includes("-instruct") + ) { + // eslint-disable-next-line no-constructor-return + return new OpenAIChat( + fields, + configuration + ) as unknown as OpenAI; + } + super(fields ?? {}); + + this.openAIApiKey = + fields?.openAIApiKey ?? getEnvironmentVariable("OPENAI_API_KEY"); + + this.azureOpenAIApiKey = + fields?.azureOpenAIApiKey ?? + getEnvironmentVariable("AZURE_OPENAI_API_KEY"); + + if (!this.azureOpenAIApiKey && !this.openAIApiKey) { + throw new Error("OpenAI or Azure OpenAI API key not found"); + } + + this.azureOpenAIApiInstanceName = + fields?.azureOpenAIApiInstanceName ?? + getEnvironmentVariable("AZURE_OPENAI_API_INSTANCE_NAME"); + + this.azureOpenAIApiDeploymentName = + (fields?.azureOpenAIApiCompletionsDeploymentName || + fields?.azureOpenAIApiDeploymentName) ?? + (getEnvironmentVariable("AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME") || + getEnvironmentVariable("AZURE_OPENAI_API_DEPLOYMENT_NAME")); + + this.azureOpenAIApiVersion = + fields?.azureOpenAIApiVersion ?? + getEnvironmentVariable("AZURE_OPENAI_API_VERSION"); + + this.azureOpenAIBasePath = + fields?.azureOpenAIBasePath ?? + getEnvironmentVariable("AZURE_OPENAI_BASE_PATH"); + + this.organization = + fields?.configuration?.organization ?? + getEnvironmentVariable("OPENAI_ORGANIZATION"); + + this.modelName = fields?.modelName ?? this.modelName; + this.modelKwargs = fields?.modelKwargs ?? {}; + this.batchSize = fields?.batchSize ?? this.batchSize; + this.timeout = fields?.timeout; + + this.temperature = fields?.temperature ?? this.temperature; + this.maxTokens = fields?.maxTokens ?? this.maxTokens; + this.topP = fields?.topP ?? this.topP; + this.frequencyPenalty = fields?.frequencyPenalty ?? this.frequencyPenalty; + this.presencePenalty = fields?.presencePenalty ?? this.presencePenalty; + this.n = fields?.n ?? this.n; + this.bestOf = fields?.bestOf ?? this.bestOf; + this.logitBias = fields?.logitBias; + this.stop = fields?.stop; + this.user = fields?.user; + + this.streaming = fields?.streaming ?? false; + + if (this.streaming && this.bestOf && this.bestOf > 1) { + throw new Error("Cannot stream results when bestOf > 1"); + } + + if (this.azureOpenAIApiKey) { + if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { + throw new Error("Azure OpenAI API instance name not found"); + } + if (!this.azureOpenAIApiDeploymentName) { + throw new Error("Azure OpenAI API deployment name not found"); + } + if (!this.azureOpenAIApiVersion) { + throw new Error("Azure OpenAI API version not found"); + } + this.openAIApiKey = this.openAIApiKey ?? ""; + } + + this.clientConfig = { + apiKey: this.openAIApiKey, + organization: this.organization, + baseURL: configuration?.basePath ?? fields?.configuration?.basePath, + dangerouslyAllowBrowser: true, + defaultHeaders: + configuration?.baseOptions?.headers ?? + fields?.configuration?.baseOptions?.headers, + defaultQuery: + configuration?.baseOptions?.params ?? + fields?.configuration?.baseOptions?.params, + ...configuration, + ...fields?.configuration, + }; + } + + /** + * Get the parameters used to invoke the model + */ + invocationParams( + options?: this["ParsedCallOptions"] + ): Omit { + return { + model: this.modelName, + temperature: this.temperature, + max_tokens: this.maxTokens, + top_p: this.topP, + frequency_penalty: this.frequencyPenalty, + presence_penalty: this.presencePenalty, + n: this.n, + best_of: this.bestOf, + logit_bias: this.logitBias, + stop: options?.stop ?? this.stop, + user: this.user, + stream: this.streaming, + ...this.modelKwargs, + }; + } + + /** @ignore */ + _identifyingParams(): Omit & { + model_name: string; + } & ClientOptions { + return { + model_name: this.modelName, + ...this.invocationParams(), + ...this.clientConfig, + }; + } + + /** + * Get the identifying parameters for the model + */ + identifyingParams(): Omit & { + model_name: string; + } & ClientOptions { + return this._identifyingParams(); + } + + /** + * Call out to OpenAI's endpoint with k unique prompts + * + * @param [prompts] - The prompts to pass into the model. + * @param [options] - Optional list of stop words to use when generating. + * @param [runManager] - Optional callback manager to use when generating. + * + * @returns The full LLM output. + * + * @example + * ```ts + * import { OpenAI } from "langchain/llms/openai"; + * const openai = new OpenAI(); + * const response = await openai.generate(["Tell me a joke."]); + * ``` + */ + async _generate( + prompts: string[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): Promise { + const subPrompts = chunkArray(prompts, this.batchSize); + const choices: OpenAIClient.CompletionChoice[] = []; + const tokenUsage: TokenUsage = {}; + + const params = this.invocationParams(options); + + if (params.max_tokens === -1) { + if (prompts.length !== 1) { + throw new Error( + "max_tokens set to -1 not supported for multiple inputs" + ); + } + params.max_tokens = await calculateMaxTokens({ + prompt: prompts[0], + // Cast here to allow for other models that may not fit the union + modelName: this.modelName as TiktokenModel, + }); + } + + for (let i = 0; i < subPrompts.length; i += 1) { + const data = params.stream + ? await (async () => { + const choices: OpenAIClient.CompletionChoice[] = []; + let response: Omit | undefined; + const stream = await this.completionWithRetry( + { + ...params, + stream: true, + prompt: subPrompts[i], + }, + options + ); + for await (const message of stream) { + // on the first message set the response properties + if (!response) { + response = { + id: message.id, + object: message.object, + created: message.created, + model: message.model, + }; + } + + // on all messages, update choice + for (const part of message.choices) { + if (!choices[part.index]) { + choices[part.index] = part; + } else { + const choice = choices[part.index]; + choice.text += part.text; + choice.finish_reason = part.finish_reason; + choice.logprobs = part.logprobs; + } + void runManager?.handleLLMNewToken(part.text, { + prompt: Math.floor(part.index / this.n), + completion: part.index % this.n, + }); + } + } + if (options.signal?.aborted) { + throw new Error("AbortError"); + } + return { ...response, choices }; + })() + : await this.completionWithRetry( + { + ...params, + stream: false, + prompt: subPrompts[i], + }, + { + signal: options.signal, + ...options.options, + } + ); + + choices.push(...data.choices); + const { + completion_tokens: completionTokens, + prompt_tokens: promptTokens, + total_tokens: totalTokens, + } = data.usage + ? data.usage + : { + completion_tokens: undefined, + prompt_tokens: undefined, + total_tokens: undefined, + }; + + if (completionTokens) { + tokenUsage.completionTokens = + (tokenUsage.completionTokens ?? 0) + completionTokens; + } + + if (promptTokens) { + tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens; + } + + if (totalTokens) { + tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens; + } + } + + const generations = chunkArray(choices, this.n).map((promptChoices) => + promptChoices.map((choice) => ({ + text: choice.text ?? "", + generationInfo: { + finishReason: choice.finish_reason, + logprobs: choice.logprobs, + }, + })) + ); + return { + generations, + llmOutput: { tokenUsage }, + }; + } + + // TODO(jacoblee): Refactor with _generate(..., {stream: true}) implementation? + async *_streamResponseChunks( + input: string, + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const params = { + ...this.invocationParams(options), + prompt: input, + stream: true as const, + }; + const stream = await this.completionWithRetry(params, options); + for await (const data of stream) { + const choice = data?.choices[0]; + if (!choice) { + continue; + } + const chunk = new GenerationChunk({ + text: choice.text, + generationInfo: { + finishReason: choice.finish_reason, + }, + }); + yield chunk; + // eslint-disable-next-line no-void + void runManager?.handleLLMNewToken(chunk.text ?? ""); + } + if (options.signal?.aborted) { + throw new Error("AbortError"); + } + } + + /** + * Calls the OpenAI API with retry logic in case of failures. + * @param request The request to send to the OpenAI API. + * @param options Optional configuration for the API call. + * @returns The response from the OpenAI API. + */ + async completionWithRetry( + request: OpenAIClient.CompletionCreateParamsStreaming, + options?: OpenAICoreRequestOptions + ): Promise>; + + async completionWithRetry( + request: OpenAIClient.CompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise; + + async completionWithRetry( + request: + | OpenAIClient.CompletionCreateParamsStreaming + | OpenAIClient.CompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise< + AsyncIterable | OpenAIClient.Completions.Completion + > { + const requestOptions = this._getClientOptions(options); + return this.caller.call(async () => { + try { + const res = await this.client.completions.create( + request, + requestOptions + ); + return res; + } catch (e) { + const error = wrapOpenAIClientError(e); + throw error; + } + }); + } + + /** + * Calls the OpenAI API with retry logic in case of failures. + * @param request The request to send to the OpenAI API. + * @param options Optional configuration for the API call. + * @returns The response from the OpenAI API. + */ + private _getClientOptions(options: OpenAICoreRequestOptions | undefined) { + if (!this.client) { + const openAIEndpointConfig: OpenAIEndpointConfig = { + azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName, + azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName, + azureOpenAIApiKey: this.azureOpenAIApiKey, + azureOpenAIBasePath: this.azureOpenAIBasePath, + baseURL: this.clientConfig.baseURL, + }; + + const endpoint = getEndpoint(openAIEndpointConfig); + + const params = { + ...this.clientConfig, + baseURL: endpoint, + timeout: this.timeout, + maxRetries: 0, + }; + + if (!params.baseURL) { + delete params.baseURL; + } + + this.client = new OpenAIClient(params); + } + const requestOptions = { + ...this.clientConfig, + ...options, + } as OpenAICoreRequestOptions; + if (this.azureOpenAIApiKey) { + requestOptions.headers = { + "api-key": this.azureOpenAIApiKey, + ...requestOptions.headers, + }; + requestOptions.query = { + "api-version": this.azureOpenAIApiVersion, + ...requestOptions.query, + }; + } + return requestOptions; + } + + _llmType() { + return "openai"; + } +} /** * PromptLayer wrapper to OpenAI diff --git a/langchain/src/load/import_type.d.ts b/langchain/src/load/import_type.d.ts index 23fa4c3304b6..ff96bdc124d3 100644 --- a/langchain/src/load/import_type.d.ts +++ b/langchain/src/load/import_type.d.ts @@ -493,10 +493,12 @@ export interface OptionalImportMap { } export interface SecretMap { + ANTHROPIC_API_KEY?: string; AWS_ACCESS_KEY_ID?: string; AWS_SECRETE_ACCESS_KEY?: string; AWS_SECRET_ACCESS_KEY?: string; AWS_SESSION_TOKEN?: string; + AZURE_OPENAI_API_KEY?: string; BAIDU_API_KEY?: string; BAIDU_SECRET_KEY?: string; BEDROCK_AWS_ACCESS_KEY_ID?: string; @@ -516,6 +518,7 @@ export interface SecretMap { MINIMAX_API_KEY?: string; MINIMAX_GROUP_ID?: string; OPENAI_API_KEY?: string; + OPENAI_ORGANIZATION?: string; PLANETSCALE_DATABASE_URL?: string; PLANETSCALE_HOST?: string; PLANETSCALE_PASSWORD?: string; diff --git a/langchain/src/schema/index.ts b/langchain/src/schema/index.ts index 14c4fc12f9c3..299068a67252 100644 --- a/langchain/src/schema/index.ts +++ b/langchain/src/schema/index.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import { BaseMessage, HumanMessage, diff --git a/langchain/src/tools/convert_to_openai.ts b/langchain/src/tools/convert_to_openai.ts index c53a7cf9bb5a..0c599a14e3b7 100644 --- a/langchain/src/tools/convert_to_openai.ts +++ b/langchain/src/tools/convert_to_openai.ts @@ -1,5 +1,47 @@ -export { - formatToOpenAIFunction, - formatToOpenAITool, - formatToOpenAIAssistantTool, -} from "@langchain/openai"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import type { OpenAI as OpenAIClient } from "openai"; + +import { StructuredTool } from "./base.js"; + +/** + * Formats a `StructuredTool` instance into a format that is compatible + * with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema` + * function to convert the schema of the `StructuredTool` into a JSON + * schema, which is then used as the parameters for the OpenAI function. + */ +export function formatToOpenAIFunction( + tool: StructuredTool +): OpenAIClient.Chat.ChatCompletionCreateParams.Function { + return { + name: tool.name, + description: tool.description, + parameters: zodToJsonSchema(tool.schema), + }; +} + +export function formatToOpenAITool( + tool: StructuredTool +): OpenAIClient.Chat.ChatCompletionTool { + const schema = zodToJsonSchema(tool.schema); + return { + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: schema, + }, + }; +} + +export function formatToOpenAIAssistantTool( + tool: StructuredTool +): OpenAIClient.Beta.AssistantCreateParams.AssistantToolsFunction { + return { + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: zodToJsonSchema(tool.schema), + }, + }; +} diff --git a/langchain/src/types/openai-types.ts b/langchain/src/types/openai-types.ts index 31b7fd48082e..f3df0278a6a9 100644 --- a/langchain/src/types/openai-types.ts +++ b/langchain/src/types/openai-types.ts @@ -1,14 +1,172 @@ +import type { OpenAI as OpenAIClient } from "openai"; + import { TiktokenModel } from "js-tiktoken/lite"; +import { BaseLanguageModelCallOptions } from "../base_language/index.js"; // reexport this type from the included package so we can easily override and extend it if needed in the future // also makes it easier for folks to import this type without digging around into the dependent packages export type { TiktokenModel }; -export type { - OpenAIBaseInput, - OpenAICoreRequestOptions, - OpenAICallOptions, - OpenAIInput, - LegacyOpenAIInput, - OpenAIChatInput, - AzureOpenAIInput, -} from "@langchain/openai"; + +export declare interface OpenAIBaseInput { + /** Sampling temperature to use */ + temperature: number; + + /** + * Maximum number of tokens to generate in the completion. -1 returns as many + * tokens as possible given the prompt and the model's maximum context size. + */ + maxTokens?: number; + + /** Total probability mass of tokens to consider at each step */ + topP: number; + + /** Penalizes repeated tokens according to frequency */ + frequencyPenalty: number; + + /** Penalizes repeated tokens */ + presencePenalty: number; + + /** Number of completions to generate for each prompt */ + n: number; + + /** Dictionary used to adjust the probability of specific tokens being generated */ + logitBias?: Record; + + /** Unique string identifier representing your end-user, which can help OpenAI to monitor and detect abuse. */ + user?: string; + + /** Whether to stream the results or not. Enabling disables tokenUsage reporting */ + streaming: boolean; + + /** Model name to use */ + modelName: string; + + /** Holds any additional parameters that are valid to pass to {@link + * https://platform.openai.com/docs/api-reference/completions/create | + * `openai.createCompletion`} that are not explicitly specified on this class. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + modelKwargs?: Record; + + /** List of stop words to use when generating */ + stop?: string[]; + + /** + * Timeout to use when making requests to OpenAI. + */ + timeout?: number; + + /** + * API key to use when making requests to OpenAI. Defaults to the value of + * `OPENAI_API_KEY` environment variable. + */ + openAIApiKey?: string; +} + +// TODO use OpenAI.Core.RequestOptions when SDK is updated to make it available +export type OpenAICoreRequestOptions< + Req extends object = Record +> = { + path?: string; + query?: Req | undefined; + body?: Req | undefined; + headers?: Record | undefined; + + maxRetries?: number; + stream?: boolean | undefined; + timeout?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + httpAgent?: any; + signal?: AbortSignal | undefined | null; + idempotencyKey?: string; +}; + +export interface OpenAICallOptions extends BaseLanguageModelCallOptions { + /** + * Additional options to pass to the underlying axios request. + */ + options?: OpenAICoreRequestOptions; +} + +/** + * Input to OpenAI class. + */ +export declare interface OpenAIInput extends OpenAIBaseInput { + /** Generates `bestOf` completions server side and returns the "best" */ + bestOf?: number; + + /** Batch size to use when passing multiple documents to generate */ + batchSize: number; +} + +/** + * @deprecated Use "baseURL", "defaultHeaders", and "defaultParams" instead. + */ +export interface LegacyOpenAIInput { + /** @deprecated Use baseURL instead */ + basePath?: string; + /** @deprecated Use defaultHeaders and defaultQuery instead */ + baseOptions?: { + headers?: Record; + params?: Record; + }; +} + +export interface OpenAIChatInput extends OpenAIBaseInput { + /** ChatGPT messages to pass as a prefix to the prompt */ + prefixMessages?: OpenAIClient.Chat.CreateChatCompletionRequestMessage[]; +} + +export declare interface AzureOpenAIInput { + /** + * API version to use when making requests to Azure OpenAI. + */ + azureOpenAIApiVersion?: string; + + /** + * API key to use when making requests to Azure OpenAI. + */ + azureOpenAIApiKey?: string; + + /** + * Azure OpenAI API instance name to use when making requests to Azure OpenAI. + * this is the name of the instance you created in the Azure portal. + * e.g. "my-openai-instance" + * this will be used in the endpoint URL: https://my-openai-instance.openai.azure.com/openai/deployments/{DeploymentName}/ + */ + azureOpenAIApiInstanceName?: string; + + /** + * Azure OpenAI API deployment name to use for completions when making requests to Azure OpenAI. + * This is the name of the deployment you created in the Azure portal. + * e.g. "my-openai-deployment" + * this will be used in the endpoint URL: https://{InstanceName}.openai.azure.com/openai/deployments/my-openai-deployment/ + */ + azureOpenAIApiDeploymentName?: string; + + /** + * Azure OpenAI API deployment name to use for embedding when making requests to Azure OpenAI. + * This is the name of the deployment you created in the Azure portal. + * This will fallback to azureOpenAIApiDeploymentName if not provided. + * e.g. "my-openai-deployment" + * this will be used in the endpoint URL: https://{InstanceName}.openai.azure.com/openai/deployments/my-openai-deployment/ + */ + azureOpenAIApiEmbeddingsDeploymentName?: string; + + /** + * Azure OpenAI API deployment name to use for completions when making requests to Azure OpenAI. + * Completions are only available for gpt-3.5-turbo and text-davinci-003 deployments. + * This is the name of the deployment you created in the Azure portal. + * This will fallback to azureOpenAIApiDeploymentName if not provided. + * e.g. "my-openai-deployment" + * this will be used in the endpoint URL: https://{InstanceName}.openai.azure.com/openai/deployments/my-openai-deployment/ + */ + azureOpenAIApiCompletionsDeploymentName?: string; + + /** + * Custom endpoint for Azure OpenAI API. This is useful in case you have a deployment in another region. + * e.g. setting this value to "https://westeurope.api.cognitive.microsoft.com/openai/deployments" + * will be result in the endpoint URL: https://westeurope.api.cognitive.microsoft.com/openai/deployments/{DeploymentName}/ + */ + azureOpenAIBasePath?: string; +} diff --git a/langchain/src/util/env.ts b/langchain/src/util/env.ts index 15639cb471a3..8fa1f10717e5 100644 --- a/langchain/src/util/env.ts +++ b/langchain/src/util/env.ts @@ -1,88 +1,11 @@ -// Inlined from https://github.com/flexdinesh/browser-or-node -declare global { - const Deno: - | { - version: { - deno: string; - }; - } - | undefined; -} - -export const isBrowser = () => - typeof window !== "undefined" && typeof window.document !== "undefined"; - -export const isWebWorker = () => - typeof globalThis === "object" && - globalThis.constructor && - globalThis.constructor.name === "DedicatedWorkerGlobalScope"; - -export const isJsDom = () => - (typeof window !== "undefined" && window.name === "nodejs") || - (typeof navigator !== "undefined" && - (navigator.userAgent.includes("Node.js") || - navigator.userAgent.includes("jsdom"))); - -// Supabase Edge Function provides a `Deno` global object -// without `version` property -export const isDeno = () => typeof Deno !== "undefined"; - -// Mark not-as-node if in Supabase Edge Function -export const isNode = () => - typeof process !== "undefined" && - typeof process.versions !== "undefined" && - typeof process.versions.node !== "undefined" && - !isDeno(); - -export const getEnv = () => { - let env: string; - if (isBrowser()) { - env = "browser"; - } else if (isNode()) { - env = "node"; - } else if (isWebWorker()) { - env = "webworker"; - } else if (isJsDom()) { - env = "jsdom"; - } else if (isDeno()) { - env = "deno"; - } else { - env = "other"; - } - - return env; -}; - -export type RuntimeEnvironment = { - library: string; - libraryVersion?: string; - runtime: string; - runtimeVersion?: string; -}; - -let runtimeEnvironment: RuntimeEnvironment | undefined; - -export async function getRuntimeEnvironment(): Promise { - if (runtimeEnvironment === undefined) { - const env = getEnv(); - - runtimeEnvironment = { - library: "langchain-js", - runtime: env, - }; - } - return runtimeEnvironment; -} - -export function getEnvironmentVariable(name: string): string | undefined { - // Certain Deno setups will throw an error if you try to access environment variables - // https://github.com/langchain-ai/langchainjs/issues/1412 - try { - return typeof process !== "undefined" - ? // eslint-disable-next-line no-process-env - process.env?.[name] - : undefined; - } catch (e) { - return undefined; - } -} +export { + isBrowser, + isWebWorker, + isJsDom, + isDeno, + isNode, + getEnv, + type RuntimeEnvironment, + getRuntimeEnvironment, + getEnvironmentVariable, +} from "@langchain/core/utils/env"; diff --git a/langchain/src/util/openai-format-fndef.ts b/langchain/src/util/openai-format-fndef.ts new file mode 100644 index 000000000000..3fe476b90554 --- /dev/null +++ b/langchain/src/util/openai-format-fndef.ts @@ -0,0 +1,135 @@ +/** + * Formatting function definitions for calculating openai function defination token usage. + * + * https://github.com/hmarr/openai-chat-tokens/blob/main/src/functions.ts + * (c) 2023 Harry Marr + * MIT license + */ +import OpenAI from "openai"; + +type OpenAIFunction = OpenAI.Chat.ChatCompletionCreateParams.Function; + +// Types representing the OpenAI function definitions. While the OpenAI client library +// does have types for function definitions, the properties are just Record, +// which isn't very useful for type checking this formatting code. +export interface FunctionDef extends Omit { + name: string; + description?: string; + parameters: ObjectProp; +} + +interface ObjectProp { + type: "object"; + properties?: { + [key: string]: Prop; + }; + required?: string[]; +} + +interface AnyOfProp { + anyOf: Prop[]; +} + +type Prop = { + description?: string; +} & ( + | AnyOfProp + | ObjectProp + | { + type: "string"; + enum?: string[]; + } + | { + type: "number" | "integer"; + minimum?: number; + maximum?: number; + enum?: number[]; + } + | { type: "boolean" } + | { type: "null" } + | { + type: "array"; + items?: Prop; + } +); + +function isAnyOfProp(prop: Prop): prop is AnyOfProp { + return ( + (prop as AnyOfProp).anyOf !== undefined && + Array.isArray((prop as AnyOfProp).anyOf) + ); +} + +// When OpenAI use functions in the prompt, they format them as TypeScript definitions rather than OpenAPI JSON schemas. +// This function converts the JSON schemas into TypeScript definitions. +export function formatFunctionDefinitions(functions: FunctionDef[]) { + const lines = ["namespace functions {", ""]; + for (const f of functions) { + if (f.description) { + lines.push(`// ${f.description}`); + } + if (Object.keys(f.parameters.properties ?? {}).length > 0) { + lines.push(`type ${f.name} = (_: {`); + lines.push(formatObjectProperties(f.parameters, 0)); + lines.push("}) => any;"); + } else { + lines.push(`type ${f.name} = () => any;`); + } + lines.push(""); + } + lines.push("} // namespace functions"); + return lines.join("\n"); +} + +// Format just the properties of an object (not including the surrounding braces) +function formatObjectProperties(obj: ObjectProp, indent: number): string { + const lines: string[] = []; + for (const [name, param] of Object.entries(obj.properties ?? {})) { + if (param.description && indent < 2) { + lines.push(`// ${param.description}`); + } + if (obj.required?.includes(name)) { + lines.push(`${name}: ${formatType(param, indent)},`); + } else { + lines.push(`${name}?: ${formatType(param, indent)},`); + } + } + return lines.map((line) => " ".repeat(indent) + line).join("\n"); +} + +// Format a single property type +function formatType(param: Prop, indent: number): string { + if (isAnyOfProp(param)) { + return param.anyOf.map((v) => formatType(v, indent)).join(" | "); + } + switch (param.type) { + case "string": + if (param.enum) { + return param.enum.map((v) => `"${v}"`).join(" | "); + } + return "string"; + case "number": + if (param.enum) { + return param.enum.map((v) => `${v}`).join(" | "); + } + return "number"; + case "integer": + if (param.enum) { + return param.enum.map((v) => `${v}`).join(" | "); + } + return "number"; + case "boolean": + return "boolean"; + case "null": + return "null"; + case "object": + return ["{", formatObjectProperties(param, indent + 2), "}"].join("\n"); + case "array": + if (param.items) { + return `${formatType(param.items, indent)}[]`; + } + return "any[]"; + default: + return ""; + } +} diff --git a/langchain/src/util/openai.ts b/langchain/src/util/openai.ts index a4464b94f1e1..db7fe66f032e 100644 --- a/langchain/src/util/openai.ts +++ b/langchain/src/util/openai.ts @@ -1 +1,16 @@ -export { wrapOpenAIClientError } from "@langchain/openai"; +import { APIConnectionTimeoutError, APIUserAbortError } from "openai"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapOpenAIClientError(e: any) { + let error; + if (e.constructor.name === APIConnectionTimeoutError.name) { + error = new Error(e.message); + error.name = "TimeoutError"; + } else if (e.constructor.name === APIUserAbortError.name) { + error = new Error(e.message); + error.name = "AbortError"; + } else { + error = e; + } + return error; +} diff --git a/langchain/src/util/prompt-layer.ts b/langchain/src/util/prompt-layer.ts index 207cf4bb2feb..4a18cf4bca4b 100644 --- a/langchain/src/util/prompt-layer.ts +++ b/langchain/src/util/prompt-layer.ts @@ -1,4 +1,4 @@ -import type { OpenAIClient } from "@langchain/openai"; +import type { OpenAI as OpenAIClient } from "openai"; import { AsyncCaller } from "../util/async_caller.js"; export const promptLayerTrackRequest = async ( diff --git a/yarn.lock b/yarn.lock index c4d7df0ebdf4..28f9c68d69b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -228,6 +228,23 @@ __metadata: languageName: node linkType: hard +"@anthropic-ai/sdk@npm:^0.9.1": + version: 0.9.1 + resolution: "@anthropic-ai/sdk@npm:0.9.1" + dependencies: + "@types/node": ^18.11.18 + "@types/node-fetch": ^2.6.4 + abort-controller: ^3.0.0 + agentkeepalive: ^4.2.1 + digest-fetch: ^1.3.0 + form-data-encoder: 1.7.2 + formdata-node: ^4.3.2 + node-fetch: ^2.6.7 + web-streams-polyfill: ^3.2.1 + checksum: 0ec50abc0ffc694d903d516f4ac110aafa3a588438791851dd2c3d220da49ceaf2723e218b7f1bc13e737fee1561b18ad8c3b4cc4347e8212131f69637216413 + languageName: node + linkType: hard + "@apache-arrow/ts@npm:^12.0.0": version: 12.0.0 resolution: "@apache-arrow/ts@npm:12.0.0" @@ -7939,7 +7956,7 @@ __metadata: languageName: node linkType: hard -"@langchain/anthropic@workspace:*, @langchain/anthropic@workspace:libs/langchain-anthropic": +"@langchain/anthropic@workspace:libs/langchain-anthropic": version: 0.0.0-use.local resolution: "@langchain/anthropic@workspace:libs/langchain-anthropic" dependencies: @@ -7996,7 +8013,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/openai@workspace:*, @langchain/openai@workspace:libs/langchain-openai": +"@langchain/openai@workspace:libs/langchain-openai": version: 0.0.0-use.local resolution: "@langchain/openai@workspace:libs/langchain-openai" dependencies: @@ -22291,6 +22308,7 @@ __metadata: version: 0.0.0-use.local resolution: "langchain@workspace:langchain" dependencies: + "@anthropic-ai/sdk": ^0.9.1 "@aws-crypto/sha256-js": ^5.0.0 "@aws-sdk/client-bedrock-runtime": ^3.422.0 "@aws-sdk/client-dynamodb": ^3.310.0 @@ -22315,9 +22333,7 @@ __metadata: "@google-cloud/storage": ^6.10.1 "@huggingface/inference": ^2.6.4 "@jest/globals": ^29.5.0 - "@langchain/anthropic": "workspace:*" "@langchain/core": "workspace:*" - "@langchain/openai": "workspace:*" "@mozilla/readability": ^0.4.4 "@notionhq/client": ^2.2.10 "@opensearch-project/opensearch": ^2.2.0 @@ -22413,6 +22429,7 @@ __metadata: neo4j-driver: ^5.12.0 node-llama-cpp: 2.7.3 notion-to-md: ^3.1.0 + openai: ^4.19.0 openapi-types: ^12.1.3 p-retry: 4 pdf-parse: 1.1.1