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.
- package/README.md +131 -1
- package/dist/index.cjs +206 -128
- package/dist/index.js +206 -128
- 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(
|
|
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:", {
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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:
|
|
1175
|
+
output: null,
|
|
1176
|
+
// No output on error
|
|
1151
1177
|
inputMessages: sanitizedReq.messages || null,
|
|
1152
|
-
outputMessages:
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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 ||
|
|
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(
|
|
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:", {
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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:
|
|
1155
|
+
output: null,
|
|
1156
|
+
// No output on error
|
|
1131
1157
|
inputMessages: sanitizedReq.messages || null,
|
|
1132
|
-
outputMessages:
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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 ||
|
|
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