vercel.cjs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.AISDKExporter = void 0;
  4. const index_js_1 = require("./index.cjs");
  5. const uuid_1 = require("uuid");
  6. const traceable_js_1 = require("./singletons/traceable.cjs");
  7. const env_js_1 = require("./utils/env.cjs");
  8. const env_js_2 = require("./env.cjs");
  9. // Attempt to convert CoreMessage to a LangChain-compatible format
  10. // which allows us to render messages more nicely in LangSmith
  11. function convertCoreToSmith(message) {
  12. if (message.role === "assistant") {
  13. const data = { content: message.content };
  14. if (Array.isArray(message.content)) {
  15. data.content = message.content.map((part) => {
  16. if (part.type === "text") {
  17. return {
  18. type: "text",
  19. text: part.text,
  20. ...part.experimental_providerMetadata,
  21. };
  22. }
  23. if (part.type === "tool-call") {
  24. return {
  25. type: "tool_use",
  26. name: part.toolName,
  27. id: part.toolCallId,
  28. input: part.args,
  29. ...part.experimental_providerMetadata,
  30. };
  31. }
  32. return part;
  33. });
  34. const toolCalls = message.content.filter((part) => part.type === "tool-call");
  35. if (toolCalls.length > 0) {
  36. data.additional_kwargs ??= {};
  37. data.additional_kwargs.tool_calls = toolCalls.map((part) => {
  38. return {
  39. id: part.toolCallId,
  40. type: "function",
  41. function: {
  42. name: part.toolName,
  43. id: part.toolCallId,
  44. arguments: JSON.stringify(part.args),
  45. },
  46. };
  47. });
  48. }
  49. }
  50. return { type: "ai", data };
  51. }
  52. if (message.role === "user") {
  53. const data = { content: message.content };
  54. if (Array.isArray(message.content)) {
  55. data.content = message.content.map((part) => {
  56. if (part.type === "text") {
  57. return {
  58. type: "text",
  59. text: part.text,
  60. ...part.experimental_providerMetadata,
  61. };
  62. }
  63. if (part.type === "image") {
  64. let imageUrl = part.image;
  65. if (typeof imageUrl !== "string") {
  66. let uint8Array;
  67. if (imageUrl != null &&
  68. typeof imageUrl === "object" &&
  69. "type" in imageUrl &&
  70. "data" in imageUrl) {
  71. // Typing is wrong here if a buffer is passed in
  72. uint8Array = new Uint8Array(imageUrl.data);
  73. }
  74. else if (imageUrl != null &&
  75. typeof imageUrl === "object" &&
  76. Object.keys(imageUrl).every((key) => !isNaN(Number(key)))) {
  77. // ArrayBuffers get turned into objects with numeric keys for some reason
  78. uint8Array = new Uint8Array(Array.from({
  79. ...imageUrl,
  80. length: Object.keys(imageUrl).length,
  81. }));
  82. }
  83. if (uint8Array) {
  84. let binary = "";
  85. for (let i = 0; i < uint8Array.length; i++) {
  86. binary += String.fromCharCode(uint8Array[i]);
  87. }
  88. imageUrl = btoa(binary);
  89. }
  90. }
  91. return {
  92. type: "image_url",
  93. image_url: imageUrl,
  94. ...part.experimental_providerMetadata,
  95. };
  96. }
  97. return part;
  98. });
  99. }
  100. return { type: "human", data };
  101. }
  102. if (message.role === "system") {
  103. return { type: "system", data: { content: message.content } };
  104. }
  105. if (message.role === "tool") {
  106. const res = message.content.map((toolCall) => {
  107. return {
  108. type: "tool",
  109. data: {
  110. content: JSON.stringify(toolCall.result),
  111. name: toolCall.toolName,
  112. tool_call_id: toolCall.toolCallId,
  113. },
  114. };
  115. });
  116. if (res.length === 1)
  117. return res[0];
  118. return res;
  119. }
  120. return message;
  121. }
  122. const tryJson = (str) => {
  123. try {
  124. if (!str)
  125. return str;
  126. if (typeof str !== "string")
  127. return str;
  128. return JSON.parse(str);
  129. }
  130. catch {
  131. return str;
  132. }
  133. };
  134. function stripNonAlphanumeric(input) {
  135. return input.replace(/[-:.]/g, "");
  136. }
  137. function getDotOrder(item) {
  138. const { startTime: [seconds, nanoseconds], id: runId, executionOrder, } = item;
  139. // Date only has millisecond precision, so we use the microseconds to break
  140. // possible ties, avoiding incorrect run order
  141. const nanosecondString = String(nanoseconds).padStart(9, "0");
  142. const msFull = Number(nanosecondString.slice(0, 6)) + executionOrder;
  143. const msString = String(msFull).padStart(6, "0");
  144. const ms = Number(msString.slice(0, -3));
  145. const ns = msString.slice(-3);
  146. return (stripNonAlphanumeric(`${new Date(seconds * 1000 + ms).toISOString().slice(0, -1)}${ns}Z`) + runId);
  147. }
  148. function joinDotOrder(...segments) {
  149. return segments.filter(Boolean).join(".");
  150. }
  151. function removeDotOrder(dotOrder, ...ids) {
  152. return dotOrder
  153. .split(".")
  154. .filter((i) => !ids.some((id) => i.includes(id)))
  155. .join(".");
  156. }
  157. function reparentDotOrder(dotOrder, sourceRunId, parentDotOrder) {
  158. const segments = dotOrder.split(".");
  159. const sourceIndex = segments.findIndex((i) => i.includes(sourceRunId));
  160. if (sourceIndex === -1)
  161. return dotOrder;
  162. return joinDotOrder(...parentDotOrder.split("."), ...segments.slice(sourceIndex));
  163. }
  164. function getMutableRunCreate(dotOrder) {
  165. const segments = dotOrder.split(".").map((i) => {
  166. const [startTime, runId] = i.split("Z");
  167. return { startTime, runId };
  168. });
  169. const traceId = segments[0].runId;
  170. const parentRunId = segments.at(-2)?.runId;
  171. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  172. const runId = segments.at(-1).runId;
  173. return {
  174. id: runId,
  175. trace_id: traceId,
  176. dotted_order: dotOrder,
  177. parent_run_id: parentRunId,
  178. };
  179. }
  180. function convertToTimestamp([seconds, nanoseconds]) {
  181. const ms = String(nanoseconds).slice(0, 3);
  182. return Number(String(seconds) + ms);
  183. }
  184. function sortByHr(a, b) {
  185. if (a.startTime[0] !== b.startTime[0]) {
  186. return Math.sign(a.startTime[0] - b.startTime[0]);
  187. }
  188. else if (a.startTime[1] !== b.startTime[1]) {
  189. return Math.sign(a.startTime[1] - b.startTime[1]);
  190. }
  191. else if (getParentSpanId(a) === b.spanContext().spanId) {
  192. return -1;
  193. }
  194. else if (getParentSpanId(b) === a.spanContext().spanId) {
  195. return 1;
  196. }
  197. else {
  198. return 0;
  199. }
  200. }
  201. const ROOT = "$";
  202. const RUN_ID_NAMESPACE = "5c718b20-9078-11ef-9a3d-325096b39f47";
  203. const RUN_ID_METADATA_KEY = {
  204. input: "langsmith:runId",
  205. output: "ai.telemetry.metadata.langsmith:runId",
  206. };
  207. const RUN_NAME_METADATA_KEY = {
  208. input: "langsmith:runName",
  209. output: "ai.telemetry.metadata.langsmith:runName",
  210. };
  211. const TRACE_METADATA_KEY = {
  212. input: "langsmith:trace",
  213. output: "ai.telemetry.metadata.langsmith:trace",
  214. };
  215. const BAGGAGE_METADATA_KEY = {
  216. input: "langsmith:baggage",
  217. output: "ai.telemetry.metadata.langsmith:baggage",
  218. };
  219. const RESERVED_METADATA_KEYS = [
  220. RUN_ID_METADATA_KEY.output,
  221. RUN_NAME_METADATA_KEY.output,
  222. TRACE_METADATA_KEY.output,
  223. BAGGAGE_METADATA_KEY.output,
  224. ];
  225. function getParentSpanId(span) {
  226. // Backcompat shim to support OTEL 1.x and 2.x
  227. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  228. return (span.parentSpanId ?? span.parentSpanContext?.spanId ?? undefined);
  229. }
  230. /**
  231. * OpenTelemetry trace exporter for Vercel AI SDK.
  232. *
  233. * @example
  234. * ```ts
  235. * import { AISDKExporter } from "langsmith/vercel";
  236. * import { Client } from "langsmith";
  237. *
  238. * import { generateText } from "ai";
  239. * import { openai } from "@ai-sdk/openai";
  240. *
  241. * import { NodeSDK } from "@opentelemetry/sdk-node";
  242. * import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
  243. *
  244. * const client = new Client();
  245. *
  246. * const sdk = new NodeSDK({
  247. * traceExporter: new AISDKExporter({ client }),
  248. * instrumentations: [getNodeAutoInstrumentations()],
  249. * });
  250. *
  251. * sdk.start();
  252. *
  253. * const res = await generateText({
  254. * model: openai("gpt-4o-mini"),
  255. * messages: [
  256. * {
  257. * role: "user",
  258. * content: "What color is the sky?",
  259. * },
  260. * ],
  261. * experimental_telemetry: AISDKExporter.getSettings({
  262. * runName: "langsmith_traced_call",
  263. * metadata: { userId: "123", language: "english" },
  264. * }),
  265. * });
  266. *
  267. * await sdk.shutdown();
  268. * ```
  269. */
  270. class AISDKExporter {
  271. constructor(args) {
  272. Object.defineProperty(this, "client", {
  273. enumerable: true,
  274. configurable: true,
  275. writable: true,
  276. value: void 0
  277. });
  278. Object.defineProperty(this, "traceByMap", {
  279. enumerable: true,
  280. configurable: true,
  281. writable: true,
  282. value: {}
  283. });
  284. Object.defineProperty(this, "seenSpanInfo", {
  285. enumerable: true,
  286. configurable: true,
  287. writable: true,
  288. value: {}
  289. });
  290. Object.defineProperty(this, "pendingSpans", {
  291. enumerable: true,
  292. configurable: true,
  293. writable: true,
  294. value: {}
  295. });
  296. Object.defineProperty(this, "debug", {
  297. enumerable: true,
  298. configurable: true,
  299. writable: true,
  300. value: void 0
  301. });
  302. Object.defineProperty(this, "projectName", {
  303. enumerable: true,
  304. configurable: true,
  305. writable: true,
  306. value: void 0
  307. });
  308. /** @internal */
  309. Object.defineProperty(this, "getSpanAttributeKey", {
  310. enumerable: true,
  311. configurable: true,
  312. writable: true,
  313. value: (span, key) => {
  314. const attributes = span.attributes;
  315. return key in attributes && typeof attributes[key] === "string"
  316. ? attributes[key]
  317. : undefined;
  318. }
  319. });
  320. this.client = args?.client ?? new index_js_1.Client();
  321. this.debug =
  322. args?.debug ?? (0, env_js_1.getEnvironmentVariable)("OTEL_LOG_LEVEL") === "DEBUG";
  323. this.projectName = args?.projectName;
  324. this.logDebug("creating exporter", { tracingEnabled: (0, env_js_2.isTracingEnabled)() });
  325. }
  326. static getSettings(settings) {
  327. const { runId, runName, ...rest } = settings ?? {};
  328. const metadata = { ...rest?.metadata };
  329. if (runId != null)
  330. metadata[RUN_ID_METADATA_KEY.input] = runId;
  331. if (runName != null)
  332. metadata[RUN_NAME_METADATA_KEY.input] = runName;
  333. // attempt to obtain the run tree if used within a traceable function
  334. let defaultEnabled = settings?.isEnabled ?? (0, env_js_2.isTracingEnabled)();
  335. try {
  336. const runTree = (0, traceable_js_1.getCurrentRunTree)();
  337. const headers = runTree.toHeaders();
  338. metadata[TRACE_METADATA_KEY.input] = headers["langsmith-trace"];
  339. metadata[BAGGAGE_METADATA_KEY.input] = headers["baggage"];
  340. // honor the tracingEnabled flag if coming from traceable
  341. if (runTree.tracingEnabled != null) {
  342. defaultEnabled = runTree.tracingEnabled;
  343. }
  344. }
  345. catch {
  346. // pass
  347. }
  348. if (metadata[RUN_ID_METADATA_KEY.input] &&
  349. metadata[TRACE_METADATA_KEY.input]) {
  350. throw new Error("Cannot provide `runId` when used within traceable function.");
  351. }
  352. return { ...rest, isEnabled: rest.isEnabled ?? defaultEnabled, metadata };
  353. }
  354. /** @internal */
  355. parseInteropFromMetadata(span, parentSpan) {
  356. if (!this.isRootRun(span))
  357. return undefined;
  358. if (parentSpan?.name === "ai.toolCall") {
  359. return undefined;
  360. }
  361. const userTraceId = this.getSpanAttributeKey(span, RUN_ID_METADATA_KEY.output);
  362. const parentTrace = this.getSpanAttributeKey(span, TRACE_METADATA_KEY.output);
  363. if (parentTrace && userTraceId) {
  364. throw new Error(`Cannot provide both "${RUN_ID_METADATA_KEY.input}" and "${TRACE_METADATA_KEY.input}" metadata keys.`);
  365. }
  366. if (parentTrace) {
  367. const parentRunTree = index_js_1.RunTree.fromHeaders({
  368. "langsmith-trace": parentTrace,
  369. baggage: this.getSpanAttributeKey(span, BAGGAGE_METADATA_KEY.output) || "",
  370. });
  371. if (!parentRunTree)
  372. throw new Error("Unreachable code: empty parent run tree");
  373. return { type: "traceable", parentRunTree };
  374. }
  375. if (userTraceId)
  376. return { type: "user", userRunId: userTraceId };
  377. return undefined;
  378. }
  379. /** @internal */
  380. getRunCreate(span, projectName) {
  381. const asRunCreate = (rawConfig) => {
  382. const aiMetadata = Object.keys(span.attributes)
  383. .filter((key) => key.startsWith("ai.telemetry.metadata.") &&
  384. !RESERVED_METADATA_KEYS.includes(key))
  385. .reduce((acc, key) => {
  386. acc[key.slice("ai.telemetry.metadata.".length)] =
  387. span.attributes[key];
  388. return acc;
  389. }, {});
  390. if (("ai.telemetry.functionId" in span.attributes &&
  391. span.attributes["ai.telemetry.functionId"]) ||
  392. ("resource.name" in span.attributes && span.attributes["resource.name"])) {
  393. aiMetadata["functionId"] =
  394. span.attributes["ai.telemetry.functionId"] ||
  395. span.attributes["resource.name"];
  396. }
  397. const parsedStart = convertToTimestamp(span.startTime);
  398. const parsedEnd = convertToTimestamp(span.endTime);
  399. let name = rawConfig.name;
  400. // if user provided a custom name, only use it if it's the root
  401. if (this.isRootRun(span)) {
  402. name =
  403. this.getSpanAttributeKey(span, RUN_NAME_METADATA_KEY.output) || name;
  404. }
  405. const config = {
  406. ...rawConfig,
  407. name,
  408. extra: {
  409. ...rawConfig.extra,
  410. metadata: {
  411. ...rawConfig.extra?.metadata,
  412. ...aiMetadata,
  413. "ai.operationId": span.attributes["ai.operationId"],
  414. },
  415. },
  416. session_name: projectName ??
  417. this.projectName ??
  418. (0, env_js_1.getLangSmithEnvironmentVariable)("PROJECT") ??
  419. (0, env_js_1.getLangSmithEnvironmentVariable)("SESSION"),
  420. start_time: Math.min(parsedStart, parsedEnd),
  421. end_time: Math.max(parsedStart, parsedEnd),
  422. };
  423. return config;
  424. };
  425. switch (span.name) {
  426. case "ai.generateText.doGenerate":
  427. case "ai.generateText":
  428. case "ai.streamText.doStream":
  429. case "ai.streamText": {
  430. const inputs = (() => {
  431. if ("ai.prompt.messages" in span.attributes) {
  432. return {
  433. messages: tryJson(span.attributes["ai.prompt.messages"]).flatMap((i) => convertCoreToSmith(i)),
  434. };
  435. }
  436. if ("ai.prompt" in span.attributes) {
  437. const input = tryJson(span.attributes["ai.prompt"]);
  438. if (typeof input === "object" &&
  439. input != null &&
  440. "messages" in input &&
  441. Array.isArray(input.messages)) {
  442. return {
  443. messages: input.messages.flatMap((i) => convertCoreToSmith(i)),
  444. };
  445. }
  446. return { input };
  447. }
  448. return {};
  449. })();
  450. const outputs = (() => {
  451. let result = undefined;
  452. if (span.attributes["ai.response.toolCalls"]) {
  453. let content = tryJson(span.attributes["ai.response.toolCalls"]);
  454. if (Array.isArray(content)) {
  455. content = content.map((i) => ({
  456. type: "tool-call",
  457. ...i,
  458. args: tryJson(i.args),
  459. }));
  460. }
  461. result = {
  462. llm_output: convertCoreToSmith({
  463. role: "assistant",
  464. content,
  465. }),
  466. };
  467. }
  468. else if (span.attributes["ai.response.text"]) {
  469. result = {
  470. llm_output: convertCoreToSmith({
  471. role: "assistant",
  472. content: span.attributes["ai.response.text"],
  473. }),
  474. };
  475. }
  476. if (span.attributes["ai.usage.completionTokens"]) {
  477. result ??= {};
  478. result.llm_output ??= {};
  479. result.llm_output.token_usage ??= {};
  480. result.llm_output.token_usage["completion_tokens"] =
  481. span.attributes["ai.usage.completionTokens"];
  482. }
  483. if (span.attributes["ai.usage.promptTokens"]) {
  484. result ??= {};
  485. result.llm_output ??= {};
  486. result.llm_output.token_usage ??= {};
  487. result.llm_output.token_usage["prompt_tokens"] =
  488. span.attributes["ai.usage.promptTokens"];
  489. }
  490. return result;
  491. })();
  492. const invocationParams = (() => {
  493. if ("ai.prompt.tools" in span.attributes) {
  494. return {
  495. tools: span.attributes["ai.prompt.tools"].flatMap((tool) => {
  496. try {
  497. return JSON.parse(tool);
  498. }
  499. catch {
  500. // pass
  501. }
  502. return [];
  503. }),
  504. };
  505. }
  506. return {};
  507. })();
  508. const events = [];
  509. const firstChunkEvent = span.events.find((i) => i.name === "ai.stream.firstChunk");
  510. if (firstChunkEvent) {
  511. events.push({
  512. name: "new_token",
  513. time: convertToTimestamp(firstChunkEvent.time),
  514. });
  515. }
  516. // TODO: add first_token_time
  517. return asRunCreate({
  518. run_type: "llm",
  519. name: span.attributes["ai.model.provider"],
  520. inputs,
  521. outputs,
  522. events,
  523. extra: {
  524. invocation_params: invocationParams,
  525. batch_size: 1,
  526. metadata: {
  527. ls_provider: span.attributes["ai.model.provider"]
  528. .split(".")
  529. .at(0),
  530. ls_model_type: span.attributes["ai.model.provider"]
  531. .split(".")
  532. .at(1),
  533. ls_model_name: span.attributes["ai.model.id"],
  534. },
  535. },
  536. });
  537. }
  538. case "ai.toolCall": {
  539. const args = tryJson(span.attributes["ai.toolCall.args"]);
  540. let inputs = { args };
  541. if (typeof args === "object" && args != null) {
  542. inputs = args;
  543. }
  544. const output = tryJson(span.attributes["ai.toolCall.result"]);
  545. let outputs = { output };
  546. if (typeof output === "object" && output != null) {
  547. outputs = output;
  548. }
  549. return asRunCreate({
  550. run_type: "tool",
  551. name: span.attributes["ai.toolCall.name"],
  552. inputs,
  553. outputs,
  554. });
  555. }
  556. case "ai.streamObject":
  557. case "ai.streamObject.doStream":
  558. case "ai.generateObject":
  559. case "ai.generateObject.doGenerate": {
  560. const inputs = (() => {
  561. if ("ai.prompt.messages" in span.attributes) {
  562. return {
  563. messages: tryJson(span.attributes["ai.prompt.messages"]).flatMap((i) => convertCoreToSmith(i)),
  564. };
  565. }
  566. if ("ai.prompt" in span.attributes) {
  567. return { input: tryJson(span.attributes["ai.prompt"]) };
  568. }
  569. return {};
  570. })();
  571. const outputs = (() => {
  572. let result = undefined;
  573. if (span.attributes["ai.response.object"]) {
  574. result = {
  575. output: tryJson(span.attributes["ai.response.object"]),
  576. };
  577. }
  578. if (span.attributes["ai.usage.completionTokens"]) {
  579. result ??= {};
  580. result.llm_output ??= {};
  581. result.llm_output.token_usage ??= {};
  582. result.llm_output.token_usage["completion_tokens"] =
  583. span.attributes["ai.usage.completionTokens"];
  584. }
  585. if (span.attributes["ai.usage.promptTokens"]) {
  586. result ??= {};
  587. result.llm_output ??= {};
  588. result.llm_output.token_usage ??= {};
  589. result.llm_output.token_usage["prompt_tokens"] =
  590. +span.attributes["ai.usage.promptTokens"];
  591. }
  592. return result;
  593. })();
  594. const events = [];
  595. const firstChunkEvent = span.events.find((i) => i.name === "ai.stream.firstChunk");
  596. if (firstChunkEvent) {
  597. events.push({
  598. name: "new_token",
  599. time: convertToTimestamp(firstChunkEvent.time),
  600. });
  601. }
  602. return asRunCreate({
  603. run_type: "llm",
  604. name: span.attributes["ai.model.provider"],
  605. inputs,
  606. outputs,
  607. events,
  608. extra: {
  609. batch_size: 1,
  610. metadata: {
  611. ls_provider: span.attributes["ai.model.provider"]
  612. .split(".")
  613. .at(0),
  614. ls_model_type: span.attributes["ai.model.provider"]
  615. .split(".")
  616. .at(1),
  617. ls_model_name: span.attributes["ai.model.id"],
  618. },
  619. },
  620. });
  621. }
  622. case "ai.embed":
  623. case "ai.embed.doEmbed":
  624. case "ai.embedMany":
  625. case "ai.embedMany.doEmbed":
  626. default:
  627. return undefined;
  628. }
  629. }
  630. /** @internal */
  631. isRootRun(span) {
  632. switch (span.name) {
  633. case "ai.generateText":
  634. case "ai.streamText":
  635. case "ai.generateObject":
  636. case "ai.streamObject":
  637. case "ai.embed":
  638. case "ai.embedMany":
  639. return true;
  640. default:
  641. return false;
  642. }
  643. }
  644. _export(spans, resultCallback) {
  645. this.logDebug("exporting spans", spans);
  646. const typedSpans = spans
  647. .concat(Object.values(this.pendingSpans))
  648. .slice()
  649. // Parent spans should go before child spans in the final order,
  650. // but may have the same exact start time as their children.
  651. // They will end earlier, so break ties by end time.
  652. // TODO: Figure out why this happens.
  653. .sort((a, b) => sortByHr(a, b));
  654. for (const span of typedSpans) {
  655. const { traceId, spanId } = span.spanContext();
  656. const runId = (0, uuid_1.v5)(spanId, RUN_ID_NAMESPACE);
  657. let parentId = getParentSpanId(span);
  658. let parentRunId = parentId
  659. ? (0, uuid_1.v5)(parentId, RUN_ID_NAMESPACE)
  660. : undefined;
  661. let parentSpanInfo = parentRunId
  662. ? this.seenSpanInfo[parentRunId]
  663. : undefined;
  664. // Unrelated, untraced spans should behave as passthroughs from LangSmith's perspective.
  665. while (parentSpanInfo != null &&
  666. this.getRunCreate(parentSpanInfo.span) == null) {
  667. parentId = getParentSpanId(parentSpanInfo.span);
  668. if (parentId == null) {
  669. break;
  670. }
  671. parentRunId = parentId ? (0, uuid_1.v5)(parentId, RUN_ID_NAMESPACE) : undefined;
  672. parentSpanInfo = parentRunId
  673. ? this.seenSpanInfo[parentRunId]
  674. : undefined;
  675. }
  676. // Export may be called in any order, so we need to queue any spans with missing parents
  677. // for retry later in order to determine whether their parents are tool calls
  678. // and should not be reparented below.
  679. if (parentRunId !== undefined && parentSpanInfo === undefined) {
  680. this.pendingSpans[spanId] = span;
  681. continue;
  682. }
  683. else {
  684. delete this.pendingSpans[spanId];
  685. }
  686. this.traceByMap[traceId] ??= {
  687. childMap: {},
  688. nodeMap: {},
  689. relativeExecutionOrder: {},
  690. };
  691. const traceMap = this.traceByMap[traceId];
  692. traceMap.relativeExecutionOrder[parentRunId ?? ROOT] ??= -1;
  693. traceMap.relativeExecutionOrder[parentRunId ?? ROOT] += 1;
  694. const interop = this.parseInteropFromMetadata(span, parentSpanInfo?.span);
  695. const projectName = (interop?.type === "traceable"
  696. ? interop.parentRunTree.project_name
  697. : undefined) ?? parentSpanInfo?.projectName;
  698. const run = this.getRunCreate(span, projectName);
  699. traceMap.nodeMap[runId] ??= {
  700. id: runId,
  701. startTime: span.startTime,
  702. run,
  703. sent: false,
  704. interop,
  705. executionOrder: traceMap.relativeExecutionOrder[parentRunId ?? ROOT],
  706. };
  707. if (this.seenSpanInfo[runId] == null) {
  708. this.seenSpanInfo[runId] = {
  709. span,
  710. dotOrder: joinDotOrder(parentSpanInfo?.dotOrder, getDotOrder(traceMap.nodeMap[runId])),
  711. projectName,
  712. sent: false,
  713. };
  714. }
  715. if (this.debug)
  716. console.log(`[${span.name}] ${runId}`, run);
  717. traceMap.childMap[parentRunId ?? ROOT] ??= [];
  718. traceMap.childMap[parentRunId ?? ROOT].push(traceMap.nodeMap[runId]);
  719. }
  720. const sampled = [];
  721. const actions = [];
  722. for (const traceId of Object.keys(this.traceByMap)) {
  723. const traceMap = this.traceByMap[traceId];
  724. const queue = Object.keys(traceMap.childMap)
  725. .map((runId) => {
  726. if (runId === ROOT) {
  727. return traceMap.childMap[runId];
  728. }
  729. return [];
  730. })
  731. .flat();
  732. const seen = new Set();
  733. while (queue.length) {
  734. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  735. const task = queue.shift();
  736. if (seen.has(task.id))
  737. continue;
  738. let taskDotOrder = this.seenSpanInfo[task.id].dotOrder;
  739. if (!task.sent) {
  740. if (task.run != null) {
  741. if (task.interop?.type === "user") {
  742. actions.push({
  743. type: "rename",
  744. sourceRunId: task.id,
  745. targetRunId: task.interop.userRunId,
  746. });
  747. }
  748. if (task.interop?.type === "traceable") {
  749. actions.push({
  750. type: "reparent",
  751. runId: task.id,
  752. parentDotOrder: task.interop.parentRunTree.dotted_order,
  753. });
  754. }
  755. for (const action of actions) {
  756. if (action.type === "delete") {
  757. taskDotOrder = removeDotOrder(taskDotOrder, action.runId);
  758. }
  759. if (action.type === "reparent") {
  760. taskDotOrder = reparentDotOrder(taskDotOrder, action.runId, action.parentDotOrder);
  761. }
  762. if (action.type === "rename") {
  763. taskDotOrder = taskDotOrder.replace(action.sourceRunId, action.targetRunId);
  764. }
  765. }
  766. this.seenSpanInfo[task.id].dotOrder = taskDotOrder;
  767. if (!this.seenSpanInfo[task.id].sent) {
  768. sampled.push({
  769. ...task.run,
  770. ...getMutableRunCreate(taskDotOrder),
  771. });
  772. }
  773. this.seenSpanInfo[task.id].sent = true;
  774. }
  775. else {
  776. actions.push({ type: "delete", runId: task.id });
  777. }
  778. task.sent = true;
  779. }
  780. const children = traceMap.childMap[task.id] ?? [];
  781. queue.push(...children);
  782. }
  783. }
  784. this.logDebug(`sampled runs to be sent to LangSmith`, sampled);
  785. Promise.all(sampled.map((run) => this.client.createRun(run))).then(() => resultCallback({ code: 0 }), (error) => resultCallback({ code: 1, error }));
  786. }
  787. export(spans, resultCallback) {
  788. this._export(spans, (result) => {
  789. if (result.code === 0) {
  790. // Empty export to try flushing pending spans to rule out any trace order shenanigans
  791. this._export([], resultCallback);
  792. }
  793. else {
  794. resultCallback(result);
  795. }
  796. });
  797. }
  798. async shutdown() {
  799. // find nodes which are incomplete
  800. const incompleteNodes = Object.values(this.traceByMap).flatMap((trace) => Object.values(trace.nodeMap).filter((i) => !i.sent && i.run != null));
  801. this.logDebug("shutting down", { incompleteNodes });
  802. if (incompleteNodes.length > 0) {
  803. console.warn("Some incomplete nodes were found before shutdown and not sent to LangSmith.");
  804. }
  805. await this.forceFlush();
  806. }
  807. async forceFlush() {
  808. await new Promise((resolve) => {
  809. this.export([], resolve);
  810. });
  811. await this.client.awaitPendingTraceBatches();
  812. }
  813. logDebug(...args) {
  814. if (!this.debug)
  815. return;
  816. console.debug(`[${new Date().toISOString()}] [LangSmith]`, ...args);
  817. }
  818. }
  819. exports.AISDKExporter = AISDKExporter;