langwatch 0.0.3 → 0.1.1

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.
Files changed (40) hide show
  1. package/dist/{chunk-AP23NJ57.mjs → chunk-OVS4NSDE.mjs} +373 -2
  2. package/dist/chunk-OVS4NSDE.mjs.map +1 -0
  3. package/dist/index.d.mts +47 -5
  4. package/dist/index.d.ts +47 -5
  5. package/dist/index.js +6275 -485
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +329 -349
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{utils-Dg5eWsAz.d.mts → utils-K-jSEpnZ.d.mts} +11 -7
  10. package/dist/{utils-Dg5eWsAz.d.ts → utils-K-jSEpnZ.d.ts} +11 -7
  11. package/dist/utils.d.mts +1 -1
  12. package/dist/utils.d.ts +1 -1
  13. package/dist/utils.js +370 -0
  14. package/dist/utils.js.map +1 -1
  15. package/dist/utils.mjs +3 -1
  16. package/example/README.md +3 -1
  17. package/example/app/(chat)/chat/[id]/page.tsx +1 -1
  18. package/example/app/(chat)/page.tsx +10 -5
  19. package/example/app/langchain/page.tsx +27 -0
  20. package/example/app/langchain-rag/page.tsx +28 -0
  21. package/example/app/share/[id]/page.tsx +1 -1
  22. package/example/components/chat-list.tsx +1 -1
  23. package/example/components/chat-panel.tsx +1 -1
  24. package/example/components/header.tsx +35 -13
  25. package/example/components/prompt-form.tsx +1 -1
  26. package/example/components/stocks/stock-purchase.tsx +1 -1
  27. package/example/components/stocks/stocks.tsx +1 -1
  28. package/example/lib/chat/langchain-rag.tsx +191 -0
  29. package/example/lib/chat/langchain.tsx +112 -0
  30. package/example/lib/chat/{actions.tsx → vercel-ai.tsx} +4 -6
  31. package/example/package-lock.json +287 -4
  32. package/example/package.json +1 -0
  33. package/package.json +12 -2
  34. package/src/index.test.ts +96 -28
  35. package/src/index.ts +18 -9
  36. package/src/langchain.ts +557 -0
  37. package/src/types.ts +4 -4
  38. package/src/utils.ts +28 -1
  39. package/dist/chunk-AP23NJ57.mjs.map +0 -1
  40. /package/src/{helpers.ts → typeUtils.ts} +0 -0
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import EventEmitter from "events";
2
2
  import { nanoid } from "nanoid";
3
3
  import { ZodError } from "zod";
4
4
  import { fromZodError } from "zod-validation-error";
