@observyze/sdk 0.1.2 → 0.1.4
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 +175 -10
- package/dist/chunk-FQBYUOJB.mjs +316 -0
- package/dist/{chunk-YRMQCX2P.mjs → chunk-YSYKPKKX.mjs} +5 -20
- package/dist/index--b41_E-1.d.mts +440 -0
- package/dist/index--b41_E-1.d.ts +440 -0
- package/dist/{index-DEorAmFu.d.mts → index-Cr-FN-y5.d.mts} +89 -89
- package/dist/{index-DEorAmFu.d.ts → index-Cr-FN-y5.d.ts} +89 -89
- package/dist/index-D4UXMom5.d.mts +429 -0
- package/dist/index-D4UXMom5.d.ts +429 -0
- package/dist/index-IZdiaORv.d.mts +477 -0
- package/dist/index-IZdiaORv.d.ts +477 -0
- package/dist/index.d.mts +4 -37
- package/dist/index.d.ts +4 -37
- package/dist/index.js +665 -349
- package/dist/index.mjs +396 -70
- package/dist/opentelemetry/index.d.mts +2 -1
- package/dist/opentelemetry/index.d.ts +2 -1
- package/dist/opentelemetry/index.js +7 -4
- package/dist/opentelemetry/index.mjs +1 -1
- package/package.json +13 -6
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __esm = (fn, res) => function __init() {
|
|
7
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
-
};
|
|
9
8
|
var __export = (target, all) => {
|
|
10
9
|
for (var name in all)
|
|
11
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -18,303 +17,38 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
17
|
}
|
|
19
18
|
return to;
|
|
20
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
21
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
29
|
|
|
23
|
-
// src/types.ts
|
|
24
|
-
var SpanType, TraceStatus;
|
|
25
|
-
var init_types = __esm({
|
|
26
|
-
"src/types.ts"() {
|
|
27
|
-
"use strict";
|
|
28
|
-
SpanType = /* @__PURE__ */ ((SpanType3) => {
|
|
29
|
-
SpanType3["LLM"] = "llm";
|
|
30
|
-
SpanType3["TOOL"] = "tool";
|
|
31
|
-
SpanType3["AGENT"] = "agent";
|
|
32
|
-
SpanType3["CHAIN"] = "chain";
|
|
33
|
-
SpanType3["RETRIEVAL"] = "retrieval";
|
|
34
|
-
return SpanType3;
|
|
35
|
-
})(SpanType || {});
|
|
36
|
-
TraceStatus = /* @__PURE__ */ ((TraceStatus2) => {
|
|
37
|
-
TraceStatus2["SUCCESS"] = "success";
|
|
38
|
-
TraceStatus2["ERROR"] = "error";
|
|
39
|
-
TraceStatus2["TIMEOUT"] = "timeout";
|
|
40
|
-
TraceStatus2["RUNNING"] = "running";
|
|
41
|
-
return TraceStatus2;
|
|
42
|
-
})(TraceStatus || {});
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// src/instrumentation/openai.ts
|
|
47
|
-
function wrapOpenAI(client, nwClient) {
|
|
48
|
-
const anyClient = client;
|
|
49
|
-
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
50
|
-
client.chat.completions.create = async function(params, options) {
|
|
51
|
-
const trace = nwClient.startTrace(`openai.chat.completions.create`, {
|
|
52
|
-
provider: "openai",
|
|
53
|
-
model: params.model
|
|
54
|
-
});
|
|
55
|
-
const span = trace.startSpan("chat.completions.create", "llm" /* LLM */);
|
|
56
|
-
span.setMetadata("model", params.model);
|
|
57
|
-
span.setMetadata("provider", "openai");
|
|
58
|
-
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
59
|
-
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
60
|
-
span.setInput({
|
|
61
|
-
model: params.model,
|
|
62
|
-
messages: params.messages,
|
|
63
|
-
temperature: params.temperature,
|
|
64
|
-
max_tokens: params.max_tokens
|
|
65
|
-
});
|
|
66
|
-
const startTime = Date.now();
|
|
67
|
-
try {
|
|
68
|
-
const response = await originalCreate(params, options);
|
|
69
|
-
if (params.stream) {
|
|
70
|
-
return wrapOpenAIStream(response, span, trace, startTime);
|
|
71
|
-
}
|
|
72
|
-
const completionResponse = response;
|
|
73
|
-
const latency = Date.now() - startTime;
|
|
74
|
-
span.setOutput({
|
|
75
|
-
id: completionResponse.id,
|
|
76
|
-
model: completionResponse.model,
|
|
77
|
-
choices: completionResponse.choices
|
|
78
|
-
});
|
|
79
|
-
if (completionResponse.usage) {
|
|
80
|
-
span.setTokens({
|
|
81
|
-
input: completionResponse.usage.prompt_tokens,
|
|
82
|
-
output: completionResponse.usage.completion_tokens,
|
|
83
|
-
total: completionResponse.usage.total_tokens
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
span.setMetadata("latency_ms", latency);
|
|
87
|
-
span.end();
|
|
88
|
-
trace.end();
|
|
89
|
-
return response;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
const latency = Date.now() - startTime;
|
|
92
|
-
span.setMetadata("latency_ms", latency);
|
|
93
|
-
span.setError(error);
|
|
94
|
-
span.end();
|
|
95
|
-
trace.end();
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
return client;
|
|
100
|
-
}
|
|
101
|
-
function wrapOpenAIStream(stream, span, trace, startTime) {
|
|
102
|
-
const bufferedChunks = [];
|
|
103
|
-
let streamId = "";
|
|
104
|
-
let streamModel = "";
|
|
105
|
-
return {
|
|
106
|
-
[Symbol.asyncIterator]: async function* () {
|
|
107
|
-
try {
|
|
108
|
-
for await (const chunk of stream) {
|
|
109
|
-
if (chunk.id) streamId = chunk.id;
|
|
110
|
-
if (chunk.model) streamModel = chunk.model;
|
|
111
|
-
const delta = chunk.choices[0]?.delta;
|
|
112
|
-
if (delta?.content) {
|
|
113
|
-
bufferedChunks.push(delta.content);
|
|
114
|
-
}
|
|
115
|
-
yield chunk;
|
|
116
|
-
}
|
|
117
|
-
const latency = Date.now() - startTime;
|
|
118
|
-
const completeOutput = bufferedChunks.join("");
|
|
119
|
-
span.setOutput({
|
|
120
|
-
id: streamId,
|
|
121
|
-
model: streamModel,
|
|
122
|
-
content: completeOutput
|
|
123
|
-
});
|
|
124
|
-
span.setMetadata("latency_ms", latency);
|
|
125
|
-
span.setMetadata("streaming", true);
|
|
126
|
-
span.end();
|
|
127
|
-
trace.end();
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const latency = Date.now() - startTime;
|
|
130
|
-
span.setMetadata("latency_ms", latency);
|
|
131
|
-
span.setError(error);
|
|
132
|
-
span.end();
|
|
133
|
-
trace.end();
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
var init_openai = __esm({
|
|
140
|
-
"src/instrumentation/openai.ts"() {
|
|
141
|
-
"use strict";
|
|
142
|
-
init_types();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// src/instrumentation/anthropic.ts
|
|
147
|
-
function wrapAnthropic(client, nwClient) {
|
|
148
|
-
const anyClient = client;
|
|
149
|
-
const originalCreate = client.messages.create.bind(client.messages);
|
|
150
|
-
client.messages.create = async function(params, options) {
|
|
151
|
-
const trace = nwClient.startTrace(`anthropic.messages.create`, {
|
|
152
|
-
provider: "anthropic",
|
|
153
|
-
model: params.model
|
|
154
|
-
});
|
|
155
|
-
const span = trace.startSpan("messages.create", "llm" /* LLM */);
|
|
156
|
-
span.setMetadata("model", params.model);
|
|
157
|
-
span.setMetadata("provider", "anthropic");
|
|
158
|
-
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
159
|
-
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
160
|
-
if (params.system !== void 0) span.setMetadata("system", params.system);
|
|
161
|
-
span.setInput({
|
|
162
|
-
model: params.model,
|
|
163
|
-
messages: params.messages,
|
|
164
|
-
max_tokens: params.max_tokens,
|
|
165
|
-
temperature: params.temperature,
|
|
166
|
-
system: params.system
|
|
167
|
-
});
|
|
168
|
-
const startTime = Date.now();
|
|
169
|
-
try {
|
|
170
|
-
const response = await originalCreate(params, options);
|
|
171
|
-
if (params.stream) {
|
|
172
|
-
return wrapAnthropicStream(response, span, trace, startTime);
|
|
173
|
-
}
|
|
174
|
-
const messageResponse = response;
|
|
175
|
-
const latency = Date.now() - startTime;
|
|
176
|
-
span.setOutput({
|
|
177
|
-
id: messageResponse.id,
|
|
178
|
-
model: messageResponse.model,
|
|
179
|
-
role: messageResponse.role,
|
|
180
|
-
content: messageResponse.content,
|
|
181
|
-
stop_reason: messageResponse.stop_reason
|
|
182
|
-
});
|
|
183
|
-
if (messageResponse.usage) {
|
|
184
|
-
span.setTokens({
|
|
185
|
-
input: messageResponse.usage.input_tokens,
|
|
186
|
-
output: messageResponse.usage.output_tokens,
|
|
187
|
-
total: messageResponse.usage.input_tokens + messageResponse.usage.output_tokens
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
span.setMetadata("latency_ms", latency);
|
|
191
|
-
span.end();
|
|
192
|
-
trace.end();
|
|
193
|
-
return response;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const latency = Date.now() - startTime;
|
|
196
|
-
span.setMetadata("latency_ms", latency);
|
|
197
|
-
span.setError(error);
|
|
198
|
-
span.end();
|
|
199
|
-
trace.end();
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
return client;
|
|
204
|
-
}
|
|
205
|
-
function wrapAnthropicStream(stream, span, trace, startTime) {
|
|
206
|
-
const bufferedChunks = [];
|
|
207
|
-
let messageId = "";
|
|
208
|
-
let messageModel = "";
|
|
209
|
-
let stopReason = null;
|
|
210
|
-
let inputTokens = 0;
|
|
211
|
-
let outputTokens = 0;
|
|
212
|
-
return {
|
|
213
|
-
[Symbol.asyncIterator]: async function* () {
|
|
214
|
-
try {
|
|
215
|
-
for await (const event of stream) {
|
|
216
|
-
if (event.type === "message_start" && event.message) {
|
|
217
|
-
messageId = event.message.id;
|
|
218
|
-
messageModel = event.message.model;
|
|
219
|
-
if (event.message.usage) {
|
|
220
|
-
inputTokens = event.message.usage.input_tokens;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
224
|
-
bufferedChunks.push(event.delta.text);
|
|
225
|
-
}
|
|
226
|
-
if (event.type === "message_delta" && event.delta) {
|
|
227
|
-
if (event.delta.stop_reason) {
|
|
228
|
-
stopReason = event.delta.stop_reason;
|
|
229
|
-
}
|
|
230
|
-
if (event.usage?.output_tokens) {
|
|
231
|
-
outputTokens = event.usage.output_tokens;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
yield event;
|
|
235
|
-
}
|
|
236
|
-
const latency = Date.now() - startTime;
|
|
237
|
-
const completeOutput = bufferedChunks.join("");
|
|
238
|
-
span.setOutput({
|
|
239
|
-
id: messageId,
|
|
240
|
-
model: messageModel,
|
|
241
|
-
content: completeOutput,
|
|
242
|
-
stop_reason: stopReason
|
|
243
|
-
});
|
|
244
|
-
if (inputTokens > 0 || outputTokens > 0) {
|
|
245
|
-
span.setTokens({
|
|
246
|
-
input: inputTokens,
|
|
247
|
-
output: outputTokens,
|
|
248
|
-
total: inputTokens + outputTokens
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
span.setMetadata("latency_ms", latency);
|
|
252
|
-
span.setMetadata("streaming", true);
|
|
253
|
-
span.end();
|
|
254
|
-
trace.end();
|
|
255
|
-
} catch (error) {
|
|
256
|
-
const latency = Date.now() - startTime;
|
|
257
|
-
span.setMetadata("latency_ms", latency);
|
|
258
|
-
span.setError(error);
|
|
259
|
-
span.end();
|
|
260
|
-
trace.end();
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
var init_anthropic = __esm({
|
|
267
|
-
"src/instrumentation/anthropic.ts"() {
|
|
268
|
-
"use strict";
|
|
269
|
-
init_types();
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// src/instrumentation/index.ts
|
|
274
|
-
var instrumentation_exports = {};
|
|
275
|
-
__export(instrumentation_exports, {
|
|
276
|
-
wrap: () => wrap,
|
|
277
|
-
wrapAnthropic: () => wrapAnthropic,
|
|
278
|
-
wrapOpenAI: () => wrapOpenAI
|
|
279
|
-
});
|
|
280
|
-
function wrap(client, nwClient) {
|
|
281
|
-
if ("chat" in client && client.chat && "completions" in client.chat) {
|
|
282
|
-
return wrapOpenAI(client, nwClient);
|
|
283
|
-
}
|
|
284
|
-
if ("messages" in client && client.messages && "create" in client.messages) {
|
|
285
|
-
return wrapAnthropic(client, nwClient);
|
|
286
|
-
}
|
|
287
|
-
throw new Error(
|
|
288
|
-
"Observyze SDK: Unsupported client type. Supported clients: OpenAI, Anthropic"
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
var init_instrumentation = __esm({
|
|
292
|
-
"src/instrumentation/index.ts"() {
|
|
293
|
-
"use strict";
|
|
294
|
-
init_openai();
|
|
295
|
-
init_anthropic();
|
|
296
|
-
init_openai();
|
|
297
|
-
init_anthropic();
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
30
|
// src/index.ts
|
|
302
31
|
var index_exports = {};
|
|
303
32
|
__export(index_exports, {
|
|
304
33
|
ObservyzeClient: () => ObservyzeClient,
|
|
305
34
|
ObservyzeSpanExporter: () => ObservyzeSpanExporter,
|
|
306
35
|
Span: () => Span,
|
|
307
|
-
SpanType: () => SpanType,
|
|
36
|
+
SpanType: () => import_types.SpanType,
|
|
308
37
|
Trace: () => Trace,
|
|
309
|
-
TraceStatus: () => TraceStatus,
|
|
38
|
+
TraceStatus: () => import_types.TraceStatus,
|
|
310
39
|
wrap: () => wrap,
|
|
311
40
|
wrapAnthropic: () => wrapAnthropic,
|
|
312
41
|
wrapOpenAI: () => wrapOpenAI
|
|
313
42
|
});
|
|
314
43
|
module.exports = __toCommonJS(index_exports);
|
|
315
44
|
|
|
45
|
+
// src/client.ts
|
|
46
|
+
var import_debug3 = __toESM(require("debug"));
|
|
47
|
+
|
|
48
|
+
// src/types.ts
|
|
49
|
+
var import_types = require("@observyze/types");
|
|
50
|
+
|
|
316
51
|
// src/trace.ts
|
|
317
|
-
init_types();
|
|
318
52
|
var import_crypto = require("crypto");
|
|
319
53
|
function generateId() {
|
|
320
54
|
return `${Date.now()}-${(0, import_crypto.randomUUID)().substring(0, 8)}`;
|
|
@@ -417,7 +151,7 @@ var Trace = class {
|
|
|
417
151
|
organization_id: organizationId,
|
|
418
152
|
project_id: projectId,
|
|
419
153
|
name,
|
|
420
|
-
status:
|
|
154
|
+
status: import_types.TraceStatus.RUNNING,
|
|
421
155
|
start_time: new Date(this.startTime),
|
|
422
156
|
end_time: new Date(this.startTime),
|
|
423
157
|
// Will be updated on end()
|
|
@@ -485,47 +219,326 @@ var Trace = class {
|
|
|
485
219
|
/**
|
|
486
220
|
* End the trace with a final status
|
|
487
221
|
*/
|
|
488
|
-
end(status =
|
|
222
|
+
end(status = import_types.TraceStatus.SUCCESS) {
|
|
489
223
|
if (this.ended) {
|
|
490
224
|
return;
|
|
491
225
|
}
|
|
492
|
-
const endTime = Date.now();
|
|
493
|
-
this.data.end_time = new Date(endTime);
|
|
494
|
-
this.data.duration_ms = endTime - this.startTime;
|
|
495
|
-
this.data.status = status;
|
|
496
|
-
this.data.spans = this.spans.map((span) => span.toJSON());
|
|
497
|
-
this.ended = true;
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Get the trace ID
|
|
501
|
-
*/
|
|
502
|
-
get id() {
|
|
503
|
-
return this.data.trace_id;
|
|
226
|
+
const endTime = Date.now();
|
|
227
|
+
this.data.end_time = new Date(endTime);
|
|
228
|
+
this.data.duration_ms = endTime - this.startTime;
|
|
229
|
+
this.data.status = status;
|
|
230
|
+
this.data.spans = this.spans.map((span) => span.toJSON());
|
|
231
|
+
this.ended = true;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the trace ID
|
|
235
|
+
*/
|
|
236
|
+
get id() {
|
|
237
|
+
return this.data.trace_id;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Check if the trace has ended
|
|
241
|
+
*/
|
|
242
|
+
get isEnded() {
|
|
243
|
+
return this.ended;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get the trace data for serialization
|
|
247
|
+
*/
|
|
248
|
+
toJSON() {
|
|
249
|
+
return { ...this.data };
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/instrumentation/openai.ts
|
|
254
|
+
var import_debug = __toESM(require("debug"));
|
|
255
|
+
var log = (0, import_debug.default)("observyze:sdk");
|
|
256
|
+
function wrapOpenAI(client, nwClient) {
|
|
257
|
+
const anyClient = client;
|
|
258
|
+
if (nwClient.getConfig().enableProxyRedirect && anyClient.baseURL && anyClient.apiKey) {
|
|
259
|
+
const isAlreadyRedirected = anyClient.baseURL.includes("/api/v1/proxy/openai");
|
|
260
|
+
if (!isAlreadyRedirected) {
|
|
261
|
+
const originalApiKey = anyClient.apiKey;
|
|
262
|
+
anyClient.baseURL = `${nwClient.getConfig().endpoint}/api/v1/proxy/openai/v1`;
|
|
263
|
+
anyClient.apiKey = nwClient.getConfig().apiKey;
|
|
264
|
+
anyClient.defaultHeaders = {
|
|
265
|
+
...anyClient.defaultHeaders,
|
|
266
|
+
"x-provider-key": originalApiKey
|
|
267
|
+
};
|
|
268
|
+
if (nwClient.getConfig().debug) {
|
|
269
|
+
log("[Observyze SDK] Transparently redirected OpenAI client to proxy gateway:", anyClient.baseURL);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
274
|
+
client.chat.completions.create = async function(params, options) {
|
|
275
|
+
const isProxyRedirected = nwClient.getConfig().enableProxyRedirect && anyClient.baseURL?.includes("/api/v1/proxy/openai");
|
|
276
|
+
if (isProxyRedirected) {
|
|
277
|
+
return originalCreate(params, options);
|
|
278
|
+
}
|
|
279
|
+
const trace = nwClient.startTrace(`openai.chat.completions.create`, {
|
|
280
|
+
provider: "openai",
|
|
281
|
+
model: params.model
|
|
282
|
+
});
|
|
283
|
+
const span = trace.startSpan("chat.completions.create", import_types.SpanType.LLM);
|
|
284
|
+
span.setMetadata("model", params.model);
|
|
285
|
+
span.setMetadata("provider", "openai");
|
|
286
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
287
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
288
|
+
span.setInput({
|
|
289
|
+
model: params.model,
|
|
290
|
+
messages: params.messages,
|
|
291
|
+
temperature: params.temperature,
|
|
292
|
+
max_tokens: params.max_tokens
|
|
293
|
+
});
|
|
294
|
+
const startTime = Date.now();
|
|
295
|
+
try {
|
|
296
|
+
const response = await originalCreate(params, options);
|
|
297
|
+
if (params.stream) {
|
|
298
|
+
return wrapOpenAIStream(response, span, trace, startTime);
|
|
299
|
+
}
|
|
300
|
+
const completionResponse = response;
|
|
301
|
+
const latency = Date.now() - startTime;
|
|
302
|
+
span.setOutput({
|
|
303
|
+
id: completionResponse.id,
|
|
304
|
+
model: completionResponse.model,
|
|
305
|
+
choices: completionResponse.choices
|
|
306
|
+
});
|
|
307
|
+
if (completionResponse.usage) {
|
|
308
|
+
span.setTokens({
|
|
309
|
+
input: completionResponse.usage.prompt_tokens,
|
|
310
|
+
output: completionResponse.usage.completion_tokens,
|
|
311
|
+
total: completionResponse.usage.total_tokens
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
span.setMetadata("latency_ms", latency);
|
|
315
|
+
span.end();
|
|
316
|
+
trace.end();
|
|
317
|
+
return response;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const latency = Date.now() - startTime;
|
|
320
|
+
span.setMetadata("latency_ms", latency);
|
|
321
|
+
span.setError(error);
|
|
322
|
+
span.end();
|
|
323
|
+
trace.end();
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
return client;
|
|
328
|
+
}
|
|
329
|
+
function wrapOpenAIStream(stream, span, trace, startTime) {
|
|
330
|
+
const bufferedChunks = [];
|
|
331
|
+
let streamId = "";
|
|
332
|
+
let streamModel = "";
|
|
333
|
+
return {
|
|
334
|
+
[Symbol.asyncIterator]: async function* () {
|
|
335
|
+
try {
|
|
336
|
+
for await (const chunk of stream) {
|
|
337
|
+
if (chunk.id) streamId = chunk.id;
|
|
338
|
+
if (chunk.model) streamModel = chunk.model;
|
|
339
|
+
const delta = chunk.choices[0]?.delta;
|
|
340
|
+
if (delta?.content) {
|
|
341
|
+
bufferedChunks.push(delta.content);
|
|
342
|
+
}
|
|
343
|
+
yield chunk;
|
|
344
|
+
}
|
|
345
|
+
const latency = Date.now() - startTime;
|
|
346
|
+
const completeOutput = bufferedChunks.join("");
|
|
347
|
+
span.setOutput({
|
|
348
|
+
id: streamId,
|
|
349
|
+
model: streamModel,
|
|
350
|
+
content: completeOutput
|
|
351
|
+
});
|
|
352
|
+
span.setMetadata("latency_ms", latency);
|
|
353
|
+
span.setMetadata("streaming", true);
|
|
354
|
+
span.end();
|
|
355
|
+
trace.end();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const latency = Date.now() - startTime;
|
|
358
|
+
span.setMetadata("latency_ms", latency);
|
|
359
|
+
span.setError(error);
|
|
360
|
+
span.end();
|
|
361
|
+
trace.end();
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/instrumentation/anthropic.ts
|
|
369
|
+
var import_debug2 = __toESM(require("debug"));
|
|
370
|
+
var log2 = (0, import_debug2.default)("observyze:sdk");
|
|
371
|
+
function wrapAnthropic(client, nwClient) {
|
|
372
|
+
const anyClient = client;
|
|
373
|
+
if (nwClient.getConfig().enableProxyRedirect && anyClient.baseURL && anyClient.apiKey) {
|
|
374
|
+
const isAlreadyRedirected = anyClient.baseURL.includes("/api/v1/proxy/anthropic");
|
|
375
|
+
if (!isAlreadyRedirected) {
|
|
376
|
+
const originalApiKey = anyClient.apiKey;
|
|
377
|
+
anyClient.baseURL = `${nwClient.getConfig().endpoint}/api/v1/proxy/anthropic/v1`;
|
|
378
|
+
anyClient.apiKey = nwClient.getConfig().apiKey;
|
|
379
|
+
anyClient.defaultHeaders = {
|
|
380
|
+
...anyClient.defaultHeaders,
|
|
381
|
+
"x-provider-key": originalApiKey
|
|
382
|
+
};
|
|
383
|
+
if (nwClient.getConfig().debug) {
|
|
384
|
+
log2("[Observyze SDK] Transparently redirected Anthropic client to proxy gateway:", anyClient.baseURL);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
504
387
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
388
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
389
|
+
client.messages.create = async function(params, options) {
|
|
390
|
+
const isProxyRedirected = nwClient.getConfig().enableProxyRedirect && anyClient.baseURL?.includes("/api/v1/proxy/anthropic");
|
|
391
|
+
if (isProxyRedirected) {
|
|
392
|
+
return originalCreate(params, options);
|
|
393
|
+
}
|
|
394
|
+
const trace = nwClient.startTrace(`anthropic.messages.create`, {
|
|
395
|
+
provider: "anthropic",
|
|
396
|
+
model: params.model
|
|
397
|
+
});
|
|
398
|
+
const span = trace.startSpan("messages.create", import_types.SpanType.LLM);
|
|
399
|
+
span.setMetadata("model", params.model);
|
|
400
|
+
span.setMetadata("provider", "anthropic");
|
|
401
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
402
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
403
|
+
if (params.system !== void 0) span.setMetadata("system", params.system);
|
|
404
|
+
span.setInput({
|
|
405
|
+
model: params.model,
|
|
406
|
+
messages: params.messages,
|
|
407
|
+
max_tokens: params.max_tokens,
|
|
408
|
+
temperature: params.temperature,
|
|
409
|
+
system: params.system
|
|
410
|
+
});
|
|
411
|
+
const startTime = Date.now();
|
|
412
|
+
try {
|
|
413
|
+
const response = await originalCreate(params, options);
|
|
414
|
+
if (params.stream) {
|
|
415
|
+
return wrapAnthropicStream(response, span, trace, startTime);
|
|
416
|
+
}
|
|
417
|
+
const messageResponse = response;
|
|
418
|
+
const latency = Date.now() - startTime;
|
|
419
|
+
span.setOutput({
|
|
420
|
+
id: messageResponse.id,
|
|
421
|
+
model: messageResponse.model,
|
|
422
|
+
role: messageResponse.role,
|
|
423
|
+
content: messageResponse.content,
|
|
424
|
+
stop_reason: messageResponse.stop_reason
|
|
425
|
+
});
|
|
426
|
+
if (messageResponse.usage) {
|
|
427
|
+
span.setTokens({
|
|
428
|
+
input: messageResponse.usage.input_tokens,
|
|
429
|
+
output: messageResponse.usage.output_tokens,
|
|
430
|
+
total: messageResponse.usage.input_tokens + messageResponse.usage.output_tokens
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
span.setMetadata("latency_ms", latency);
|
|
434
|
+
span.end();
|
|
435
|
+
trace.end();
|
|
436
|
+
return response;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
const latency = Date.now() - startTime;
|
|
439
|
+
span.setMetadata("latency_ms", latency);
|
|
440
|
+
span.setError(error);
|
|
441
|
+
span.end();
|
|
442
|
+
trace.end();
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
return client;
|
|
447
|
+
}
|
|
448
|
+
function wrapAnthropicStream(stream, span, trace, startTime) {
|
|
449
|
+
const bufferedChunks = [];
|
|
450
|
+
let messageId = "";
|
|
451
|
+
let messageModel = "";
|
|
452
|
+
let stopReason = null;
|
|
453
|
+
let inputTokens = 0;
|
|
454
|
+
let outputTokens = 0;
|
|
455
|
+
return {
|
|
456
|
+
[Symbol.asyncIterator]: async function* () {
|
|
457
|
+
try {
|
|
458
|
+
for await (const event of stream) {
|
|
459
|
+
if (event.type === "message_start" && event.message) {
|
|
460
|
+
messageId = event.message.id;
|
|
461
|
+
messageModel = event.message.model;
|
|
462
|
+
if (event.message.usage) {
|
|
463
|
+
inputTokens = event.message.usage.input_tokens;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
467
|
+
bufferedChunks.push(event.delta.text);
|
|
468
|
+
}
|
|
469
|
+
if (event.type === "message_delta" && event.delta) {
|
|
470
|
+
if (event.delta.stop_reason) {
|
|
471
|
+
stopReason = event.delta.stop_reason;
|
|
472
|
+
}
|
|
473
|
+
if (event.usage?.output_tokens) {
|
|
474
|
+
outputTokens = event.usage.output_tokens;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
yield event;
|
|
478
|
+
}
|
|
479
|
+
const latency = Date.now() - startTime;
|
|
480
|
+
const completeOutput = bufferedChunks.join("");
|
|
481
|
+
span.setOutput({
|
|
482
|
+
id: messageId,
|
|
483
|
+
model: messageModel,
|
|
484
|
+
content: completeOutput,
|
|
485
|
+
stop_reason: stopReason
|
|
486
|
+
});
|
|
487
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
488
|
+
span.setTokens({
|
|
489
|
+
input: inputTokens,
|
|
490
|
+
output: outputTokens,
|
|
491
|
+
total: inputTokens + outputTokens
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
span.setMetadata("latency_ms", latency);
|
|
495
|
+
span.setMetadata("streaming", true);
|
|
496
|
+
span.end();
|
|
497
|
+
trace.end();
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const latency = Date.now() - startTime;
|
|
500
|
+
span.setMetadata("latency_ms", latency);
|
|
501
|
+
span.setError(error);
|
|
502
|
+
span.end();
|
|
503
|
+
trace.end();
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/instrumentation/index.ts
|
|
511
|
+
function wrap(client, nwClient) {
|
|
512
|
+
if ("chat" in client && client.chat && "completions" in client.chat) {
|
|
513
|
+
return wrapOpenAI(client, nwClient);
|
|
510
514
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
*/
|
|
514
|
-
toJSON() {
|
|
515
|
-
return { ...this.data };
|
|
515
|
+
if ("messages" in client && client.messages && "create" in client.messages) {
|
|
516
|
+
return wrapAnthropic(client, nwClient);
|
|
516
517
|
}
|
|
517
|
-
|
|
518
|
+
throw new Error(
|
|
519
|
+
"Observyze SDK: Unsupported client type. Supported clients: OpenAI, Anthropic"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
518
522
|
|
|
519
523
|
// src/client.ts
|
|
520
|
-
|
|
524
|
+
var import_fs = __toESM(require("fs"));
|
|
525
|
+
var import_path = __toESM(require("path"));
|
|
526
|
+
var log3 = (0, import_debug3.default)("observyze:sdk");
|
|
521
527
|
var DEFAULT_CONFIG = {
|
|
522
|
-
endpoint: "
|
|
528
|
+
endpoint: "http://localhost:3001",
|
|
523
529
|
batchSize: 100,
|
|
524
530
|
flushInterval: 5e3,
|
|
525
531
|
enableAutoInstrumentation: true,
|
|
526
532
|
debug: false,
|
|
527
533
|
dryRun: false,
|
|
528
|
-
enablePiiRedaction: true
|
|
534
|
+
enablePiiRedaction: true,
|
|
535
|
+
hallucinationThreshold: 0.8,
|
|
536
|
+
safetyThreshold: 0.9,
|
|
537
|
+
confidenceThreshold: 0.4,
|
|
538
|
+
evalEndpoint: process.env.EVAL_ENDPOINT || (process.env.NODE_ENV === "production" ? "https://api.observyze.com" : "http://localhost:3001"),
|
|
539
|
+
enableCircuitBreaker: true,
|
|
540
|
+
failClosed: true,
|
|
541
|
+
enableProxyRedirect: true
|
|
529
542
|
};
|
|
530
543
|
var ObservyzeClient = class _ObservyzeClient {
|
|
531
544
|
config;
|
|
@@ -534,7 +547,37 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
534
547
|
isShuttingDown = false;
|
|
535
548
|
MAX_QUEUE_SIZE = 1e3;
|
|
536
549
|
RETRY_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
537
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Parse a JSON API error response and extract trace_id, error code, and message.
|
|
552
|
+
* The api-gateway error handler includes these fields in every error response.
|
|
553
|
+
*/
|
|
554
|
+
static parseApiError(_response, body) {
|
|
555
|
+
try {
|
|
556
|
+
const parsed = JSON.parse(body);
|
|
557
|
+
const error = parsed.error || parsed;
|
|
558
|
+
return {
|
|
559
|
+
traceId: error.trace_id || "unknown",
|
|
560
|
+
code: error.code || "UNKNOWN_ERROR",
|
|
561
|
+
message: error.message || body.slice(0, 200)
|
|
562
|
+
};
|
|
563
|
+
} catch {
|
|
564
|
+
return {
|
|
565
|
+
traceId: "unknown",
|
|
566
|
+
code: "UNKNOWN_ERROR",
|
|
567
|
+
message: body.slice(0, 200)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Format an API error into a user-friendly message with trace_id for correlation.
|
|
573
|
+
* Example output:
|
|
574
|
+
* "Observyze API error (401 [ref: err_a1b2c3d4]): MISSING_PROVIDER_KEY — No API key configured..."
|
|
575
|
+
*/
|
|
576
|
+
static formatApiError(response, body) {
|
|
577
|
+
const { traceId, code, message } = _ObservyzeClient.parseApiError(response, body);
|
|
578
|
+
const prefix = traceId !== "unknown" ? ` [ref: ${traceId}]` : "";
|
|
579
|
+
return `Observyze API error (${response.status}${prefix}): ${code} \u2014 ${message}`;
|
|
580
|
+
}
|
|
538
581
|
constructor(config) {
|
|
539
582
|
if (!config.apiKey) {
|
|
540
583
|
throw new Error("Observyze SDK: apiKey is required");
|
|
@@ -547,7 +590,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
547
590
|
};
|
|
548
591
|
this.startFlushTimer();
|
|
549
592
|
if (this.config.debug) {
|
|
550
|
-
|
|
593
|
+
log3("[Observyze SDK] Initialized with config:", {
|
|
551
594
|
endpoint: this.config.endpoint,
|
|
552
595
|
batchSize: this.config.batchSize,
|
|
553
596
|
flushInterval: this.config.flushInterval,
|
|
@@ -568,7 +611,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
568
611
|
trace.setMetadataAll(metadata);
|
|
569
612
|
}
|
|
570
613
|
const originalEnd = trace.end.bind(trace);
|
|
571
|
-
trace.end = (status =
|
|
614
|
+
trace.end = (status = import_types.TraceStatus.SUCCESS) => {
|
|
572
615
|
originalEnd(status);
|
|
573
616
|
this.bufferTrace(trace);
|
|
574
617
|
};
|
|
@@ -580,23 +623,23 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
580
623
|
bufferTrace(trace) {
|
|
581
624
|
if (!trace.isEnded) {
|
|
582
625
|
if (this.config.debug) {
|
|
583
|
-
|
|
626
|
+
log3.extend("warn")("[Observyze SDK] Attempted to buffer a trace that has not ended");
|
|
584
627
|
}
|
|
585
628
|
return;
|
|
586
629
|
}
|
|
587
630
|
if (this.traceBuffer.length >= this.MAX_QUEUE_SIZE) {
|
|
588
631
|
if (this.config.debug) {
|
|
589
|
-
|
|
632
|
+
log3.extend("warn")(`[Observyze SDK] Queue at max capacity (${this.MAX_QUEUE_SIZE}), dropping oldest trace`);
|
|
590
633
|
}
|
|
591
634
|
this.traceBuffer.shift();
|
|
592
635
|
}
|
|
593
636
|
this.traceBuffer.push(trace);
|
|
594
637
|
if (this.config.debug) {
|
|
595
|
-
|
|
638
|
+
log3(`[Observyze SDK] Buffered trace ${trace.id} (${this.traceBuffer.length}/${this.config.batchSize})`);
|
|
596
639
|
}
|
|
597
640
|
if (this.traceBuffer.length >= this.config.batchSize) {
|
|
598
641
|
this.flush().catch((err) => {
|
|
599
|
-
|
|
642
|
+
log3.extend("error")("[Observyze SDK] Error flushing buffer:", err);
|
|
600
643
|
});
|
|
601
644
|
}
|
|
602
645
|
}
|
|
@@ -610,7 +653,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
610
653
|
this.flushTimer = setInterval(() => {
|
|
611
654
|
if (this.traceBuffer.length > 0) {
|
|
612
655
|
this.flush().catch((err) => {
|
|
613
|
-
|
|
656
|
+
log3.extend("error")("[Observyze SDK] Error in auto-flush:", err);
|
|
614
657
|
});
|
|
615
658
|
}
|
|
616
659
|
}, this.config.flushInterval);
|
|
@@ -627,11 +670,11 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
627
670
|
}
|
|
628
671
|
const tracesToSend = this.traceBuffer.splice(0, this.config.batchSize);
|
|
629
672
|
if (this.config.debug) {
|
|
630
|
-
|
|
673
|
+
log3(`[Observyze SDK] Flushing ${tracesToSend.length} traces`);
|
|
631
674
|
}
|
|
632
675
|
if (this.config.dryRun) {
|
|
633
676
|
if (this.config.debug) {
|
|
634
|
-
|
|
677
|
+
log3("[Observyze SDK] Dry-run mode: traces not sent");
|
|
635
678
|
}
|
|
636
679
|
return;
|
|
637
680
|
}
|
|
@@ -642,15 +685,15 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
642
685
|
if (remainingSpace > 0) {
|
|
643
686
|
this.traceBuffer.unshift(...tracesToSend.slice(0, remainingSpace));
|
|
644
687
|
if (this.config.debug) {
|
|
645
|
-
|
|
688
|
+
log3(`[Observyze SDK] Re-queued ${Math.min(tracesToSend.length, remainingSpace)} traces after failure`);
|
|
646
689
|
}
|
|
647
690
|
} else {
|
|
648
691
|
if (this.config.debug) {
|
|
649
|
-
|
|
692
|
+
log3.extend("warn")(`[Observyze SDK] Queue full, dropped ${tracesToSend.length} traces`);
|
|
650
693
|
}
|
|
651
694
|
}
|
|
652
695
|
if (this.config.debug) {
|
|
653
|
-
|
|
696
|
+
log3.extend("error")("[Observyze SDK] Failed to send traces after retries:", error);
|
|
654
697
|
}
|
|
655
698
|
throw error;
|
|
656
699
|
}
|
|
@@ -680,10 +723,11 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
680
723
|
});
|
|
681
724
|
if (!response.ok) {
|
|
682
725
|
const errorBody = await response.text();
|
|
683
|
-
|
|
726
|
+
const formatted = _ObservyzeClient.formatApiError(response, errorBody);
|
|
727
|
+
throw new Error(`[Observyze SDK] Trace ingestion failed. ${formatted}`);
|
|
684
728
|
}
|
|
685
729
|
if (this.config.debug) {
|
|
686
|
-
|
|
730
|
+
log3(`[Observyze SDK] Successfully sent ${traces.length} traces${attempt > 0 ? ` (after ${attempt} retries)` : ""}`);
|
|
687
731
|
}
|
|
688
732
|
return;
|
|
689
733
|
} catch (error) {
|
|
@@ -693,7 +737,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
693
737
|
}
|
|
694
738
|
const delay = this.RETRY_DELAYS[attempt];
|
|
695
739
|
if (this.config.debug) {
|
|
696
|
-
|
|
740
|
+
log3.extend("warn")(`[Observyze SDK] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error);
|
|
697
741
|
}
|
|
698
742
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
699
743
|
}
|
|
@@ -709,7 +753,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
709
753
|
}
|
|
710
754
|
this.isShuttingDown = true;
|
|
711
755
|
if (this.config.debug) {
|
|
712
|
-
|
|
756
|
+
log3("[Observyze SDK] Shutting down...");
|
|
713
757
|
}
|
|
714
758
|
if (this.flushTimer) {
|
|
715
759
|
clearInterval(this.flushTimer);
|
|
@@ -718,10 +762,10 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
718
762
|
try {
|
|
719
763
|
await this.flush();
|
|
720
764
|
} catch (error) {
|
|
721
|
-
|
|
765
|
+
log3.extend("error")("[Observyze SDK] Error during shutdown flush:", error);
|
|
722
766
|
}
|
|
723
767
|
if (this.config.debug) {
|
|
724
|
-
|
|
768
|
+
log3("[Observyze SDK] Shutdown complete");
|
|
725
769
|
}
|
|
726
770
|
}
|
|
727
771
|
/**
|
|
@@ -758,8 +802,70 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
758
802
|
* ```
|
|
759
803
|
*/
|
|
760
804
|
wrap(client) {
|
|
761
|
-
|
|
762
|
-
|
|
805
|
+
return wrap(client, this);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Verify that the SDK can reach Observyze and send traces end-to-end.
|
|
809
|
+
*
|
|
810
|
+
* This is the definitive "is my integration working?" test for SDK users.
|
|
811
|
+
* It sends a real test trace through the EXACT same pipeline used by
|
|
812
|
+
* `flush()` / the wrapped LLM clients (same endpoint, apiKey, retry logic),
|
|
813
|
+
* so a successful call proves the whole chain works from your code:
|
|
814
|
+
* - apiKey is valid and authorized
|
|
815
|
+
* - endpoint is reachable from your environment
|
|
816
|
+
* - organization / project resolution works
|
|
817
|
+
* - the ingest pipeline accepts and stores traces
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* ```typescript
|
|
821
|
+
* const nw = new ObservyzeClient({ apiKey: process.env.OBSERVYZE_API_KEY })
|
|
822
|
+
* const result = await nw.testConnection()
|
|
823
|
+
* // { ok: true, traceId: 'nw_xxx', message: 'Connection successful...' }
|
|
824
|
+
* ```
|
|
825
|
+
*
|
|
826
|
+
* The returned traceId can be searched in the Observyze dashboard (Traces →
|
|
827
|
+
* search the trace name "Observyze Connection Test") to confirm it landed.
|
|
828
|
+
*/
|
|
829
|
+
async testConnection() {
|
|
830
|
+
if (this.config.dryRun) {
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
message: "Dry-run mode is enabled, so no trace was actually sent. Set dryRun: false to run a real connection test."
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const trace = new Trace(
|
|
837
|
+
"Observyze Connection Test",
|
|
838
|
+
this.config.organizationId,
|
|
839
|
+
this.config.projectId
|
|
840
|
+
);
|
|
841
|
+
const span = trace.startSpan("connection-test", import_types.SpanType.LLM);
|
|
842
|
+
span.setInput({ prompt: "Observyze SDK connection test" });
|
|
843
|
+
span.setOutput({ response: "Connection successful" });
|
|
844
|
+
span.setTokens({ input: 5, output: 4, total: 9 });
|
|
845
|
+
span.setMetadata("source", "sdk-test-connection");
|
|
846
|
+
span.end();
|
|
847
|
+
trace.setMetadata("source", "sdk-test-connection");
|
|
848
|
+
trace.addTag("setup-test");
|
|
849
|
+
trace.end(import_types.TraceStatus.SUCCESS);
|
|
850
|
+
try {
|
|
851
|
+
await this.sendWithRetry([trace]);
|
|
852
|
+
return {
|
|
853
|
+
ok: true,
|
|
854
|
+
traceId: trace.id,
|
|
855
|
+
message: `Connection successful. Test trace ${trace.id} was sent to Observyze. Search for "Observyze Connection Test" in Dashboard \u2192 Traces to confirm it landed.`
|
|
856
|
+
};
|
|
857
|
+
} catch (error) {
|
|
858
|
+
const rawMessage = error?.message || String(error);
|
|
859
|
+
const statusMatch = rawMessage.match(/\((\d{3})/);
|
|
860
|
+
const isNetworkFailure = !statusMatch && /fetch|network|ENOTFOUND|ECONNREFUSED|ETIMEDOUT/i.test(rawMessage);
|
|
861
|
+
const defaultEndpoint = "http://localhost:3001";
|
|
862
|
+
const hint = isNetworkFailure && this.config.endpoint === defaultEndpoint ? ` You are using the default endpoint (${defaultEndpoint}). For production, set endpoint: "https://api.observyze.com" in the ObservyzeClient config, then re-run.` : isNetworkFailure ? " Check that your endpoint is reachable from this environment (firewalls, proxies, DNS) and that you are not using a local endpoint in production." : "";
|
|
863
|
+
return {
|
|
864
|
+
ok: false,
|
|
865
|
+
...statusMatch ? { status: parseInt(statusMatch[1], 10) } : {},
|
|
866
|
+
message: rawMessage + hint
|
|
867
|
+
};
|
|
868
|
+
}
|
|
763
869
|
}
|
|
764
870
|
/**
|
|
765
871
|
* Sync local agent .history file to Observyze cloud
|
|
@@ -770,13 +876,11 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
770
876
|
if (typeof process === "undefined" || !process.versions?.node) {
|
|
771
877
|
throw new Error("syncLocalHistory is only available in Node.js environments");
|
|
772
878
|
}
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
const fullPath = path.resolve(process.cwd(), filePath);
|
|
776
|
-
if (!fs.existsSync(fullPath)) {
|
|
879
|
+
const fullPath = import_path.default.resolve(process.cwd(), filePath);
|
|
880
|
+
if (!import_fs.default.existsSync(fullPath)) {
|
|
777
881
|
throw new Error(`History file not found: ${fullPath}`);
|
|
778
882
|
}
|
|
779
|
-
const content =
|
|
883
|
+
const content = import_fs.default.readFileSync(fullPath, "utf-8");
|
|
780
884
|
let items = [];
|
|
781
885
|
try {
|
|
782
886
|
items = JSON.parse(content);
|
|
@@ -787,7 +891,7 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
787
891
|
items = [items];
|
|
788
892
|
}
|
|
789
893
|
if (this.config.debug) {
|
|
790
|
-
|
|
894
|
+
log3(`[Observyze SDK] Syncing ${items.length} traces from ${filePath}`);
|
|
791
895
|
}
|
|
792
896
|
for (let i = 0; i < items.length; i += this.config.batchSize) {
|
|
793
897
|
const batch = items.slice(i, i + this.config.batchSize);
|
|
@@ -801,14 +905,15 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
801
905
|
});
|
|
802
906
|
if (!response.ok) {
|
|
803
907
|
const errorBody = await response.text();
|
|
804
|
-
|
|
908
|
+
const formatted = _ObservyzeClient.formatApiError(response, errorBody);
|
|
909
|
+
throw new Error(`[Observyze SDK] Local history sync failed. ${formatted}`);
|
|
805
910
|
}
|
|
806
911
|
if (this.config.debug) {
|
|
807
|
-
|
|
912
|
+
log3(`[Observyze SDK] Synced batch of ${batch.length} traces from local history`);
|
|
808
913
|
}
|
|
809
914
|
}
|
|
810
915
|
} catch (err) {
|
|
811
|
-
|
|
916
|
+
log3.extend("error")("[Observyze SDK] Failed to sync local history:", err);
|
|
812
917
|
throw err;
|
|
813
918
|
}
|
|
814
919
|
}
|
|
@@ -905,14 +1010,225 @@ var ObservyzeClient = class _ObservyzeClient {
|
|
|
905
1010
|
}
|
|
906
1011
|
return data;
|
|
907
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Phase 4: Autonomous Circuit Breakers (Requirement 4.1)
|
|
1015
|
+
* Evaluate a trace or text for hallucination in real-time.
|
|
1016
|
+
* If hallucination score > hallucinationThreshold, the SDK blocks execution.
|
|
1017
|
+
*
|
|
1018
|
+
* Returns GuardrailResult with score: null when evaluation couldn't be performed.
|
|
1019
|
+
* In failClosed mode, null scores result in blocked execution.
|
|
1020
|
+
* In failOpen mode, null scores allow execution through.
|
|
1021
|
+
*/
|
|
1022
|
+
async checkGuardrails(content) {
|
|
1023
|
+
if (!this.config.enableCircuitBreaker) {
|
|
1024
|
+
return { pass: true, score: 0, safetyScore: 0, evaluationSource: "disabled" };
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
if (this.config.debug) {
|
|
1028
|
+
log3(`[Observyze Guardrail] Analyzing payload for hallucination anomalies...`);
|
|
1029
|
+
}
|
|
1030
|
+
const evalEndpoint = this.config.evalEndpoint;
|
|
1031
|
+
const payload = typeof content === "string" ? { text: content, organization_id: this.config.organizationId } : { trace: content, organization_id: this.config.organizationId };
|
|
1032
|
+
const controller = new AbortController();
|
|
1033
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1034
|
+
let evalResult = null;
|
|
1035
|
+
try {
|
|
1036
|
+
const response = await fetch(`${evalEndpoint}/api/v1/evaluate/hallucination`, {
|
|
1037
|
+
method: "POST",
|
|
1038
|
+
headers: {
|
|
1039
|
+
"Content-Type": "application/json",
|
|
1040
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
1041
|
+
},
|
|
1042
|
+
body: JSON.stringify(payload),
|
|
1043
|
+
signal: controller.signal
|
|
1044
|
+
});
|
|
1045
|
+
clearTimeout(timeout);
|
|
1046
|
+
if (response.ok) {
|
|
1047
|
+
evalResult = await response.json();
|
|
1048
|
+
} else {
|
|
1049
|
+
const errorBody = await response.text();
|
|
1050
|
+
const { traceId, code, message } = _ObservyzeClient.parseApiError(response, errorBody);
|
|
1051
|
+
if (this.config.debug) {
|
|
1052
|
+
log3.extend("warn")(`[Observyze Guardrail] Eval returned ${response.status} [${code}] (ref: ${traceId}): ${message}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
} catch (fetchError) {
|
|
1056
|
+
clearTimeout(timeout);
|
|
1057
|
+
if (fetchError.name === "AbortError") {
|
|
1058
|
+
if (this.config.debug) {
|
|
1059
|
+
log3.extend("warn")("[Observyze Guardrail] Evaluation timed out after 5s");
|
|
1060
|
+
}
|
|
1061
|
+
} else if (this.config.debug) {
|
|
1062
|
+
log3.extend("warn")("[Observyze Guardrail] Evaluation request failed:", fetchError.message);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
let hallucinationScore = 0;
|
|
1066
|
+
let safetyScore = 0;
|
|
1067
|
+
let evaluationSource = "live";
|
|
1068
|
+
let confidence = null;
|
|
1069
|
+
if (evalResult) {
|
|
1070
|
+
hallucinationScore = evalResult.score ?? evalResult.hallucination_score ?? null;
|
|
1071
|
+
safetyScore = evalResult.safety_score ?? 0;
|
|
1072
|
+
evaluationSource = evalResult.evaluation_source ?? "live";
|
|
1073
|
+
confidence = evalResult.confidence ?? null;
|
|
1074
|
+
if (hallucinationScore === null) {
|
|
1075
|
+
if (this.config.failClosed) {
|
|
1076
|
+
if (this.config.debug) {
|
|
1077
|
+
log3.extend("warn")("[Observyze Guardrail] Eval returned null score \u2014 failing closed (blocking)");
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
pass: false,
|
|
1081
|
+
score: null,
|
|
1082
|
+
confidence: null,
|
|
1083
|
+
safetyScore: null,
|
|
1084
|
+
evaluationSource: "error",
|
|
1085
|
+
fallbackReason: evalResult.message || "Evaluation failed to produce a score",
|
|
1086
|
+
reason: "Evaluation service failed to produce a score. Fail-closed: execution blocked."
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (this.config.debug) {
|
|
1090
|
+
log3("[Observyze Guardrail] Eval returned null score \u2014 allowing (fail-open)");
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
pass: true,
|
|
1094
|
+
score: null,
|
|
1095
|
+
confidence: null,
|
|
1096
|
+
safetyScore: null,
|
|
1097
|
+
evaluationSource: "error",
|
|
1098
|
+
fallbackReason: evalResult.message || "Evaluation failed to produce a score"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
} else if (this.config.failClosed) {
|
|
1102
|
+
if (this.config.debug) {
|
|
1103
|
+
log3.extend("warn")("[Observyze Guardrail] Eval unavailable \u2014 failing closed (blocking)");
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
pass: false,
|
|
1107
|
+
score: null,
|
|
1108
|
+
confidence: null,
|
|
1109
|
+
safetyScore: null,
|
|
1110
|
+
evaluationSource: "error",
|
|
1111
|
+
fallbackReason: "Evaluation service unreachable",
|
|
1112
|
+
reason: "Evaluation service unreachable. Fail-closed: execution blocked."
|
|
1113
|
+
};
|
|
1114
|
+
} else {
|
|
1115
|
+
if (this.config.debug) {
|
|
1116
|
+
log3("[Observyze Guardrail] Eval unavailable \u2014 allowing (fail-open)");
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
pass: true,
|
|
1120
|
+
score: null,
|
|
1121
|
+
confidence: null,
|
|
1122
|
+
safetyScore: null,
|
|
1123
|
+
evaluationSource: "error",
|
|
1124
|
+
fallbackReason: "Evaluation service unreachable"
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const hallThreshold = this.config.hallucinationThreshold;
|
|
1128
|
+
const safeThreshold = this.config.safetyThreshold;
|
|
1129
|
+
const confThreshold = this.config.confidenceThreshold;
|
|
1130
|
+
if (confidence !== null && confidence < confThreshold) {
|
|
1131
|
+
if (hallucinationScore >= hallThreshold) {
|
|
1132
|
+
if (this.config.debug) {
|
|
1133
|
+
log3.extend("warn")(`[Observyze Guardrail] High score (${hallucinationScore.toFixed(2)}) but low confidence (${confidence.toFixed(2)}). Alerting only.`);
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
pass: true,
|
|
1137
|
+
score: hallucinationScore,
|
|
1138
|
+
confidence,
|
|
1139
|
+
safetyScore,
|
|
1140
|
+
evaluationSource,
|
|
1141
|
+
reason: `Score ${hallucinationScore.toFixed(2)} but confidence ${confidence.toFixed(2)} is low. Execution allowed with alert.`
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (hallucinationScore >= hallThreshold) {
|
|
1146
|
+
if (this.config.debug) {
|
|
1147
|
+
log3.extend("warn")(`[Observyze Guardrail] Hallucination circuit breached! Score: ${hallucinationScore.toFixed(2)} >= ${hallThreshold}`);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
pass: false,
|
|
1151
|
+
score: hallucinationScore,
|
|
1152
|
+
confidence,
|
|
1153
|
+
safetyScore,
|
|
1154
|
+
evaluationSource,
|
|
1155
|
+
reason: `Hallucination score ${hallucinationScore.toFixed(2)} exceeds threshold ${hallThreshold}. Execution blocked for human review.`
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
if (safetyScore !== null && safetyScore >= safeThreshold) {
|
|
1159
|
+
if (this.config.debug) {
|
|
1160
|
+
log3.extend("warn")(`[Observyze Guardrail] Safety circuit breached! Score: ${safetyScore.toFixed(2)} >= ${safeThreshold}`);
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
pass: false,
|
|
1164
|
+
score: hallucinationScore,
|
|
1165
|
+
confidence,
|
|
1166
|
+
safetyScore,
|
|
1167
|
+
evaluationSource,
|
|
1168
|
+
reason: `Safety score ${safetyScore.toFixed(2)} exceeds threshold ${safeThreshold}. Execution blocked for safety review.`
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return { pass: true, score: hallucinationScore, confidence, safetyScore, evaluationSource };
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
log3.extend("error")("[Observyze Guardrail] Failed to evaluate:", err);
|
|
1174
|
+
if (this.config.failClosed) {
|
|
1175
|
+
return { pass: false, score: null, confidence: null, safetyScore: null, evaluationSource: "error", fallbackReason: "Guardrail exception", reason: "Guardrail error \u2014 fail-closed: execution blocked." };
|
|
1176
|
+
}
|
|
1177
|
+
return { pass: true, score: null, confidence: null, safetyScore: null, evaluationSource: "error", fallbackReason: "Guardrail exception" };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Phase 4: Autonomous Circuit Breakers
|
|
1182
|
+
* Execute an agent action wrapped with the Circuit Breaker.
|
|
1183
|
+
* Pauses execution if hallucination score >= hallucinationThreshold and requests human review.
|
|
1184
|
+
* @throws Error when execution is blocked by circuit breaker
|
|
1185
|
+
*/
|
|
1186
|
+
async executeWithCircuitBreaker(agentExecution, traceContext) {
|
|
1187
|
+
if (!this.config.enableCircuitBreaker) {
|
|
1188
|
+
return await agentExecution();
|
|
1189
|
+
}
|
|
1190
|
+
const guardResult = await this.checkGuardrails(traceContext || "execution context");
|
|
1191
|
+
if (!guardResult.pass) {
|
|
1192
|
+
const error = new Error(`[Observyze] Execution Blocked by Autonomous Circuit Breaker. Hallucination: ${guardResult.score?.toFixed(2) ?? "N/A"}, Safety: ${(guardResult.safetyScore ?? 0)?.toFixed(2) ?? "N/A"}. Reason: ${guardResult.reason}. Human approval required before agent can continue.`);
|
|
1193
|
+
if (this.config.debug) {
|
|
1194
|
+
log3.extend("error")("[Observyze CircuitBreaker] Execution blocked:", error.message);
|
|
1195
|
+
}
|
|
1196
|
+
throw error;
|
|
1197
|
+
}
|
|
1198
|
+
if (this.config.debug) {
|
|
1199
|
+
log3(`[Observyze CircuitBreaker] Execution allowed. Hallucination: ${guardResult.score?.toFixed(2) ?? "N/A"}, Safety: ${(guardResult.safetyScore ?? 0)?.toFixed(2) ?? "N/A"}`);
|
|
1200
|
+
}
|
|
1201
|
+
return await agentExecution();
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Phase 4: Bug Bounty Protocol (Automated) (Requirement 4.2)
|
|
1205
|
+
* Automatically shard persistent failure cases to external security researcher endpoints (e.g. HackerOne wrapper)
|
|
1206
|
+
*/
|
|
1207
|
+
async reportBugBounty(traceId, securityEndpoint, failureContext) {
|
|
1208
|
+
try {
|
|
1209
|
+
if (this.config.debug) {
|
|
1210
|
+
log3(`[Observyze SDK] Sharding persistent failure case ${traceId} to Bug Bounty Protocol endpoint...`);
|
|
1211
|
+
}
|
|
1212
|
+
await fetch(securityEndpoint, {
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
headers: { "Content-Type": "application/json" },
|
|
1215
|
+
body: JSON.stringify({
|
|
1216
|
+
alert: "persistent_failure_sharded",
|
|
1217
|
+
trace_id: traceId,
|
|
1218
|
+
context: failureContext,
|
|
1219
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1220
|
+
})
|
|
1221
|
+
});
|
|
1222
|
+
if (this.config.debug) {
|
|
1223
|
+
log3(`[Observyze SDK] Bug Bounty payload successfully transmitted.`);
|
|
1224
|
+
}
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
log3.extend("error")("[Observyze Bug Bounty] Failed to shard failure case:", err);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
908
1229
|
};
|
|
909
1230
|
|
|
910
|
-
// src/index.ts
|
|
911
|
-
init_types();
|
|
912
|
-
init_instrumentation();
|
|
913
|
-
|
|
914
1231
|
// src/opentelemetry/exporter.ts
|
|
915
|
-
init_types();
|
|
916
1232
|
var ObservyzeSpanExporter = class {
|
|
917
1233
|
client;
|
|
918
1234
|
config;
|
|
@@ -982,9 +1298,9 @@ var ObservyzeSpanExporter = class {
|
|
|
982
1298
|
const statusCode = status?.code;
|
|
983
1299
|
if (statusCode === 2) {
|
|
984
1300
|
oSpan.setError(new Error(status?.message || "OTel span error"));
|
|
985
|
-
trace.end(
|
|
1301
|
+
trace.end(import_types.TraceStatus.ERROR);
|
|
986
1302
|
} else {
|
|
987
|
-
trace.end(
|
|
1303
|
+
trace.end(import_types.TraceStatus.SUCCESS);
|
|
988
1304
|
}
|
|
989
1305
|
}
|
|
990
1306
|
resultCallback({ code: 0 });
|