@langchain/openai 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,7 +116,12 @@ class AzureOpenAIEmbeddings extends embeddings_js_1.OpenAIEmbeddings {
116
116
  }
117
117
  return this.caller.call(async () => {
118
118
  try {
119
- const res = await this.client.embeddings.create(request, requestOptions);
119
+ const res = await this.client.embeddings.create(request,
120
+ // This unknown cast is required because OpenAI types seem to be incorrect here
121
+ // without it, we can't pass arbitrary keys via the `query` field in the options,
122
+ // which means we can't specify the `api-version` as required by Azure OpenAI.
123
+ // See https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning
124
+ requestOptions);
120
125
  return res;
121
126
  }
122
127
  catch (e) {
@@ -113,7 +113,12 @@ export class AzureOpenAIEmbeddings extends OpenAIEmbeddings {
113
113
  }
114
114
  return this.caller.call(async () => {
115
115
  try {
116
- const res = await this.client.embeddings.create(request, requestOptions);
116
+ const res = await this.client.embeddings.create(request,
117
+ // This unknown cast is required because OpenAI types seem to be incorrect here
118
+ // without it, we can't pass arbitrary keys via the `query` field in the options,
119
+ // which means we can't specify the `api-version` as required by Azure OpenAI.
120
+ // See https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning
121
+ requestOptions);
117
122
  return res;
118
123
  }
119
124
  catch (e) {
@@ -50,6 +50,132 @@ function messageToOpenAIRole(message) {
50
50
  }
51
51
  }
52
52
  exports.messageToOpenAIRole = messageToOpenAIRole;
53
+ const completionsApiContentBlockConverter = {
54
+ providerName: "ChatOpenAI",
55
+ fromStandardTextBlock(block) {
56
+ return { type: "text", text: block.text };
57
+ },
58
+ fromStandardImageBlock(block) {
59
+ if (block.source_type === "url") {
60
+ return {
61
+ type: "image_url",
62
+ image_url: {
63
+ url: block.url,
64
+ ...(block.metadata?.detail
65
+ ? { detail: block.metadata.detail }
66
+ : {}),
67
+ },
68
+ };
69
+ }
70
+ if (block.source_type === "base64") {
71
+ const url = `data:${block.mime_type ?? ""};base64,${block.data}`;
72
+ return {
73
+ type: "image_url",
74
+ image_url: {
75
+ url,
76
+ ...(block.metadata?.detail
77
+ ? { detail: block.metadata.detail }
78
+ : {}),
79
+ },
80
+ };
81
+ }
82
+ throw new Error(`Image content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
83
+ },
84
+ fromStandardAudioBlock(block) {
85
+ if (block.source_type === "url") {
86
+ const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url });
87
+ if (!data) {
88
+ throw new Error(`URL audio blocks with source_type ${block.source_type} must be formatted as a data URL for ChatOpenAI`);
89
+ }
90
+ const rawMimeType = data.mime_type || block.mime_type || "";
91
+ let mimeType;
92
+ try {
93
+ mimeType = (0, messages_1.parseMimeType)(rawMimeType);
94
+ }
95
+ catch {
96
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
97
+ }
98
+ if (mimeType.type !== "audio" ||
99
+ (mimeType.subtype !== "wav" && mimeType.subtype !== "mp3")) {
100
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
101
+ }
102
+ return {
103
+ type: "input_audio",
104
+ input_audio: {
105
+ format: mimeType.subtype,
106
+ data: data.data,
107
+ },
108
+ };
109
+ }
110
+ if (block.source_type === "base64") {
111
+ let mimeType;
112
+ try {
113
+ mimeType = (0, messages_1.parseMimeType)(block.mime_type ?? "");
114
+ }
115
+ catch {
116
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
117
+ }
118
+ if (mimeType.type !== "audio" ||
119
+ (mimeType.subtype !== "wav" && mimeType.subtype !== "mp3")) {
120
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
121
+ }
122
+ return {
123
+ type: "input_audio",
124
+ input_audio: {
125
+ format: mimeType.subtype,
126
+ data: block.data,
127
+ },
128
+ };
129
+ }
130
+ throw new Error(`Audio content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
131
+ },
132
+ fromStandardFileBlock(block) {
133
+ if (block.source_type === "url") {
134
+ const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url });
135
+ if (!data) {
136
+ throw new Error(`URL file blocks with source_type ${block.source_type} must be formatted as a data URL for ChatOpenAI`);
137
+ }
138
+ return {
139
+ type: "file",
140
+ file: {
141
+ file_data: block.url,
142
+ ...(block.metadata?.filename || block.metadata?.name
143
+ ? {
144
+ filename: (block.metadata?.filename ||
145
+ block.metadata?.name),
146
+ }
147
+ : {}),
148
+ },
149
+ };
150
+ }
151
+ if (block.source_type === "base64") {
152
+ return {
153
+ type: "file",
154
+ file: {
155
+ file_data: `data:${block.mime_type ?? ""};base64,${block.data}`,
156
+ ...(block.metadata?.filename ||
157
+ block.metadata?.name ||
158
+ block.metadata?.title
159
+ ? {
160
+ filename: (block.metadata?.filename ||
161
+ block.metadata?.name ||
162
+ block.metadata?.title),
163
+ }
164
+ : {}),
165
+ },
166
+ };
167
+ }
168
+ if (block.source_type === "id") {
169
+ return {
170
+ type: "file",
171
+ file: {
172
+ file_id: block.id,
173
+ },
174
+ };
175
+ }
176
+ throw new Error(`File content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
177
+ },
178
+ };
53
179
  // Used in LangSmith, export is important here
54
180
  function _convertMessagesToOpenAIParams(messages, model) {
55
181
  // TODO: Function messages do not support array content, fix cast
@@ -58,10 +184,18 @@ function _convertMessagesToOpenAIParams(messages, model) {
58
184
  if (role === "system" && isReasoningModel(model)) {
59
185
  role = "developer";
60
186
  }
187
+ const content = typeof message.content === "string"
188
+ ? message.content
189
+ : message.content.map((m) => {
190
+ if ((0, messages_1.isDataContentBlock)(m)) {
191
+ return (0, messages_1.convertToProviderContentBlock)(m, completionsApiContentBlockConverter);
192
+ }
193
+ return m;
194
+ });
61
195
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
196
  const completionParam = {
63
197
  role,
64
- content: message.content,
198
+ content,
65
199
  };
66
200
  if (message.name != null) {
67
201
  completionParam.name = message.name;
@@ -201,7 +335,6 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
201
335
  name: toolCall.name,
202
336
  arguments: JSON.stringify(toolCall.args),
203
337
  call_id: toolCall.id,
204
- // @ts-expect-error Might come from a non-Responses API message
205
338
  id: functionCallIds?.[toolCall.id],
206
339
  })));
207
340
  }
@@ -210,7 +343,6 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
210
343
  type: "function_call",
211
344
  name: toolCall.function.name,
212
345
  call_id: toolCall.id,
213
- // @ts-expect-error Might come from a non-Responses API message
214
346
  id: functionCallIds?.[toolCall.id],
215
347
  arguments: toolCall.function.arguments,
216
348
  })));
@@ -234,6 +366,9 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
234
366
  const content = typeof lcMsg.content === "string"
235
367
  ? lcMsg.content
236
368
  : lcMsg.content.flatMap((item) => {
369
+ if ((0, messages_1.isDataContentBlock)(item)) {
370
+ return (0, messages_1.convertToProviderContentBlock)(item, completionsApiContentBlockConverter);
371
+ }
237
372
  if (item.type === "text") {
238
373
  return { type: "input_text", text: item.text };
239
374
  }
@@ -322,7 +457,9 @@ function _convertOpenAIResponsesMessageToBaseMessage(response) {
322
457
  invalid_tool_calls.push((0, openai_tools_1.makeInvalidToolCall)(fnAdapter, errMessage));
323
458
  }
324
459
  additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY] ??= {};
325
- additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][item.call_id] = item.id;
460
+ if (item.id) {
461
+ additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][item.call_id] = item.id;
462
+ }
326
463
  }
327
464
  else if (item.type === "reasoning") {
328
465
  additional_kwargs.reasoning = item;
@@ -466,7 +603,9 @@ function _convertChatOpenAIToolTypeToOpenAITool(tool, fields) {
466
603
  return (0, tools_js_1._convertToOpenAITool)(tool, fields);
467
604
  }
468
605
  function isReasoningModel(model) {
469
- return model?.startsWith("o1") || model?.startsWith("o3");
606
+ return (model?.startsWith("o1") ||
607
+ model?.startsWith("o3") ||
608
+ model?.startsWith("o4"));
470
609
  }
471
610
  /**
472
611
  * OpenAI chat model integration.
@@ -1,5 +1,5 @@
1
1
  import { OpenAI as OpenAIClient } from "openai";
2
- import { AIMessage, AIMessageChunk, ChatMessage, ChatMessageChunk, FunctionMessageChunk, HumanMessageChunk, SystemMessageChunk, ToolMessageChunk, isAIMessage, } from "@langchain/core/messages";
2
+ import { AIMessage, AIMessageChunk, ChatMessage, ChatMessageChunk, FunctionMessageChunk, HumanMessageChunk, SystemMessageChunk, ToolMessageChunk, isAIMessage, parseBase64DataUrl, parseMimeType, convertToProviderContentBlock, isDataContentBlock, } from "@langchain/core/messages";
3
3
  import { ChatGenerationChunk, } from "@langchain/core/outputs";
4
4
  import { getEnvironmentVariable } from "@langchain/core/utils/env";
5
5
  import { BaseChatModel, } from "@langchain/core/language_models/chat_models";
@@ -46,6 +46,132 @@ export function messageToOpenAIRole(message) {
46
46
  throw new Error(`Unknown message type: ${type}`);
47
47
  }
48
48
  }
49
+ const completionsApiContentBlockConverter = {
50
+ providerName: "ChatOpenAI",
51
+ fromStandardTextBlock(block) {
52
+ return { type: "text", text: block.text };
53
+ },
54
+ fromStandardImageBlock(block) {
55
+ if (block.source_type === "url") {
56
+ return {
57
+ type: "image_url",
58
+ image_url: {
59
+ url: block.url,
60
+ ...(block.metadata?.detail
61
+ ? { detail: block.metadata.detail }
62
+ : {}),
63
+ },
64
+ };
65
+ }
66
+ if (block.source_type === "base64") {
67
+ const url = `data:${block.mime_type ?? ""};base64,${block.data}`;
68
+ return {
69
+ type: "image_url",
70
+ image_url: {
71
+ url,
72
+ ...(block.metadata?.detail
73
+ ? { detail: block.metadata.detail }
74
+ : {}),
75
+ },
76
+ };
77
+ }
78
+ throw new Error(`Image content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
79
+ },
80
+ fromStandardAudioBlock(block) {
81
+ if (block.source_type === "url") {
82
+ const data = parseBase64DataUrl({ dataUrl: block.url });
83
+ if (!data) {
84
+ throw new Error(`URL audio blocks with source_type ${block.source_type} must be formatted as a data URL for ChatOpenAI`);
85
+ }
86
+ const rawMimeType = data.mime_type || block.mime_type || "";
87
+ let mimeType;
88
+ try {
89
+ mimeType = parseMimeType(rawMimeType);
90
+ }
91
+ catch {
92
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
93
+ }
94
+ if (mimeType.type !== "audio" ||
95
+ (mimeType.subtype !== "wav" && mimeType.subtype !== "mp3")) {
96
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
97
+ }
98
+ return {
99
+ type: "input_audio",
100
+ input_audio: {
101
+ format: mimeType.subtype,
102
+ data: data.data,
103
+ },
104
+ };
105
+ }
106
+ if (block.source_type === "base64") {
107
+ let mimeType;
108
+ try {
109
+ mimeType = parseMimeType(block.mime_type ?? "");
110
+ }
111
+ catch {
112
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
113
+ }
114
+ if (mimeType.type !== "audio" ||
115
+ (mimeType.subtype !== "wav" && mimeType.subtype !== "mp3")) {
116
+ throw new Error(`Audio blocks with source_type ${block.source_type} must have mime type of audio/wav or audio/mp3`);
117
+ }
118
+ return {
119
+ type: "input_audio",
120
+ input_audio: {
121
+ format: mimeType.subtype,
122
+ data: block.data,
123
+ },
124
+ };
125
+ }
126
+ throw new Error(`Audio content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
127
+ },
128
+ fromStandardFileBlock(block) {
129
+ if (block.source_type === "url") {
130
+ const data = parseBase64DataUrl({ dataUrl: block.url });
131
+ if (!data) {
132
+ throw new Error(`URL file blocks with source_type ${block.source_type} must be formatted as a data URL for ChatOpenAI`);
133
+ }
134
+ return {
135
+ type: "file",
136
+ file: {
137
+ file_data: block.url,
138
+ ...(block.metadata?.filename || block.metadata?.name
139
+ ? {
140
+ filename: (block.metadata?.filename ||
141
+ block.metadata?.name),
142
+ }
143
+ : {}),
144
+ },
145
+ };
146
+ }
147
+ if (block.source_type === "base64") {
148
+ return {
149
+ type: "file",
150
+ file: {
151
+ file_data: `data:${block.mime_type ?? ""};base64,${block.data}`,
152
+ ...(block.metadata?.filename ||
153
+ block.metadata?.name ||
154
+ block.metadata?.title
155
+ ? {
156
+ filename: (block.metadata?.filename ||
157
+ block.metadata?.name ||
158
+ block.metadata?.title),
159
+ }
160
+ : {}),
161
+ },
162
+ };
163
+ }
164
+ if (block.source_type === "id") {
165
+ return {
166
+ type: "file",
167
+ file: {
168
+ file_id: block.id,
169
+ },
170
+ };
171
+ }
172
+ throw new Error(`File content blocks with source_type ${block.source_type} are not supported for ChatOpenAI`);
173
+ },
174
+ };
49
175
  // Used in LangSmith, export is important here
50
176
  export function _convertMessagesToOpenAIParams(messages, model) {
51
177
  // TODO: Function messages do not support array content, fix cast
@@ -54,10 +180,18 @@ export function _convertMessagesToOpenAIParams(messages, model) {
54
180
  if (role === "system" && isReasoningModel(model)) {
55
181
  role = "developer";
56
182
  }
183
+ const content = typeof message.content === "string"
184
+ ? message.content
185
+ : message.content.map((m) => {
186
+ if (isDataContentBlock(m)) {
187
+ return convertToProviderContentBlock(m, completionsApiContentBlockConverter);
188
+ }
189
+ return m;
190
+ });
57
191
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
192
  const completionParam = {
59
193
  role,
60
- content: message.content,
194
+ content,
61
195
  };
62
196
  if (message.name != null) {
63
197
  completionParam.name = message.name;
@@ -196,7 +330,6 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
196
330
  name: toolCall.name,
197
331
  arguments: JSON.stringify(toolCall.args),
198
332
  call_id: toolCall.id,
199
- // @ts-expect-error Might come from a non-Responses API message
200
333
  id: functionCallIds?.[toolCall.id],
201
334
  })));
202
335
  }
@@ -205,7 +338,6 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
205
338
  type: "function_call",
206
339
  name: toolCall.function.name,
207
340
  call_id: toolCall.id,
208
- // @ts-expect-error Might come from a non-Responses API message
209
341
  id: functionCallIds?.[toolCall.id],
210
342
  arguments: toolCall.function.arguments,
211
343
  })));
@@ -229,6 +361,9 @@ function _convertMessagesToOpenAIResponsesParams(messages, model) {
229
361
  const content = typeof lcMsg.content === "string"
230
362
  ? lcMsg.content
231
363
  : lcMsg.content.flatMap((item) => {
364
+ if (isDataContentBlock(item)) {
365
+ return convertToProviderContentBlock(item, completionsApiContentBlockConverter);
366
+ }
232
367
  if (item.type === "text") {
233
368
  return { type: "input_text", text: item.text };
234
369
  }
@@ -317,7 +452,9 @@ function _convertOpenAIResponsesMessageToBaseMessage(response) {
317
452
  invalid_tool_calls.push(makeInvalidToolCall(fnAdapter, errMessage));
318
453
  }
319
454
  additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY] ??= {};
320
- additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][item.call_id] = item.id;
455
+ if (item.id) {
456
+ additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][item.call_id] = item.id;
457
+ }
321
458
  }
322
459
  else if (item.type === "reasoning") {
323
460
  additional_kwargs.reasoning = item;
@@ -461,7 +598,9 @@ function _convertChatOpenAIToolTypeToOpenAITool(tool, fields) {
461
598
  return _convertToOpenAITool(tool, fields);
462
599
  }
463
600
  function isReasoningModel(model) {
464
- return model?.startsWith("o1") || model?.startsWith("o3");
601
+ return (model?.startsWith("o1") ||
602
+ model?.startsWith("o3") ||
603
+ model?.startsWith("o4"));
465
604
  }
466
605
  /**
467
606
  * OpenAI chat model integration.
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OpenAI as OpenAIClient } from "openai";
2
+ import type { RequestOptions as _OpenAICoreRequestOptions } from "openai/core";
2
3
  import type { ResponseFormatText, ResponseFormatJSONObject, ResponseFormatJSONSchema } from "openai/resources/shared";
3
4
  import { TiktokenModel } from "js-tiktoken/lite";
4
5
  import type { BaseLanguageModelCallOptions } from "@langchain/core/language_models/base";
@@ -73,18 +74,7 @@ export declare interface OpenAIBaseInput {
73
74
  */
74
75
  apiKey?: string;
75
76
  }
76
- export type OpenAICoreRequestOptions<Req extends object = Record<string, unknown>> = {
77
- path?: string;
78
- query?: Req | undefined;
79
- body?: Req | undefined;
80
- headers?: Record<string, string | null | undefined> | undefined;
81
- maxRetries?: number;
82
- stream?: boolean | undefined;
83
- timeout?: number;
84
- httpAgent?: any;
85
- signal?: AbortSignal | undefined | null;
86
- idempotencyKey?: string;
87
- };
77
+ export type OpenAICoreRequestOptions<Req = Record<string, unknown>> = _OpenAICoreRequestOptions<Req>;
88
78
  export interface OpenAICallOptions extends BaseLanguageModelCallOptions {
89
79
  /**
90
80
  * Additional options to pass to the underlying axios request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/openai",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "OpenAI integrations for LangChain.js",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,12 +36,12 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
38
  "js-tiktoken": "^1.0.12",
39
- "openai": "^4.87.3",
39
+ "openai": "^4.93.0",
40
40
  "zod": "^3.22.4",
41
41
  "zod-to-json-schema": "^3.22.3"
42
42
  },
43
43
  "peerDependencies": {
44
- "@langchain/core": ">=0.3.39 <0.4.0"
44
+ "@langchain/core": ">=0.3.48 <0.4.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@azure/identity": "^4.2.1",
@@ -62,7 +62,7 @@
62
62
  "jest": "^29.5.0",
63
63
  "jest-environment-node": "^29.6.4",
64
64
  "prettier": "^2.8.3",
65
- "release-it": "^17.6.0",
65
+ "release-it": "^18.1.2",
66
66
  "rimraf": "^5.0.1",
67
67
  "ts-jest": "^29.1.0",
68
68
  "typescript": "~5.1.6"