openai.cjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.wrapOpenAI = void 0;
  4. const traceable_js_1 = require("../traceable.cjs");
  5. function _combineChatCompletionChoices(choices
  6. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  7. ) {
  8. const reversedChoices = choices.slice().reverse();
  9. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  10. const message = {
  11. role: "assistant",
  12. content: "",
  13. };
  14. for (const c of reversedChoices) {
  15. if (c.delta.role) {
  16. message["role"] = c.delta.role;
  17. break;
  18. }
  19. }
  20. const toolCalls = {};
  21. for (const c of choices) {
  22. if (c.delta.content) {
  23. message.content = message.content.concat(c.delta.content);
  24. }
  25. if (c.delta.function_call) {
  26. if (!message.function_call) {
  27. message.function_call = { name: "", arguments: "" };
  28. }
  29. if (c.delta.function_call.name) {
  30. message.function_call.name += c.delta.function_call.name;
  31. }
  32. if (c.delta.function_call.arguments) {
  33. message.function_call.arguments += c.delta.function_call.arguments;
  34. }
  35. }
  36. if (c.delta.tool_calls) {
  37. for (const tool_call of c.delta.tool_calls) {
  38. if (!toolCalls[c.index]) {
  39. toolCalls[c.index] = [];
  40. }
  41. toolCalls[c.index].push(tool_call);
  42. }
  43. }
  44. }
  45. if (Object.keys(toolCalls).length > 0) {
  46. message.tool_calls = [...Array(Object.keys(toolCalls).length)];
  47. for (const [index, toolCallChunks] of Object.entries(toolCalls)) {
  48. const idx = parseInt(index);
  49. message.tool_calls[idx] = {
  50. index: idx,
  51. id: toolCallChunks.find((c) => c.id)?.id || null,
  52. type: toolCallChunks.find((c) => c.type)?.type || null,
  53. };
  54. for (const chunk of toolCallChunks) {
  55. if (chunk.function) {
  56. if (!message.tool_calls[idx].function) {
  57. message.tool_calls[idx].function = {
  58. name: "",
  59. arguments: "",
  60. };
  61. }
  62. if (chunk.function.name) {
  63. message.tool_calls[idx].function.name += chunk.function.name;
  64. }
  65. if (chunk.function.arguments) {
  66. message.tool_calls[idx].function.arguments +=
  67. chunk.function.arguments;
  68. }
  69. }
  70. }
  71. }
  72. }
  73. return {
  74. index: choices[0].index,
  75. finish_reason: reversedChoices.find((c) => c.finish_reason) || null,
  76. message: message,
  77. };
  78. }
  79. const chatAggregator = (chunks) => {
  80. if (!chunks || chunks.length === 0) {
  81. return { choices: [{ message: { role: "assistant", content: "" } }] };
  82. }
  83. const choicesByIndex = {};
  84. for (const chunk of chunks) {
  85. for (const choice of chunk.choices) {
  86. if (choicesByIndex[choice.index] === undefined) {
  87. choicesByIndex[choice.index] = [];
  88. }
  89. choicesByIndex[choice.index].push(choice);
  90. }
  91. }
  92. const aggregatedOutput = chunks[chunks.length - 1];
  93. aggregatedOutput.choices = Object.values(choicesByIndex).map((choices) => _combineChatCompletionChoices(choices));
  94. return aggregatedOutput;
  95. };
  96. const textAggregator = (allChunks
  97. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  98. ) => {
  99. if (allChunks.length === 0) {
  100. return { choices: [{ text: "" }] };
  101. }
  102. const allContent = [];
  103. for (const chunk of allChunks) {
  104. const content = chunk.choices[0].text;
  105. if (content != null) {
  106. allContent.push(content);
  107. }
  108. }
  109. const content = allContent.join("");
  110. const aggregatedOutput = allChunks[allChunks.length - 1];
  111. aggregatedOutput.choices = [
  112. { ...aggregatedOutput.choices[0], text: content },
  113. ];
  114. return aggregatedOutput;
  115. };
  116. function processChatCompletion(outputs) {
  117. const chatCompletion = outputs;
  118. // copy the original object, minus usage
  119. const result = { ...chatCompletion };
  120. const usage = chatCompletion.usage;
  121. if (usage) {
  122. const inputTokenDetails = {
  123. ...(usage.prompt_tokens_details?.audio_tokens !== null && {
  124. audio: usage.prompt_tokens_details?.audio_tokens,
  125. }),
  126. ...(usage.prompt_tokens_details?.cached_tokens !== null && {
  127. cache_read: usage.prompt_tokens_details?.cached_tokens,
  128. }),
  129. };
  130. const outputTokenDetails = {
  131. ...(usage.completion_tokens_details?.audio_tokens !== null && {
  132. audio: usage.completion_tokens_details?.audio_tokens,
  133. }),
  134. ...(usage.completion_tokens_details?.reasoning_tokens !== null && {
  135. reasoning: usage.completion_tokens_details?.reasoning_tokens,
  136. }),
  137. };
  138. result.usage_metadata = {
  139. input_tokens: usage.prompt_tokens ?? 0,
  140. output_tokens: usage.completion_tokens ?? 0,
  141. total_tokens: usage.total_tokens ?? 0,
  142. ...(Object.keys(inputTokenDetails).length > 0 && {
  143. input_token_details: inputTokenDetails,
  144. }),
  145. ...(Object.keys(outputTokenDetails).length > 0 && {
  146. output_token_details: outputTokenDetails,
  147. }),
  148. };
  149. }
  150. delete result.usage;
  151. return result;
  152. }
  153. /**
  154. * Wraps an OpenAI client's completion methods, enabling automatic LangSmith
  155. * tracing. Method signatures are unchanged, with the exception that you can pass
  156. * an additional and optional "langsmithExtra" field within the second parameter.
  157. * @param openai An OpenAI client instance.
  158. * @param options LangSmith options.
  159. * @example
  160. * ```ts
  161. * import { OpenAI } from "openai";
  162. * import { wrapOpenAI } from "langsmith/wrappers/openai";
  163. *
  164. * const patchedClient = wrapOpenAI(new OpenAI());
  165. *
  166. * const patchedStream = await patchedClient.chat.completions.create(
  167. * {
  168. * messages: [{ role: "user", content: `Say 'foo'` }],
  169. * model: "gpt-4.1-mini",
  170. * stream: true,
  171. * },
  172. * {
  173. * langsmithExtra: {
  174. * metadata: {
  175. * additional_data: "bar",
  176. * },
  177. * },
  178. * },
  179. * );
  180. * ```
  181. */
  182. const wrapOpenAI = (openai, options) => {
  183. if ((0, traceable_js_1.isTraceableFunction)(openai.chat.completions.create) ||
  184. (0, traceable_js_1.isTraceableFunction)(openai.completions.create)) {
  185. throw new Error("This instance of OpenAI client has been already wrapped once.");
  186. }
  187. // Some internal OpenAI methods call each other, so we need to preserve original
  188. // OpenAI methods.
  189. const tracedOpenAIClient = { ...openai };
  190. if (openai.beta &&
  191. openai.beta.chat &&
  192. openai.beta.chat.completions &&
  193. typeof openai.beta.chat.completions.parse === "function") {
  194. tracedOpenAIClient.beta = {
  195. ...openai.beta,
  196. chat: {
  197. ...openai.beta.chat,
  198. completions: {
  199. ...openai.beta.chat.completions,
  200. parse: (0, traceable_js_1.traceable)(openai.beta.chat.completions.parse.bind(openai.beta.chat.completions), {
  201. name: "ChatOpenAI",
  202. run_type: "llm",
  203. aggregator: chatAggregator,
  204. argsConfigPath: [1, "langsmithExtra"],
  205. getInvocationParams: (payload) => {
  206. if (typeof payload !== "object" || payload == null)
  207. return undefined;
  208. // we can safely do so, as the types are not exported in TSC
  209. const params = payload;
  210. const ls_stop = (typeof params.stop === "string"
  211. ? [params.stop]
  212. : params.stop) ?? undefined;
  213. return {
  214. ls_provider: "openai",
  215. ls_model_type: "chat",
  216. ls_model_name: params.model,
  217. ls_max_tokens: params.max_tokens ?? undefined,
  218. ls_temperature: params.temperature ?? undefined,
  219. ls_stop,
  220. };
  221. },
  222. ...options,
  223. }),
  224. },
  225. },
  226. };
  227. }
  228. tracedOpenAIClient.chat = {
  229. ...openai.chat,
  230. completions: {
  231. ...openai.chat.completions,
  232. create: (0, traceable_js_1.traceable)(openai.chat.completions.create.bind(openai.chat.completions), {
  233. name: "ChatOpenAI",
  234. run_type: "llm",
  235. aggregator: chatAggregator,
  236. argsConfigPath: [1, "langsmithExtra"],
  237. getInvocationParams: (payload) => {
  238. if (typeof payload !== "object" || payload == null)
  239. return undefined;
  240. // we can safely do so, as the types are not exported in TSC
  241. const params = payload;
  242. const ls_stop = (typeof params.stop === "string" ? [params.stop] : params.stop) ??
  243. undefined;
  244. return {
  245. ls_provider: "openai",
  246. ls_model_type: "chat",
  247. ls_model_name: params.model,
  248. ls_max_tokens: params.max_tokens ?? undefined,
  249. ls_temperature: params.temperature ?? undefined,
  250. ls_stop,
  251. };
  252. },
  253. processOutputs: processChatCompletion,
  254. ...options,
  255. }),
  256. },
  257. };
  258. tracedOpenAIClient.completions = {
  259. ...openai.completions,
  260. create: (0, traceable_js_1.traceable)(openai.completions.create.bind(openai.completions), {
  261. name: "OpenAI",
  262. run_type: "llm",
  263. aggregator: textAggregator,
  264. argsConfigPath: [1, "langsmithExtra"],
  265. getInvocationParams: (payload) => {
  266. if (typeof payload !== "object" || payload == null)
  267. return undefined;
  268. // we can safely do so, as the types are not exported in TSC
  269. const params = payload;
  270. const ls_stop = (typeof params.stop === "string" ? [params.stop] : params.stop) ??
  271. undefined;
  272. return {
  273. ls_provider: "openai",
  274. ls_model_type: "llm",
  275. ls_model_name: params.model,
  276. ls_max_tokens: params.max_tokens ?? undefined,
  277. ls_temperature: params.temperature ?? undefined,
  278. ls_stop,
  279. };
  280. },
  281. ...options,
  282. }),
  283. };
  284. return tracedOpenAIClient;
  285. };
  286. exports.wrapOpenAI = wrapOpenAI;