5
- import { camelToSnakeCaseNested, type Strict } from "./helpers";
5
+ import { camelToSnakeCaseNested, type Strict } from "./typeUtils";
6
6
  import {
7
7
  type CollectorRESTParams,
8
8
  type Span as ServerSpan,
@@ -24,7 +24,8 @@ import {
24
24
  type RAGSpan,
25
25
  type SpanInputOutput,
26
26
  } from "./types";
27
- import { captureError, convertFromVercelAIMessages } from "./utils";
27
+ import { autoconvertTypedValues, captureError, convertFromVercelAIMessages } from "./utils";
28
+ import { LangWatchCallbackHandler } from "./langchain";
28
29
 
29
30
  export type {
30
31
  BaseSpan,
@@ -39,7 +40,7 @@ export type {
39
40
  SpanInputOutput,
40
41
  };
41
42
 
42
- export { convertFromVercelAIMessages, captureError };
43
+ export { convertFromVercelAIMessages, captureError, autoconvertTypedValues };
43
44
 
44
45
  export class LangWatch extends EventEmitter {
45
46
  apiKey: string | undefined;
@@ -138,6 +139,7 @@ export class LangWatchTrace {
138
139
  metadata?: Metadata;
139
140
  finishedSpans: Record<string, ServerSpan> = {};
140
141
  timeoutRef?: NodeJS.Timeout;
142
+ langchainCallback?: LangWatchCallbackHandler;
141
143
 
142
144
  constructor({
143
145
  client,
@@ -187,6 +189,13 @@ export class LangWatchTrace {
187
189
  return span;
188
190
  }
189
191
 
192
+ getLangChainCallback() {
193
+ if (!this.langchainCallback) {
194
+ this.langchainCallback = new LangWatchCallbackHandler({ trace: this });
195
+ }
196
+ return this.langchainCallback;
197
+ }
198
+
190
199
  onEnd(span: ServerSpan) {
191
200
  this.finishedSpans[span.span_id] = span;
192
201
  this.delayedSendSpans();
@@ -230,8 +239,8 @@ export class LangWatchSpan implements PendingBaseSpan {
230
239
  parentId?: string | null;
231
240
  type: SpanTypes;
232
241
  name?: string | null;
233
- input: PendingBaseSpan["input"];
234
- outputs: PendingBaseSpan["outputs"];
242
+ input?: PendingBaseSpan["input"];
243
+ output?: PendingBaseSpan["output"];
235
244
  error?: PendingBaseSpan["error"];
236
245
  timestamps: PendingBaseSpan["timestamps"];
237
246
  metrics: PendingBaseSpan["metrics"];
@@ -243,7 +252,7 @@ export class LangWatchSpan implements PendingBaseSpan {
243
252
  type,
244
253
  name,
245
254
  input,
246
- outputs,
255
+ output,
247
256
  error,
248
257
  timestamps,
249
258
  metrics,
@@ -254,7 +263,7 @@ export class LangWatchSpan implements PendingBaseSpan {
254
263
  this.type = type ?? "span";
255
264
  this.name = name;
256
265
  this.input = input;
257
- this.outputs = outputs ?? [];
266
+ this.output = output;
258
267
  this.error = error;
259
268
  this.timestamps = timestamps ?? {
260
269
  startedAt: Date.now(),
@@ -280,8 +289,8 @@ export class LangWatchSpan implements PendingBaseSpan {
280
289
  if ("input" in params) {
281
290
  this.input = params.input;
282
291
  }
283
- if (params.outputs) {
284
- this.outputs = params.outputs;
292
+ if ("output" in params) {
293
+ this.output = params.output;
285
294
  }
286
295
  if ("error" in params) {
287
296
  this.error = params.error;
@@ -0,0 +1,557 @@
1
+ import type { AgentAction, AgentFinish } from "@langchain/core/agents";
2
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
3
+ import { type DocumentInterface } from "@langchain/core/documents";
4
+ import type { Serialized } from "@langchain/core/load/serializable";
5
+ import {
6
+ AIMessage,
7
+ AIMessageChunk,
8
+ FunctionMessage,
9
+ FunctionMessageChunk,
10
+ HumanMessage,
11
+ HumanMessageChunk,
12
+ SystemMessage,
13
+ SystemMessageChunk,
14
+ ToolMessage,
15
+ ToolMessageChunk,
16
+ mapChatMessagesToStoredMessages,
17
+ type BaseMessage,
18
+ type StoredMessage,
19
+ } from "@langchain/core/messages";
20
+ import type { ChatGeneration, LLMResult } from "@langchain/core/outputs";
21
+ import type { ChainValues } from "@langchain/core/utils/types";
22
+ import { stringify } from "javascript-stringify";
23
+ import {
24
+ type LangWatchRAGSpan,
25
+ type LangWatchSpan,
26
+ type LangWatchTrace,
27
+ } from ".";
28
+ import {
29
+ type RAGSpan,
30
+ type BaseSpan,
31
+ type ChatMessage,
32
+ type ChatRichContent,
33
+ type SpanInputOutput,
34
+ } from "./types";
35
+
36
+ export class LangWatchCallbackHandler extends BaseCallbackHandler {
37
+ name = "LangWatchCallbackHandler";
38
+ trace: LangWatchTrace;
39
+ spans: Record<string, LangWatchSpan> = {};
40
+
41
+ constructor({ trace }: { trace: LangWatchTrace }) {
42
+ super();
43
+ this.trace = trace;
44
+ }
45
+
46
+ async handleLLMStart(
47
+ llm: Serialized,
48
+ prompts: string[],
49
+ runId: string,
50
+ parentRunId?: string | undefined,
51
+ extraParams?: Record<string, unknown> | undefined,
52
+ _tags?: string[] | undefined,
53
+ metadata?: Record<string, unknown> | undefined,
54
+ name?: string
55
+ ): Promise<void> {
56
+ this.spans[runId] = this.buildLLMSpan({
57
+ llm,
58
+ runId,
59
+ parentRunId,
60
+ input: {
61
+ type: "json",
62
+ value: prompts,
63
+ },
64
+ extraParams,
65
+ metadata,
66
+ name,
67
+ });
68
+ }
69
+
70
+ private buildLLMSpan({
71
+ llm,
72
+ runId,
73
+ parentRunId,
74
+ input,
75
+ extraParams,
76
+ metadata,
77
+ name,
78
+ }: {
79
+ llm: Serialized;
80
+ runId: string;
81
+ parentRunId?: string | undefined;
82
+ input: SpanInputOutput;
83
+ extraParams?: Record<string, unknown> | undefined;
84
+ metadata?: Record<string, unknown> | undefined;
85
+ name?: string | undefined;
86
+ }) {
87
+ try {
88
+ const parent = this.getParent(parentRunId);
89
+
90
+ const vendor = metadata?.ls_provider ?? llm.id.at(-2)?.toString();
91
+ const model =
92
+ metadata?.ls_model_name ?? (llm as any).kwargs?.model ?? "unknown";
93
+
94
+ const span = parent.startLLMSpan({
95
+ spanId: runId,
96
+ name: name ?? llm.id.at(-1)?.toString(),
97
+ input,
98
+ model: [vendor, model].filter((x) => x).join("/"),
99
+ params: {
100
+ temperature: (extraParams?.invocation_params as any)?.temperature,
101
+ ...((extraParams?.invocation_params as any)?.functions
102
+ ? { functions: (extraParams?.invocation_params as any)?.functions }
103
+ : {}),
104
+ },
105
+ });
106
+
107
+ return span;
108
+ } catch (e) {
109
+ this.trace.client.emit("error", e);
110
+ throw e;
111
+ }
112
+ }
113
+
114
+ async handleChatModelStart(
115
+ llm: Serialized,
116
+ messages: BaseMessage[][],
117
+ runId: string,
118
+ parentRunId?: string | undefined,
119
+ extraParams?: Record<string, unknown> | undefined,
120
+ tags?: string[] | undefined,
121
+ metadata?: Record<string, unknown> | undefined,
122
+ name?: string
123
+ ): Promise<void> {
124
+ this.spans[runId] = this.buildLLMSpan({
125
+ name,
126
+ llm,
127
+ runId,
128
+ parentRunId,
129
+ input: {
130
+ type: "chat_messages",
131
+ value: messages.flatMap(convertFromLangChainMessages),
132
+ },
133
+ extraParams,
134
+ metadata,
135
+ });
136
+ }
137
+
138
+ async handleNewToken(_token: string, runId: string): Promise<void> {
139
+ const span = this.spans[runId];
140
+ if (runId && span && !span.timestamps.firstTokenAt) {
141
+ span.update({
142
+ timestamps: { ...span.timestamps, firstTokenAt: Date.now() },
143
+ });
144
+ }
145
+ }
146
+
147
+ async handleLLMEnd(
148
+ response: LLMResult,
149
+ runId: string,
150
+ _parentRunId?: string | undefined
151
+ ): Promise<void> {
152
+ try {
153
+ const span = this.spans[runId];
154
+ if (!span) {
155
+ return;
156
+ }
157
+
158
+ const outputs: SpanInputOutput[] = [];
159
+ for (const generation of response.generations) {
160
+ // TODO: again, why the twice loop? Can OpenAI generate multiple chat outputs?
161
+ for (const generation_ of generation) {
162
+ if ("message" in generation_) {
163
+ outputs.push({
164
+ type: "chat_messages",
165
+ value: convertFromLangChainMessages([
166
+ (generation_ as ChatGeneration).message,
167
+ ]),
168
+ });
169
+ } else if ("text" in generation_) {
170
+ outputs.push({
171
+ type: "text",
172
+ value: generation_.text,
173
+ });
174
+ } else {
175
+ outputs.push({
176
+ type: "text",
177
+ value: JSON.stringify(generation_),
178
+ });
179
+ }
180
+ }
181
+ }
182
+
183
+ const output: SpanInputOutput | undefined =
184
+ outputs.length === 1
185
+ ? outputs[0]
186
+ : { type: "list", value: outputs as any };
187
+
188
+ // Commenting it out because LangChain.js prompt and completion tokens is broken, this one doesn't work as it should with python,
189
+ // and response_metadata.prompt and response_metadata.completion is there but it's always 0. Better let our server count.
190
+ // const metrics = response.llmOutput?.token_usage
191
+ // ? {
192
+ // promptTokens: response.llmOutput.token_usage.prompt_tokens,
193
+ // completionTokens: response.llmOutput.token_usage.completion_tokens,
194
+ // }
195
+ // : undefined;
196
+
197
+ span.end({
198
+ output,
199
+ // ...(metrics ? { metrics } : {}),
200
+ });
201
+ } catch (e) {
202
+ this.trace.client.emit("error", e);
203
+ throw e;
204
+ }
205
+ }
206
+
207
+ async handleLLMError(
208
+ err: Error,
209
+ runId: string,
210
+ _parentRunId?: string | undefined
211
+ ): Promise<void> {
212
+ this.errorSpan({ runId, error: err });
213
+ }
214
+
215
+ async handleChainStart(
216
+ chain: Serialized,
217
+ inputs: ChainValues,
218
+ runId: string,
219
+ parentRunId?: string | undefined,
220
+ _tags?: string[] | undefined,
221
+ _metadata?: Record<string, unknown> | undefined,
222
+ _runType?: string,
223
+ name?: string
224
+ ): Promise<void> {
225
+ this.spans[runId] = this.buildSpan({
226
+ type: "chain",
227
+ serialized: chain,
228
+ runId,
229
+ parentRunId,
230
+ input: inputs,
231
+ name,
232
+ });
233
+ }
234
+
235
+ async handleChainEnd(
236
+ output: ChainValues,
237
+ runId: string,
238
+ _parentRunId?: string | undefined
239
+ ): Promise<void> {
240
+ this.endSpan({
241
+ runId,
242
+ output,
243
+ });
244
+ }
245
+
246
+ async handleChainError(
247
+ err: Error,
248
+ runId: string,
249
+ _parentRunId?: string | undefined,
250
+ _tags?: string[] | undefined,
251
+ _kwargs?: { inputs?: Record<string, unknown> | undefined } | undefined
252
+ ): Promise<void> {
253
+ this.errorSpan({ runId, error: err });
254
+ }
255
+
256
+ async handleToolStart(
257
+ tool: Serialized,
258
+ input: string,
259
+ runId: string,
260
+ parentRunId?: string | undefined,
261
+ _tags?: string[] | undefined,
262
+ _metadata?: Record<string, unknown> | undefined,
263
+ name?: string
264
+ ): Promise<void> {
265
+ this.spans[runId] = this.buildSpan({
266
+ type: "tool",
267
+ serialized: tool,
268
+ runId,
269
+ parentRunId,
270
+ input,
271
+ name,
272
+ });
273
+ }
274
+
275
+ async handleToolEnd(
276
+ output: string,
277
+ runId: string,
278
+ _parentRunId?: string | undefined
279
+ ): Promise<void> {
280
+ this.endSpan({ runId, output });
281
+ }
282
+
283
+ async handleToolError(
284
+ err: Error,
285
+ runId: string,
286
+ _parentRunId?: string | undefined,
287
+ _tags?: string[] | undefined
288
+ ): Promise<void> {
289
+ this.errorSpan({ runId, error: err });
290
+ }
291
+
292
+ async handleRetrieverStart(
293
+ retriever: Serialized,
294
+ query: string,
295
+ runId: string,
296
+ parentRunId?: string | undefined,
297
+ _tags?: string[] | undefined,
298
+ _metadata?: Record<string, unknown> | undefined,
299
+ name?: string | undefined
300
+ ) {
301
+ try {
302
+ const parent = this.getParent(parentRunId);
303
+
304
+ this.spans[runId] = parent.startRAGSpan({
305
+ spanId: runId,
306
+ name: name ?? retriever.name ?? retriever.id.at(-1)?.toString(),
307
+ input: this.autoconvertTypedValues(query),
308
+ });
309
+ } catch (e) {
310
+ this.trace.client.emit("error", e);
311
+ throw e;
312
+ }
313
+ }
314
+
315
+ async handleRetrieverEnd(
316
+ documents: DocumentInterface<Record<string, any>>[],
317
+ runId: string,
318
+ _parentRunId?: string | undefined,
319
+ _tags?: string[] | undefined
320
+ ) {
321
+ try {
322
+ const contexts: RAGSpan["contexts"] = documents.map((doc) => ({
323
+ content: doc.pageContent,
324
+ ...(doc.metadata.source ? { documentId: doc.metadata.source } : {}),
325
+ }));
326
+
327
+ const span = this.spans[runId] as LangWatchRAGSpan;
328
+ if (!span) {
329
+ return;
330
+ }
331
+
332
+ span.end({
333
+ contexts,
334
+ output: this.autoconvertTypedValues(documents),
335
+ });
336
+ } catch (e) {
337
+ this.trace.client.emit("error", e);
338
+ throw e;
339
+ }
340
+ }
341
+
342
+ async handleRetrieverError(
343
+ err: Error,
344
+ runId: string,
345
+ _parentRunId?: string | undefined,
346
+ _tags?: string[] | undefined
347
+ ) {
348
+ this.errorSpan({ runId, error: err });
349
+ }
350
+
351
+ async handleAgentAction(
352
+ _action: AgentAction,
353
+ runId: string,
354
+ _parentRunId?: string | undefined,
355
+ _tags?: string[] | undefined
356
+ ): Promise<void> {
357
+ const span = this.spans[runId];
358
+ if (!span) {
359
+ return;
360
+ }
361
+
362
+ span.update({
363
+ type: "agent",
364
+ });
365
+ }
366
+
367
+ async handleAgentEnd(
368
+ action: AgentFinish,
369
+ runId: string,
370
+ _parentRunId?: string | undefined,
371
+ _tags?: string[] | undefined
372
+ ): Promise<void> {
373
+ this.endSpan({
374
+ runId,
375
+ output: action.returnValues,
376
+ });
377
+ }
378
+
379
+ private buildSpan({
380
+ type,
381
+ serialized,
382
+ runId,
383
+ parentRunId,
384
+ input,
385
+ name,
386
+ }: {
387
+ type: BaseSpan["type"];
388
+ serialized: Serialized;
389
+ runId: string;
390
+ parentRunId?: string | undefined;
391
+ input: unknown;
392
+ name?: string | undefined;
393
+ }) {
394
+ try {
395
+ const parent = this.getParent(parentRunId);
396
+
397
+ const span = parent.startSpan({
398
+ spanId: runId,
399
+ type,
400
+ name: name ?? serialized.name ?? serialized.id.at(-1)?.toString(),
401
+ input: this.autoconvertTypedValues(input),
402
+ });
403
+
404
+ return span;
405
+ } catch (e) {
406
+ this.trace.client.emit("error", e);
407
+ throw e;
408
+ }
409
+ }
410
+
411
+ private endSpan({ runId, output }: { runId: string; output: unknown }): void {
412
+ try {
413
+ const span = this.spans[runId];
414
+ if (!span) {
415
+ return;
416
+ }
417
+
418
+ span.end({
419
+ output: this.autoconvertTypedValues(output),
420
+ });
421
+ } catch (e) {
422
+ this.trace.client.emit("error", e);
423
+ throw e;
424
+ }
425
+ }
426
+
427
+ private errorSpan({ runId, error }: { runId: string; error: Error }): void {
428
+ const span = this.spans[runId];
429
+ if (!span) {
430
+ return;
431
+ }
432
+
433
+ span.end({
434
+ error,
435
+ });
436
+ }
437
+
438
+ private autoconvertTypedValues(value: any): SpanInputOutput | undefined {
439
+ if (
440
+ !value ||
441
+ (typeof value === "object" && Object.keys(value).length === 0)
442
+ ) {
443
+ return undefined;
444
+ }
445
+ if (typeof value === "string") {
446
+ return { type: "text", value };
447
+ }
448
+ try {
449
+ JSON.stringify(value);
450
+ return { type: "json", value };
451
+ } catch (e) {
452
+ return { type: "text", value: stringify(value) ?? value.toString() };
453
+ }
454
+ }
455
+
456
+ private getParent(
457
+ parentRunId?: string | undefined
458
+ ): LangWatchSpan | LangWatchTrace {
459
+ return (
460
+ (parentRunId
461
+ ? this.spans[parentRunId]
462
+ : this.spans[Object.keys(this.spans).at(-1) ?? ""]) ?? this.trace
463
+ );
464
+ }
465
+ }
466
+
467
+ export const convertFromLangChainMessages = (
468
+ messages: BaseMessage[]
469
+ ): ChatMessage[] => {
470
+ const chatMessages: ChatMessage[] = [];
471
+ for (const message of messages) {
472
+ chatMessages.push(convertFromLangChainMessage(message));
473
+ }
474
+ return chatMessages;
475
+ };
476
+
477
+ const convertFromLangChainMessage = (
478
+ message: BaseMessage & { id?: string[] }
479
+ ): ChatMessage => {
480
+ let role: ChatMessage["role"] = "user";
481
+
482
+ const message_: (BaseMessage | StoredMessage) & {
483
+ id?: string[];
484
+ type?: string;
485
+ } = message.lc_serializable
486
+ ? mapChatMessagesToStoredMessages([message])[0]!
487
+ : message;
488
+
489
+ // Dang this is so hard, langchain.js has 3 ways of representing the same thing...
490
+ if (
491
+ message_ instanceof HumanMessage ||
492
+ message_ instanceof HumanMessageChunk ||
493
+ message_.id?.at(-1) === "HumanMessage" ||
494
+ message_.id?.at(-1) === "HumanMessageChunk" ||
495
+ message_.type === "human"
496
+ ) {
497
+ role = "user";
498
+ } else if (
499
+ message instanceof AIMessage ||
500
+ message instanceof AIMessageChunk ||
501
+ message.id?.at(-1) === "AIMessage" ||
502
+ message.id?.at(-1) === "AIMessageChunk" ||
503
+ message_.type === "ai"
504
+ ) {
505
+ role = "assistant";
506
+ } else if (
507
+ message instanceof SystemMessage ||
508
+ message instanceof SystemMessageChunk ||
509
+ message.id?.at(-1) === "SystemMessage" ||
510
+ message.id?.at(-1) === "SystemMessageChunk" ||
511
+ message_.type === "system"
512
+ ) {
513
+ role = "system";
514
+ } else if (
515
+ message instanceof FunctionMessage ||
516
+ message instanceof FunctionMessageChunk ||
517
+ message.id?.at(-1) === "FunctionMessage" ||
518
+ message.id?.at(-1) === "FunctionMessageChunk" ||
519
+ message_.type === "function"
520
+ ) {
521
+ role = "function";
522
+ } else if (
523
+ message instanceof ToolMessage ||
524
+ message instanceof ToolMessageChunk ||
525
+ message.id?.at(-1) === "ToolMessage" ||
526
+ message.id?.at(-1) === "ToolMessageChunk" ||
527
+ message_.type === "tool"
528
+ ) {
529
+ role = "tool";
530
+ }
531
+
532
+ const content =
533
+ typeof message.content === "string"
534
+ ? message.content
535
+ : message.content.map(
536
+ (content): ChatRichContent =>
537
+ content.type === "text"
538
+ ? { type: "text", text: content.text }
539
+ : content.type == "image_url"
540
+ ? { type: "image_url", image_url: content.image_url }
541
+ : { type: "text", text: JSON.stringify(content) }
542
+ );
543
+
544
+ const functionCall = message.additional_kwargs as
545
+ | ChatMessage["function_call"]
546
+ | undefined;
547
+
548
+ return {
549
+ role,
550
+ content,
551
+ ...(functionCall &&
552
+ typeof functionCall === "object" &&
553
+ Object.keys(functionCall).length > 0
554
+ ? { function_call: functionCall }
555
+ : {}),
556
+ };
557
+ };
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type modelPrices from "llm-cost/model_prices_and_context_window.json";
2
2
  import type { OpenAI } from "openai";
3
- import { type SnakeToCamelCaseNested } from "./helpers";
3
+ import { type SnakeToCamelCaseNested } from "./typeUtils";
4
4
  import {
5
5
  type BaseSpan as ServerBaseSpan,
6
6
  type ChatMessage as ServerChatMessage,
@@ -31,7 +31,7 @@ export type ChatRichContent = ServerChatRichContent;
31
31
  ({}) as {
32
32
  type: "chat_messages";
33
33
  value: OpenAI.Chat.ChatCompletionMessageParam[];
34
- }[] satisfies BaseSpan["outputs"];
34
+ } satisfies BaseSpan["output"];
35
35
 
36
36
  // Keep the input/output types signatures as snake case to match the official openai nodejs api
37
37
  export type SpanInputOutput =
@@ -41,9 +41,9 @@ export type SpanInputOutput =
41
41
  | (TypedValueChatMessages & { type: ChatMessage });
42
42
 
43
43
  export type ConvertServerSpan<T extends ServerBaseSpan> =
44
- SnakeToCamelCaseNested<Omit<T, "input" | "outputs" | "error">> & {
44
+ SnakeToCamelCaseNested<Omit<T, "input" | "output" | "error">> & {
45
45
  input?: SpanInputOutput | null;
46
- outputs: SpanInputOutput[];
46
+ output?: SpanInputOutput | null;
47
47
  error?: T["error"] | NonNullable<unknown>;
48
48
  };
49
49
 
package/src/utils.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { convertUint8ArrayToBase64 } from "@ai-sdk/provider-utils";
2
2
  import { type ImagePart, type CoreMessage } from "ai";
3
- import { type ChatMessage } from "./types";
3
+ import { type ChatMessage, type SpanInputOutput } from "./types";
4
4
  import { type ErrorCapture } from "./server/types/tracer";
5
+ import {
6
+ chatMessageSchema,
7
+ spanInputOutputSchema,
8
+ typedValueChatMessagesSchema,
9
+ } from "./server/types/tracer.generated";
10
+ import { z } from "zod";
5
11
 
6
12
  const convertImageToUrl = (
7
13
  image: ImagePart["image"],
@@ -177,3 +183,24 @@ export const captureError = (error: unknown): ErrorCapture => {
177
183
  };
178
184
  }
179
185
  };
186
+
187
+ export const autoconvertTypedValues = (value: unknown): SpanInputOutput => {
188
+ if (typeof value === "string") {
189
+ return { type: "text", value };
190
+ }
191
+
192
+ const chatMessages = z.array(chatMessageSchema).safeParse(value);
193
+ if (Array.isArray(value) && chatMessages.success) {
194
+ return {
195
+ type: "chat_messages",
196
+ value: chatMessages.data,
197
+ };
198
+ }
199
+
200
+ try {
201
+ JSON.stringify(value);
202
+ return { type: "json", value: value as object };
203
+ } catch (e) {
204
+ return { type: "raw", value: value as any };
205
+ }
206
+ };