observa-sdk 0.0.21 → 0.0.22

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 (4) hide show
  1. package/README.md +131 -1
  2. package/dist/index.cjs +206 -128
  3. package/dist/index.js +206 -128
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -118,7 +118,19 @@ const response = await wrappedAnthropic.messages.create({
118
118
 
119
119
  ### Auto-Capture with Vercel AI SDK
120
120
 
121
- Vercel AI SDK is a unified SDK that works with multiple providers:
121
+ Vercel AI SDK is a unified SDK that works with multiple providers (OpenAI, Anthropic, Google, etc.).
122
+
123
+ #### Installation
124
+
125
+ First, install the required packages:
126
+
127
+ ```bash
128
+ npm install observa-sdk ai @ai-sdk/openai @ai-sdk/anthropic
129
+ # or for other providers:
130
+ npm install @ai-sdk/google @ai-sdk/cohere
131
+ ```
132
+
133
+ #### Basic Example (Node.js/Server)
122
134
 
123
135
  ```typescript
124
136
  import { init } from "observa-sdk";
@@ -152,6 +164,124 @@ for await (const chunk of stream.textStream) {
152
164
  }
153
165
  ```
154
166
 
167
+ #### Next.js App Router Example
168
+
169
+ For Next.js applications, use the route handler pattern:
170
+
171
+ ```typescript
172
+ // app/api/chat/route.ts
173
+ import { streamText, UIMessage, convertToModelMessages } from "ai";
174
+ import { init } from "observa-sdk";
175
+ import { openai } from "@ai-sdk/openai";
176
+
177
+ const observa = init({
178
+ apiKey: process.env.OBSERVA_API_KEY!,
179
+ apiUrl: process.env.OBSERVA_API_URL,
180
+ });
181
+
182
+ const ai = observa.observeVercelAI({ streamText }, {
183
+ name: "my-nextjs-app",
184
+ });
185
+
186
+ export async function POST(req: Request) {
187
+ const { messages }: { messages: UIMessage[] } = await req.json();
188
+
189
+ const result = await ai.streamText({
190
+ model: openai("gpt-4"),
191
+ messages: await convertToModelMessages(messages),
192
+ });
193
+
194
+ // Return streaming response for Next.js
195
+ return result.toUIMessageStreamResponse();
196
+ }
197
+ ```
198
+
199
+ #### Client-Side with React (useChat Hook)
200
+
201
+ ```typescript
202
+ // app/page.tsx
203
+ "use client";
204
+ import { useChat } from "@ai-sdk/react";
205
+
206
+ export default function Chat() {
207
+ const { messages, input, handleInputChange, handleSubmit } = useChat({
208
+ api: "/api/chat",
209
+ });
210
+
211
+ return (
212
+ <div>
213
+ {messages.map((message) => (
214
+ <div key={message.id}>{message.content}</div>
215
+ ))}
216
+ <form onSubmit={handleSubmit}>
217
+ <input value={input} onChange={handleInputChange} />
218
+ <button type="submit">Send</button>
219
+ </form>
220
+ </div>
221
+ );
222
+ }
223
+ ```
224
+
225
+ #### With Tools/Function Calling
226
+
227
+ Observa automatically tracks tool calls:
228
+
229
+ ```typescript
230
+ import { z } from "zod";
231
+
232
+ const result = await ai.streamText({
233
+ model: openai("gpt-4"),
234
+ messages: [...],
235
+ tools: {
236
+ getWeather: {
237
+ description: "Get the weather for a location",
238
+ parameters: z.object({
239
+ location: z.string(),
240
+ }),
241
+ execute: async ({ location }) => {
242
+ // Tool implementation - automatically tracked by Observa
243
+ return { temperature: 72, condition: "sunny" };
244
+ },
245
+ },
246
+ },
247
+ });
248
+ ```
249
+
250
+ #### Model Format Options
251
+
252
+ Vercel AI SDK supports two model formats:
253
+
254
+ 1. **Provider function** (recommended):
255
+ ```typescript
256
+ import { openai } from "@ai-sdk/openai";
257
+ import { anthropic } from "@ai-sdk/anthropic";
258
+
259
+ model: openai("gpt-4")
260
+ model: anthropic("claude-3-opus-20240229")
261
+ ```
262
+
263
+ 2. **String format** (for AI Gateway):
264
+ ```typescript
265
+ model: "openai/gpt-4"
266
+ model: "anthropic/claude-3-opus-20240229"
267
+ ```
268
+
269
+ #### Error Handling
270
+
271
+ Errors are automatically tracked:
272
+
273
+ ```typescript
274
+ try {
275
+ const result = await ai.generateText({
276
+ model: openai("gpt-4"),
277
+ prompt: "Hello!",
278
+ });
279
+ } catch (error) {
280
+ // Error is automatically tracked in Observa
281
+ console.error("LLM call failed:", error);
282
+ }
283
+ ```
284
+
155
285
  ### Legacy Manual Tracking
156
286
 
157
287
  For more control, you can still use the manual `track()` method:
package/dist/index.cjs CHANGED
@@ -320,6 +320,11 @@ function observeOpenAI(client, options) {
320
320
  if (proxyCache.has(client)) {
321
321
  return proxyCache.get(client);
322
322
  }
323
+ if (!options?.observa) {
324
+ console.error(
325
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeOpenAI() instead.\n\n\u274C WRONG (importing directly):\n import { observeOpenAI } from 'observa-sdk/instrumentation';\n const wrapped = observeOpenAI(openai);\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const wrapped = observa.observeOpenAI(openai);\n"
326
+ );
327
+ }
323
328
  try {
324
329
  const wrapped = new Proxy(client, {
325
330
  get(target, prop, receiver) {
@@ -402,7 +407,13 @@ function recordTrace(req, res, start, opts, timeToFirstToken, streamingDuration)
402
407
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
403
408
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
404
409
  const otelAttributes = mapOpenAIToOTEL(sanitizedReq, sanitizedRes);
405
- if (opts?.observa) {
410
+ if (!opts?.observa) {
411
+ console.error(
412
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeOpenAI(). Tracking is disabled. Make sure you're using observa.observeOpenAI() instead of importing observeOpenAI directly from 'observa-sdk/instrumentation'."
413
+ );
414
+ return;
415
+ }
416
+ if (opts.observa) {
406
417
  const inputText = sanitizedReq.messages?.map((m) => m.content).filter(Boolean).join("\n") || null;
407
418
  const outputText = sanitizedRes?.choices?.[0]?.message?.content || null;
408
419
  const finishReason = sanitizedRes?.choices?.[0]?.finish_reason || null;
@@ -480,7 +491,13 @@ function recordError(req, error, start, opts, preExtractedInputText, preExtracte
480
491
  try {
481
492
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
482
493
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
483
- if (opts?.observa) {
494
+ if (!opts?.observa) {
495
+ console.error(
496
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeOpenAI(). Error tracking is disabled. Make sure you're using observa.observeOpenAI() instead of importing observeOpenAI directly from 'observa-sdk/instrumentation'."
497
+ );
498
+ return;
499
+ }
500
+ if (opts.observa) {
484
501
  const model = preExtractedModel || sanitizedReq.model || "unknown";
485
502
  let inputText = preExtractedInputText || null;
486
503
  let inputMessages = preExtractedInputMessages || null;
@@ -537,6 +554,11 @@ function observeAnthropic(client, options) {
537
554
  if (proxyCache2.has(client)) {
538
555
  return proxyCache2.get(client);
539
556
  }
557
+ if (!options?.observa) {
558
+ console.error(
559
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeAnthropic() instead.\n\n\u274C WRONG (importing directly):\n import { observeAnthropic } from 'observa-sdk/instrumentation';\n const wrapped = observeAnthropic(anthropic);\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const wrapped = observa.observeAnthropic(anthropic);\n"
560
+ );
561
+ }
540
562
  try {
541
563
  const wrapped = new Proxy(client, {
542
564
  get(target, prop, receiver) {
@@ -624,7 +646,13 @@ function recordTrace2(req, res, start, opts, timeToFirstToken, streamingDuration
624
646
  const context = getTraceContext();
625
647
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
626
648
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
627
- if (opts?.observa) {
649
+ if (!opts?.observa) {
650
+ console.error(
651
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeAnthropic(). Tracking is disabled. Make sure you're using observa.observeAnthropic() instead of importing observeAnthropic directly from 'observa-sdk/instrumentation'."
652
+ );
653
+ return;
654
+ }
655
+ if (opts.observa) {
628
656
  const inputText = sanitizedReq.messages?.map((m) => {
629
657
  if (typeof m.content === "string") return m.content;
630
658
  if (Array.isArray(m.content)) {
@@ -708,7 +736,13 @@ function recordError2(req, error, start, opts, preExtractedInputText, preExtract
708
736
  try {
709
737
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
710
738
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
711
- if (opts?.observa) {
739
+ if (!opts?.observa) {
740
+ console.error(
741
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeAnthropic(). Error tracking is disabled. Make sure you're using observa.observeAnthropic() instead of importing observeAnthropic directly from 'observa-sdk/instrumentation'."
742
+ );
743
+ return;
744
+ }
745
+ if (opts.observa) {
712
746
  const model = preExtractedModel || sanitizedReq.model || "unknown";
713
747
  let inputText = preExtractedInputText || null;
714
748
  let inputMessages = preExtractedInputMessages || null;
@@ -905,7 +939,11 @@ function wrapReadableStream(stream, onComplete, onError) {
905
939
  const timeoutPromise = new Promise((_, reject) => {
906
940
  timeoutId = setTimeout(() => {
907
941
  reader.cancel();
908
- reject(new Error(`Stream timeout after ${timeoutMs}ms - no response received`));
942
+ reject(
943
+ new Error(
944
+ `Stream timeout after ${timeoutMs}ms - no response received`
945
+ )
946
+ );
909
947
  }, timeoutMs);
910
948
  });
911
949
  while (true) {
@@ -945,7 +983,10 @@ function wrapReadableStream(stream, onComplete, onError) {
945
983
  }
946
984
  const fullText = chunks.join("");
947
985
  if (chunks.length === 0 || !fullText || fullText.trim().length === 0) {
948
- console.error("[Observa] Empty response detected:", { chunks: chunks.length, fullTextLength: fullText?.length || 0 });
986
+ console.error("[Observa] Empty response detected:", {
987
+ chunks: chunks.length,
988
+ fullTextLength: fullText?.length || 0
989
+ });
949
990
  onError({
950
991
  name: "EmptyResponseError",
951
992
  message: "AI returned empty response",
@@ -956,11 +997,16 @@ function wrapReadableStream(stream, onComplete, onError) {
956
997
  });
957
998
  return;
958
999
  }
959
- onComplete({
960
- text: fullText,
961
- timeToFirstToken: firstTokenTime ? firstTokenTime - streamStartTime : null,
962
- streamingDuration: firstTokenTime ? Date.now() - firstTokenTime : null,
963
- totalLatency: Date.now() - streamStartTime
1000
+ Promise.resolve(
1001
+ onComplete({
1002
+ text: fullText,
1003
+ timeToFirstToken: firstTokenTime ? firstTokenTime - streamStartTime : null,
1004
+ streamingDuration: firstTokenTime ? Date.now() - firstTokenTime : null,
1005
+ totalLatency: Date.now() - streamStartTime
1006
+ })
1007
+ ).catch((e) => {
1008
+ console.error("[Observa] Error in onComplete callback:", e);
1009
+ onError(e);
964
1010
  });
965
1011
  } catch (error) {
966
1012
  if (timeoutId) {
@@ -1003,14 +1049,33 @@ async function traceStreamText(originalFn, args, options) {
1003
1049
  if (isReadableStream) {
1004
1050
  const wrappedStream = wrapReadableStream(
1005
1051
  originalTextStream,
1006
- (fullResponse) => {
1052
+ async (fullResponse) => {
1053
+ let usage = {};
1054
+ try {
1055
+ if (result.usage) {
1056
+ usage = await Promise.resolve(result.usage);
1057
+ }
1058
+ if (!usage || Object.keys(usage).length === 0) {
1059
+ if (result.fullResponse?.usage) {
1060
+ usage = await Promise.resolve(result.fullResponse.usage);
1061
+ }
1062
+ }
1063
+ } catch (e) {
1064
+ console.warn("[Observa] Failed to extract usage from result:", e);
1065
+ }
1007
1066
  recordTrace3(
1008
1067
  {
1009
1068
  model: modelIdentifier,
1010
1069
  prompt: requestParams.prompt || requestParams.messages || null,
1011
1070
  messages: requestParams.messages || null
1012
1071
  },
1013
- fullResponse,
1072
+ {
1073
+ ...fullResponse,
1074
+ usage,
1075
+ finishReason: result.finishReason || null,
1076
+ responseId: result.response?.id || null,
1077
+ model: result.model ? extractModelIdentifier(result.model) : modelIdentifier
1078
+ },
1014
1079
  startTime,
1015
1080
  options,
1016
1081
  fullResponse.timeToFirstToken,
@@ -1083,76 +1148,37 @@ function recordTrace3(req, res, start, opts, timeToFirstToken, streamingDuration
1083
1148
  try {
1084
1149
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
1085
1150
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
1086
- if (opts?.observa) {
1087
- let inputText = null;
1088
- if (sanitizedReq.prompt) {
1089
- inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1090
- } else if (sanitizedReq.messages) {
1091
- inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1092
- }
1093
- const outputText = sanitizedRes.text || sanitizedRes.content || null;
1094
- const finishReason = sanitizedRes.finishReason || null;
1095
- const isEmptyResponse = !outputText || typeof outputText === "string" && outputText.trim().length === 0;
1096
- const isFailureFinishReason = finishReason === "content_filter" || finishReason === "length" || finishReason === "max_tokens";
1097
- if (isEmptyResponse || isFailureFinishReason) {
1098
- const usage2 = sanitizedRes.usage || {};
1099
- const inputTokens2 = usage2.promptTokens || usage2.inputTokens || null;
1100
- const outputTokens2 = usage2.completionTokens || usage2.outputTokens || null;
1101
- const totalTokens2 = usage2.totalTokens || null;
1102
- opts.observa.trackLLMCall({
1103
- model: sanitizedReq.model || sanitizedRes.model || "unknown",
1104
- input: inputText,
1105
- output: null,
1106
- // No output on error
1107
- inputMessages: sanitizedReq.messages || null,
1108
- outputMessages: null,
1109
- inputTokens: inputTokens2,
1110
- outputTokens: outputTokens2,
1111
- totalTokens: totalTokens2,
1112
- latencyMs: duration,
1113
- timeToFirstTokenMs: timeToFirstToken || null,
1114
- streamingDurationMs: streamingDuration || null,
1115
- finishReason,
1116
- responseId: sanitizedRes.responseId || sanitizedRes.id || null,
1117
- operationName: "generate_text",
1118
- providerName: provider || "vercel-ai",
1119
- responseModel: sanitizedRes.model || sanitizedReq.model || null,
1120
- temperature: sanitizedReq.temperature || null,
1121
- maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1122
- });
1123
- const errorType = isEmptyResponse ? "empty_response" : finishReason === "content_filter" ? "content_filtered" : "response_truncated";
1124
- const errorMessage = isEmptyResponse ? "AI returned empty response" : finishReason === "content_filter" ? "AI response was filtered due to content policy" : "AI response was truncated due to token limit";
1125
- opts.observa.trackError({
1126
- errorType,
1127
- errorMessage,
1128
- stackTrace: null,
1129
- context: {
1130
- request: sanitizedReq,
1131
- response: sanitizedRes,
1132
- model: sanitizedReq.model || sanitizedRes.model || "unknown",
1133
- input: inputText,
1134
- finish_reason: finishReason,
1135
- provider: provider || "vercel-ai",
1136
- duration_ms: duration
1137
- },
1138
- errorCategory: finishReason === "content_filter" ? "validation_error" : finishReason === "length" || finishReason === "max_tokens" ? "model_error" : "unknown_error",
1139
- errorCode: isEmptyResponse ? "empty_response" : finishReason
1140
- });
1141
- return;
1142
- }
1143
- const usage = sanitizedRes.usage || {};
1144
- const inputTokens = usage.promptTokens || usage.inputTokens || null;
1145
- const outputTokens = usage.completionTokens || usage.outputTokens || null;
1146
- const totalTokens = usage.totalTokens || null;
1151
+ if (!opts?.observa) {
1152
+ console.error(
1153
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeVercelAI(). Tracking is disabled. Make sure you're using observa.observeVercelAI() instead of importing observeVercelAI directly from 'observa-sdk/instrumentation'."
1154
+ );
1155
+ return;
1156
+ }
1157
+ let inputText = null;
1158
+ if (sanitizedReq.prompt) {
1159
+ inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1160
+ } else if (sanitizedReq.messages) {
1161
+ inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1162
+ }
1163
+ const outputText = sanitizedRes.text || sanitizedRes.content || null;
1164
+ const finishReason = sanitizedRes.finishReason || null;
1165
+ const isEmptyResponse = !outputText || typeof outputText === "string" && outputText.trim().length === 0;
1166
+ const isFailureFinishReason = finishReason === "content_filter" || finishReason === "length" || finishReason === "max_tokens";
1167
+ if (isEmptyResponse || isFailureFinishReason) {
1168
+ const usage2 = sanitizedRes.usage || {};
1169
+ const inputTokens2 = usage2.promptTokens || usage2.inputTokens || null;
1170
+ const outputTokens2 = usage2.completionTokens || usage2.outputTokens || null;
1171
+ const totalTokens2 = usage2.totalTokens || null;
1147
1172
  opts.observa.trackLLMCall({
1148
1173
  model: sanitizedReq.model || sanitizedRes.model || "unknown",
1149
1174
  input: inputText,
1150
- output: outputText,
1175
+ output: null,
1176
+ // No output on error
1151
1177
  inputMessages: sanitizedReq.messages || null,
1152
- outputMessages: sanitizedRes.messages || null,
1153
- inputTokens,
1154
- outputTokens,
1155
- totalTokens,
1178
+ outputMessages: null,
1179
+ inputTokens: inputTokens2,
1180
+ outputTokens: outputTokens2,
1181
+ totalTokens: totalTokens2,
1156
1182
  latencyMs: duration,
1157
1183
  timeToFirstTokenMs: timeToFirstToken || null,
1158
1184
  streamingDurationMs: streamingDuration || null,
@@ -1164,7 +1190,50 @@ function recordTrace3(req, res, start, opts, timeToFirstToken, streamingDuration
1164
1190
  temperature: sanitizedReq.temperature || null,
1165
1191
  maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1166
1192
  });
1193
+ const errorType = isEmptyResponse ? "empty_response" : finishReason === "content_filter" ? "content_filtered" : "response_truncated";
1194
+ const errorMessage = isEmptyResponse ? "AI returned empty response" : finishReason === "content_filter" ? "AI response was filtered due to content policy" : "AI response was truncated due to token limit";
1195
+ opts.observa.trackError({
1196
+ errorType,
1197
+ errorMessage,
1198
+ stackTrace: null,
1199
+ context: {
1200
+ request: sanitizedReq,
1201
+ response: sanitizedRes,
1202
+ model: sanitizedReq.model || sanitizedRes.model || "unknown",
1203
+ input: inputText,
1204
+ finish_reason: finishReason,
1205
+ provider: provider || "vercel-ai",
1206
+ duration_ms: duration
1207
+ },
1208
+ errorCategory: finishReason === "content_filter" ? "validation_error" : finishReason === "length" || finishReason === "max_tokens" ? "model_error" : "unknown_error",
1209
+ errorCode: isEmptyResponse ? "empty_response" : finishReason
1210
+ });
1211
+ return;
1167
1212
  }
1213
+ const usage = sanitizedRes.usage || {};
1214
+ const inputTokens = usage.promptTokens || usage.inputTokens || null;
1215
+ const outputTokens = usage.completionTokens || usage.outputTokens || null;
1216
+ const totalTokens = usage.totalTokens || null;
1217
+ opts.observa.trackLLMCall({
1218
+ model: sanitizedReq.model || sanitizedRes.model || "unknown",
1219
+ input: inputText,
1220
+ output: outputText,
1221
+ inputMessages: sanitizedReq.messages || null,
1222
+ outputMessages: sanitizedRes.messages || null,
1223
+ inputTokens,
1224
+ outputTokens,
1225
+ totalTokens,
1226
+ latencyMs: duration,
1227
+ timeToFirstTokenMs: timeToFirstToken || null,
1228
+ streamingDurationMs: streamingDuration || null,
1229
+ finishReason,
1230
+ responseId: sanitizedRes.responseId || sanitizedRes.id || null,
1231
+ operationName: "generate_text",
1232
+ providerName: provider || "vercel-ai",
1233
+ responseModel: sanitizedRes.model || sanitizedReq.model || null,
1234
+ temperature: sanitizedReq.temperature || null,
1235
+ maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1236
+ });
1168
1237
  } catch (e) {
1169
1238
  console.error("[Observa] Failed to record trace", e);
1170
1239
  }
@@ -1174,63 +1243,72 @@ function recordError3(req, error, start, opts, provider, preExtractedInputText,
1174
1243
  try {
1175
1244
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
1176
1245
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
1177
- if (opts?.observa) {
1178
- const model = sanitizedReq.model || "unknown";
1179
- let inputText = preExtractedInputText || null;
1180
- let inputMessages = preExtractedInputMessages || null;
1181
- if (!inputText) {
1182
- if (sanitizedReq.prompt) {
1183
- inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1184
- } else if (sanitizedReq.messages) {
1185
- inputMessages = sanitizedReq.messages;
1186
- inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1187
- }
1246
+ if (!opts?.observa) {
1247
+ console.error(
1248
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeVercelAI(). Error tracking is disabled. Make sure you're using observa.observeVercelAI() instead of importing observeVercelAI directly from 'observa-sdk/instrumentation'."
1249
+ );
1250
+ return;
1251
+ }
1252
+ const model = sanitizedReq.model || "unknown";
1253
+ let inputText = preExtractedInputText || null;
1254
+ let inputMessages = preExtractedInputMessages || null;
1255
+ if (!inputText) {
1256
+ if (sanitizedReq.prompt) {
1257
+ inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1258
+ } else if (sanitizedReq.messages) {
1259
+ inputMessages = sanitizedReq.messages;
1260
+ inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1188
1261
  }
1189
- const providerName = provider || "vercel-ai";
1190
- const extractedError = extractProviderError(error, providerName);
1191
- opts.observa.trackLLMCall({
1262
+ }
1263
+ const providerName = provider || "vercel-ai";
1264
+ const extractedError = extractProviderError(error, providerName);
1265
+ opts.observa.trackLLMCall({
1266
+ model,
1267
+ input: inputText,
1268
+ output: null,
1269
+ // No output on error
1270
+ inputMessages,
1271
+ outputMessages: null,
1272
+ inputTokens: null,
1273
+ outputTokens: null,
1274
+ totalTokens: null,
1275
+ latencyMs: duration,
1276
+ timeToFirstTokenMs: null,
1277
+ streamingDurationMs: null,
1278
+ finishReason: null,
1279
+ responseId: null,
1280
+ operationName: "generate_text",
1281
+ providerName,
1282
+ responseModel: model,
1283
+ temperature: sanitizedReq.temperature || null,
1284
+ maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1285
+ });
1286
+ opts.observa.trackError({
1287
+ errorType: error?.name || extractedError.code || "UnknownError",
1288
+ errorMessage: extractedError.message,
1289
+ stackTrace: error?.stack || null,
1290
+ context: {
1291
+ request: sanitizedReq,
1192
1292
  model,
1193
1293
  input: inputText,
1194
- output: null,
1195
- // No output on error
1196
- inputMessages,
1197
- outputMessages: null,
1198
- inputTokens: null,
1199
- outputTokens: null,
1200
- totalTokens: null,
1201
- latencyMs: duration,
1202
- timeToFirstTokenMs: null,
1203
- streamingDurationMs: null,
1204
- finishReason: null,
1205
- responseId: null,
1206
- operationName: "generate_text",
1207
- providerName,
1208
- responseModel: model,
1209
- temperature: sanitizedReq.temperature || null,
1210
- maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1211
- });
1212
- opts.observa.trackError({
1213
- errorType: error?.name || extractedError.code || "UnknownError",
1214
- errorMessage: extractedError.message,
1215
- stackTrace: error?.stack || null,
1216
- context: {
1217
- request: sanitizedReq,
1218
- model,
1219
- input: inputText,
1220
- provider: providerName,
1221
- duration_ms: duration,
1222
- status_code: extractedError.statusCode || null
1223
- },
1224
- errorCategory: extractedError.category,
1225
- errorCode: extractedError.code
1226
- });
1227
- }
1294
+ provider: providerName,
1295
+ duration_ms: duration,
1296
+ status_code: extractedError.statusCode || null
1297
+ },
1298
+ errorCategory: extractedError.category,
1299
+ errorCode: extractedError.code
1300
+ });
1228
1301
  } catch (e) {
1229
1302
  console.error("[Observa] Failed to record error", e);
1230
1303
  }
1231
1304
  }
1232
1305
  function observeVercelAI(aiSdk, options) {
1233
1306
  try {
1307
+ if (!options?.observa) {
1308
+ console.error(
1309
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeVercelAI() instead.\n\n\u274C WRONG (importing directly):\n import { observeVercelAI } from 'observa-sdk/instrumentation';\n const ai = observeVercelAI({ generateText, streamText });\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const ai = observa.observeVercelAI({ generateText, streamText });\n"
1310
+ );
1311
+ }
1234
1312
  const wrapped = { ...aiSdk };
1235
1313
  if (aiSdk.generateText && typeof aiSdk.generateText === "function") {
1236
1314
  wrapped.generateText = async function(...args) {
@@ -1663,7 +1741,7 @@ var Observa = class {
1663
1741
  }
1664
1742
  }
1665
1743
  const operationName = options.operationName || "chat";
1666
- const isError = options.output === null || typeof options.output === "string" && options.output.trim().length === 0 || (options.finishReason === "content_filter" || options.finishReason === "length" || options.finishReason === "max_tokens");
1744
+ const isError = options.output === null || typeof options.output === "string" && options.output.trim().length === 0 || options.finishReason === "content_filter" || options.finishReason === "length" || options.finishReason === "max_tokens";
1667
1745
  this.addEvent({
1668
1746
  event_type: "llm_call",
1669
1747
  span_id: spanId,
package/dist/index.js CHANGED
@@ -300,6 +300,11 @@ function observeOpenAI(client, options) {
300
300
  if (proxyCache.has(client)) {
301
301
  return proxyCache.get(client);
302
302
  }
303
+ if (!options?.observa) {
304
+ console.error(
305
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeOpenAI() instead.\n\n\u274C WRONG (importing directly):\n import { observeOpenAI } from 'observa-sdk/instrumentation';\n const wrapped = observeOpenAI(openai);\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const wrapped = observa.observeOpenAI(openai);\n"
306
+ );
307
+ }
303
308
  try {
304
309
  const wrapped = new Proxy(client, {
305
310
  get(target, prop, receiver) {
@@ -382,7 +387,13 @@ function recordTrace(req, res, start, opts, timeToFirstToken, streamingDuration)
382
387
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
383
388
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
384
389
  const otelAttributes = mapOpenAIToOTEL(sanitizedReq, sanitizedRes);
385
- if (opts?.observa) {
390
+ if (!opts?.observa) {
391
+ console.error(
392
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeOpenAI(). Tracking is disabled. Make sure you're using observa.observeOpenAI() instead of importing observeOpenAI directly from 'observa-sdk/instrumentation'."
393
+ );
394
+ return;
395
+ }
396
+ if (opts.observa) {
386
397
  const inputText = sanitizedReq.messages?.map((m) => m.content).filter(Boolean).join("\n") || null;
387
398
  const outputText = sanitizedRes?.choices?.[0]?.message?.content || null;
388
399
  const finishReason = sanitizedRes?.choices?.[0]?.finish_reason || null;
@@ -460,7 +471,13 @@ function recordError(req, error, start, opts, preExtractedInputText, preExtracte
460
471
  try {
461
472
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
462
473
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
463
- if (opts?.observa) {
474
+ if (!opts?.observa) {
475
+ console.error(
476
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeOpenAI(). Error tracking is disabled. Make sure you're using observa.observeOpenAI() instead of importing observeOpenAI directly from 'observa-sdk/instrumentation'."
477
+ );
478
+ return;
479
+ }
480
+ if (opts.observa) {
464
481
  const model = preExtractedModel || sanitizedReq.model || "unknown";
465
482
  let inputText = preExtractedInputText || null;
466
483
  let inputMessages = preExtractedInputMessages || null;
@@ -517,6 +534,11 @@ function observeAnthropic(client, options) {
517
534
  if (proxyCache2.has(client)) {
518
535
  return proxyCache2.get(client);
519
536
  }
537
+ if (!options?.observa) {
538
+ console.error(
539
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeAnthropic() instead.\n\n\u274C WRONG (importing directly):\n import { observeAnthropic } from 'observa-sdk/instrumentation';\n const wrapped = observeAnthropic(anthropic);\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const wrapped = observa.observeAnthropic(anthropic);\n"
540
+ );
541
+ }
520
542
  try {
521
543
  const wrapped = new Proxy(client, {
522
544
  get(target, prop, receiver) {
@@ -604,7 +626,13 @@ function recordTrace2(req, res, start, opts, timeToFirstToken, streamingDuration
604
626
  const context = getTraceContext();
605
627
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
606
628
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
607
- if (opts?.observa) {
629
+ if (!opts?.observa) {
630
+ console.error(
631
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeAnthropic(). Tracking is disabled. Make sure you're using observa.observeAnthropic() instead of importing observeAnthropic directly from 'observa-sdk/instrumentation'."
632
+ );
633
+ return;
634
+ }
635
+ if (opts.observa) {
608
636
  const inputText = sanitizedReq.messages?.map((m) => {
609
637
  if (typeof m.content === "string") return m.content;
610
638
  if (Array.isArray(m.content)) {
@@ -688,7 +716,13 @@ function recordError2(req, error, start, opts, preExtractedInputText, preExtract
688
716
  try {
689
717
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
690
718
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
691
- if (opts?.observa) {
719
+ if (!opts?.observa) {
720
+ console.error(
721
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeAnthropic(). Error tracking is disabled. Make sure you're using observa.observeAnthropic() instead of importing observeAnthropic directly from 'observa-sdk/instrumentation'."
722
+ );
723
+ return;
724
+ }
725
+ if (opts.observa) {
692
726
  const model = preExtractedModel || sanitizedReq.model || "unknown";
693
727
  let inputText = preExtractedInputText || null;
694
728
  let inputMessages = preExtractedInputMessages || null;
@@ -885,7 +919,11 @@ function wrapReadableStream(stream, onComplete, onError) {
885
919
  const timeoutPromise = new Promise((_, reject) => {
886
920
  timeoutId = setTimeout(() => {
887
921
  reader.cancel();
888
- reject(new Error(`Stream timeout after ${timeoutMs}ms - no response received`));
922
+ reject(
923
+ new Error(
924
+ `Stream timeout after ${timeoutMs}ms - no response received`
925
+ )
926
+ );
889
927
  }, timeoutMs);
890
928
  });
891
929
  while (true) {
@@ -925,7 +963,10 @@ function wrapReadableStream(stream, onComplete, onError) {
925
963
  }
926
964
  const fullText = chunks.join("");
927
965
  if (chunks.length === 0 || !fullText || fullText.trim().length === 0) {
928
- console.error("[Observa] Empty response detected:", { chunks: chunks.length, fullTextLength: fullText?.length || 0 });
966
+ console.error("[Observa] Empty response detected:", {
967
+ chunks: chunks.length,
968
+ fullTextLength: fullText?.length || 0
969
+ });
929
970
  onError({
930
971
  name: "EmptyResponseError",
931
972
  message: "AI returned empty response",
@@ -936,11 +977,16 @@ function wrapReadableStream(stream, onComplete, onError) {
936
977
  });
937
978
  return;
938
979
  }
939
- onComplete({
940
- text: fullText,
941
- timeToFirstToken: firstTokenTime ? firstTokenTime - streamStartTime : null,
942
- streamingDuration: firstTokenTime ? Date.now() - firstTokenTime : null,
943
- totalLatency: Date.now() - streamStartTime
980
+ Promise.resolve(
981
+ onComplete({
982
+ text: fullText,
983
+ timeToFirstToken: firstTokenTime ? firstTokenTime - streamStartTime : null,
984
+ streamingDuration: firstTokenTime ? Date.now() - firstTokenTime : null,
985
+ totalLatency: Date.now() - streamStartTime
986
+ })
987
+ ).catch((e) => {
988
+ console.error("[Observa] Error in onComplete callback:", e);
989
+ onError(e);
944
990
  });
945
991
  } catch (error) {
946
992
  if (timeoutId) {
@@ -983,14 +1029,33 @@ async function traceStreamText(originalFn, args, options) {
983
1029
  if (isReadableStream) {
984
1030
  const wrappedStream = wrapReadableStream(
985
1031
  originalTextStream,
986
- (fullResponse) => {
1032
+ async (fullResponse) => {
1033
+ let usage = {};
1034
+ try {
1035
+ if (result.usage) {
1036
+ usage = await Promise.resolve(result.usage);
1037
+ }
1038
+ if (!usage || Object.keys(usage).length === 0) {
1039
+ if (result.fullResponse?.usage) {
1040
+ usage = await Promise.resolve(result.fullResponse.usage);
1041
+ }
1042
+ }
1043
+ } catch (e) {
1044
+ console.warn("[Observa] Failed to extract usage from result:", e);
1045
+ }
987
1046
  recordTrace3(
988
1047
  {
989
1048
  model: modelIdentifier,
990
1049
  prompt: requestParams.prompt || requestParams.messages || null,
991
1050
  messages: requestParams.messages || null
992
1051
  },
993
- fullResponse,
1052
+ {
1053
+ ...fullResponse,
1054
+ usage,
1055
+ finishReason: result.finishReason || null,
1056
+ responseId: result.response?.id || null,
1057
+ model: result.model ? extractModelIdentifier(result.model) : modelIdentifier
1058
+ },
994
1059
  startTime,
995
1060
  options,
996
1061
  fullResponse.timeToFirstToken,
@@ -1063,76 +1128,37 @@ function recordTrace3(req, res, start, opts, timeToFirstToken, streamingDuration
1063
1128
  try {
1064
1129
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
1065
1130
  const sanitizedRes = opts?.redact ? opts.redact(res) : res;
1066
- if (opts?.observa) {
1067
- let inputText = null;
1068
- if (sanitizedReq.prompt) {
1069
- inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1070
- } else if (sanitizedReq.messages) {
1071
- inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1072
- }
1073
- const outputText = sanitizedRes.text || sanitizedRes.content || null;
1074
- const finishReason = sanitizedRes.finishReason || null;
1075
- const isEmptyResponse = !outputText || typeof outputText === "string" && outputText.trim().length === 0;
1076
- const isFailureFinishReason = finishReason === "content_filter" || finishReason === "length" || finishReason === "max_tokens";
1077
- if (isEmptyResponse || isFailureFinishReason) {
1078
- const usage2 = sanitizedRes.usage || {};
1079
- const inputTokens2 = usage2.promptTokens || usage2.inputTokens || null;
1080
- const outputTokens2 = usage2.completionTokens || usage2.outputTokens || null;
1081
- const totalTokens2 = usage2.totalTokens || null;
1082
- opts.observa.trackLLMCall({
1083
- model: sanitizedReq.model || sanitizedRes.model || "unknown",
1084
- input: inputText,
1085
- output: null,
1086
- // No output on error
1087
- inputMessages: sanitizedReq.messages || null,
1088
- outputMessages: null,
1089
- inputTokens: inputTokens2,
1090
- outputTokens: outputTokens2,
1091
- totalTokens: totalTokens2,
1092
- latencyMs: duration,
1093
- timeToFirstTokenMs: timeToFirstToken || null,
1094
- streamingDurationMs: streamingDuration || null,
1095
- finishReason,
1096
- responseId: sanitizedRes.responseId || sanitizedRes.id || null,
1097
- operationName: "generate_text",
1098
- providerName: provider || "vercel-ai",
1099
- responseModel: sanitizedRes.model || sanitizedReq.model || null,
1100
- temperature: sanitizedReq.temperature || null,
1101
- maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1102
- });
1103
- const errorType = isEmptyResponse ? "empty_response" : finishReason === "content_filter" ? "content_filtered" : "response_truncated";
1104
- const errorMessage = isEmptyResponse ? "AI returned empty response" : finishReason === "content_filter" ? "AI response was filtered due to content policy" : "AI response was truncated due to token limit";
1105
- opts.observa.trackError({
1106
- errorType,
1107
- errorMessage,
1108
- stackTrace: null,
1109
- context: {
1110
- request: sanitizedReq,
1111
- response: sanitizedRes,
1112
- model: sanitizedReq.model || sanitizedRes.model || "unknown",
1113
- input: inputText,
1114
- finish_reason: finishReason,
1115
- provider: provider || "vercel-ai",
1116
- duration_ms: duration
1117
- },
1118
- errorCategory: finishReason === "content_filter" ? "validation_error" : finishReason === "length" || finishReason === "max_tokens" ? "model_error" : "unknown_error",
1119
- errorCode: isEmptyResponse ? "empty_response" : finishReason
1120
- });
1121
- return;
1122
- }
1123
- const usage = sanitizedRes.usage || {};
1124
- const inputTokens = usage.promptTokens || usage.inputTokens || null;
1125
- const outputTokens = usage.completionTokens || usage.outputTokens || null;
1126
- const totalTokens = usage.totalTokens || null;
1131
+ if (!opts?.observa) {
1132
+ console.error(
1133
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeVercelAI(). Tracking is disabled. Make sure you're using observa.observeVercelAI() instead of importing observeVercelAI directly from 'observa-sdk/instrumentation'."
1134
+ );
1135
+ return;
1136
+ }
1137
+ let inputText = null;
1138
+ if (sanitizedReq.prompt) {
1139
+ inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1140
+ } else if (sanitizedReq.messages) {
1141
+ inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1142
+ }
1143
+ const outputText = sanitizedRes.text || sanitizedRes.content || null;
1144
+ const finishReason = sanitizedRes.finishReason || null;
1145
+ const isEmptyResponse = !outputText || typeof outputText === "string" && outputText.trim().length === 0;
1146
+ const isFailureFinishReason = finishReason === "content_filter" || finishReason === "length" || finishReason === "max_tokens";
1147
+ if (isEmptyResponse || isFailureFinishReason) {
1148
+ const usage2 = sanitizedRes.usage || {};
1149
+ const inputTokens2 = usage2.promptTokens || usage2.inputTokens || null;
1150
+ const outputTokens2 = usage2.completionTokens || usage2.outputTokens || null;
1151
+ const totalTokens2 = usage2.totalTokens || null;
1127
1152
  opts.observa.trackLLMCall({
1128
1153
  model: sanitizedReq.model || sanitizedRes.model || "unknown",
1129
1154
  input: inputText,
1130
- output: outputText,
1155
+ output: null,
1156
+ // No output on error
1131
1157
  inputMessages: sanitizedReq.messages || null,
1132
- outputMessages: sanitizedRes.messages || null,
1133
- inputTokens,
1134
- outputTokens,
1135
- totalTokens,
1158
+ outputMessages: null,
1159
+ inputTokens: inputTokens2,
1160
+ outputTokens: outputTokens2,
1161
+ totalTokens: totalTokens2,
1136
1162
  latencyMs: duration,
1137
1163
  timeToFirstTokenMs: timeToFirstToken || null,
1138
1164
  streamingDurationMs: streamingDuration || null,
@@ -1144,7 +1170,50 @@ function recordTrace3(req, res, start, opts, timeToFirstToken, streamingDuration
1144
1170
  temperature: sanitizedReq.temperature || null,
1145
1171
  maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1146
1172
  });
1173
+ const errorType = isEmptyResponse ? "empty_response" : finishReason === "content_filter" ? "content_filtered" : "response_truncated";
1174
+ const errorMessage = isEmptyResponse ? "AI returned empty response" : finishReason === "content_filter" ? "AI response was filtered due to content policy" : "AI response was truncated due to token limit";
1175
+ opts.observa.trackError({
1176
+ errorType,
1177
+ errorMessage,
1178
+ stackTrace: null,
1179
+ context: {
1180
+ request: sanitizedReq,
1181
+ response: sanitizedRes,
1182
+ model: sanitizedReq.model || sanitizedRes.model || "unknown",
1183
+ input: inputText,
1184
+ finish_reason: finishReason,
1185
+ provider: provider || "vercel-ai",
1186
+ duration_ms: duration
1187
+ },
1188
+ errorCategory: finishReason === "content_filter" ? "validation_error" : finishReason === "length" || finishReason === "max_tokens" ? "model_error" : "unknown_error",
1189
+ errorCode: isEmptyResponse ? "empty_response" : finishReason
1190
+ });
1191
+ return;
1147
1192
  }
1193
+ const usage = sanitizedRes.usage || {};
1194
+ const inputTokens = usage.promptTokens || usage.inputTokens || null;
1195
+ const outputTokens = usage.completionTokens || usage.outputTokens || null;
1196
+ const totalTokens = usage.totalTokens || null;
1197
+ opts.observa.trackLLMCall({
1198
+ model: sanitizedReq.model || sanitizedRes.model || "unknown",
1199
+ input: inputText,
1200
+ output: outputText,
1201
+ inputMessages: sanitizedReq.messages || null,
1202
+ outputMessages: sanitizedRes.messages || null,
1203
+ inputTokens,
1204
+ outputTokens,
1205
+ totalTokens,
1206
+ latencyMs: duration,
1207
+ timeToFirstTokenMs: timeToFirstToken || null,
1208
+ streamingDurationMs: streamingDuration || null,
1209
+ finishReason,
1210
+ responseId: sanitizedRes.responseId || sanitizedRes.id || null,
1211
+ operationName: "generate_text",
1212
+ providerName: provider || "vercel-ai",
1213
+ responseModel: sanitizedRes.model || sanitizedReq.model || null,
1214
+ temperature: sanitizedReq.temperature || null,
1215
+ maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1216
+ });
1148
1217
  } catch (e) {
1149
1218
  console.error("[Observa] Failed to record trace", e);
1150
1219
  }
@@ -1154,63 +1223,72 @@ function recordError3(req, error, start, opts, provider, preExtractedInputText,
1154
1223
  try {
1155
1224
  console.error("[Observa] \u26A0\uFE0F Error Captured:", error?.message || error);
1156
1225
  const sanitizedReq = opts?.redact ? opts.redact(req) : req;
1157
- if (opts?.observa) {
1158
- const model = sanitizedReq.model || "unknown";
1159
- let inputText = preExtractedInputText || null;
1160
- let inputMessages = preExtractedInputMessages || null;
1161
- if (!inputText) {
1162
- if (sanitizedReq.prompt) {
1163
- inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1164
- } else if (sanitizedReq.messages) {
1165
- inputMessages = sanitizedReq.messages;
1166
- inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1167
- }
1226
+ if (!opts?.observa) {
1227
+ console.error(
1228
+ "[Observa] \u26A0\uFE0F CRITICAL: observa instance not provided to observeVercelAI(). Error tracking is disabled. Make sure you're using observa.observeVercelAI() instead of importing observeVercelAI directly from 'observa-sdk/instrumentation'."
1229
+ );
1230
+ return;
1231
+ }
1232
+ const model = sanitizedReq.model || "unknown";
1233
+ let inputText = preExtractedInputText || null;
1234
+ let inputMessages = preExtractedInputMessages || null;
1235
+ if (!inputText) {
1236
+ if (sanitizedReq.prompt) {
1237
+ inputText = typeof sanitizedReq.prompt === "string" ? sanitizedReq.prompt : JSON.stringify(sanitizedReq.prompt);
1238
+ } else if (sanitizedReq.messages) {
1239
+ inputMessages = sanitizedReq.messages;
1240
+ inputText = sanitizedReq.messages.map((m) => m.content || m.text || "").filter(Boolean).join("\n");
1168
1241
  }
1169
- const providerName = provider || "vercel-ai";
1170
- const extractedError = extractProviderError(error, providerName);
1171
- opts.observa.trackLLMCall({
1242
+ }
1243
+ const providerName = provider || "vercel-ai";
1244
+ const extractedError = extractProviderError(error, providerName);
1245
+ opts.observa.trackLLMCall({
1246
+ model,
1247
+ input: inputText,
1248
+ output: null,
1249
+ // No output on error
1250
+ inputMessages,
1251
+ outputMessages: null,
1252
+ inputTokens: null,
1253
+ outputTokens: null,
1254
+ totalTokens: null,
1255
+ latencyMs: duration,
1256
+ timeToFirstTokenMs: null,
1257
+ streamingDurationMs: null,
1258
+ finishReason: null,
1259
+ responseId: null,
1260
+ operationName: "generate_text",
1261
+ providerName,
1262
+ responseModel: model,
1263
+ temperature: sanitizedReq.temperature || null,
1264
+ maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1265
+ });
1266
+ opts.observa.trackError({
1267
+ errorType: error?.name || extractedError.code || "UnknownError",
1268
+ errorMessage: extractedError.message,
1269
+ stackTrace: error?.stack || null,
1270
+ context: {
1271
+ request: sanitizedReq,
1172
1272
  model,
1173
1273
  input: inputText,
1174
- output: null,
1175
- // No output on error
1176
- inputMessages,
1177
- outputMessages: null,
1178
- inputTokens: null,
1179
- outputTokens: null,
1180
- totalTokens: null,
1181
- latencyMs: duration,
1182
- timeToFirstTokenMs: null,
1183
- streamingDurationMs: null,
1184
- finishReason: null,
1185
- responseId: null,
1186
- operationName: "generate_text",
1187
- providerName,
1188
- responseModel: model,
1189
- temperature: sanitizedReq.temperature || null,
1190
- maxTokens: sanitizedReq.maxTokens || sanitizedReq.max_tokens || null
1191
- });
1192
- opts.observa.trackError({
1193
- errorType: error?.name || extractedError.code || "UnknownError",
1194
- errorMessage: extractedError.message,
1195
- stackTrace: error?.stack || null,
1196
- context: {
1197
- request: sanitizedReq,
1198
- model,
1199
- input: inputText,
1200
- provider: providerName,
1201
- duration_ms: duration,
1202
- status_code: extractedError.statusCode || null
1203
- },
1204
- errorCategory: extractedError.category,
1205
- errorCode: extractedError.code
1206
- });
1207
- }
1274
+ provider: providerName,
1275
+ duration_ms: duration,
1276
+ status_code: extractedError.statusCode || null
1277
+ },
1278
+ errorCategory: extractedError.category,
1279
+ errorCode: extractedError.code
1280
+ });
1208
1281
  } catch (e) {
1209
1282
  console.error("[Observa] Failed to record error", e);
1210
1283
  }
1211
1284
  }
1212
1285
  function observeVercelAI(aiSdk, options) {
1213
1286
  try {
1287
+ if (!options?.observa) {
1288
+ console.error(
1289
+ "[Observa] \u26A0\uFE0F CRITICAL ERROR: observa instance not provided!\n\nTracking will NOT work. You must use observa.observeVercelAI() instead.\n\n\u274C WRONG (importing directly):\n import { observeVercelAI } from 'observa-sdk/instrumentation';\n const ai = observeVercelAI({ generateText, streamText });\n\n\u2705 CORRECT (using instance method):\n import { init } from 'observa-sdk';\n const observa = init({ apiKey: '...' });\n const ai = observa.observeVercelAI({ generateText, streamText });\n"
1290
+ );
1291
+ }
1214
1292
  const wrapped = { ...aiSdk };
1215
1293
  if (aiSdk.generateText && typeof aiSdk.generateText === "function") {
1216
1294
  wrapped.generateText = async function(...args) {
@@ -1643,7 +1721,7 @@ var Observa = class {
1643
1721
  }
1644
1722
  }
1645
1723
  const operationName = options.operationName || "chat";
1646
- const isError = options.output === null || typeof options.output === "string" && options.output.trim().length === 0 || (options.finishReason === "content_filter" || options.finishReason === "length" || options.finishReason === "max_tokens");
1724
+ const isError = options.output === null || typeof options.output === "string" && options.output.trim().length === 0 || options.finishReason === "content_filter" || options.finishReason === "length" || options.finishReason === "max_tokens";
1647
1725
  this.addEvent({
1648
1726
  event_type: "llm_call",
1649
1727
  span_id: spanId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "observa-sdk",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Enterprise-grade observability SDK for AI applications. Track and monitor LLM interactions with zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",