@visibe.ai/node 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.
Files changed (41) hide show
  1. package/README.md +330 -0
  2. package/dist/cjs/api.js +92 -0
  3. package/dist/cjs/client.js +242 -0
  4. package/dist/cjs/index.js +216 -0
  5. package/dist/cjs/integrations/anthropic.js +277 -0
  6. package/dist/cjs/integrations/base.js +32 -0
  7. package/dist/cjs/integrations/bedrock.js +442 -0
  8. package/dist/cjs/integrations/group-context.js +10 -0
  9. package/dist/cjs/integrations/langchain.js +274 -0
  10. package/dist/cjs/integrations/langgraph.js +173 -0
  11. package/dist/cjs/integrations/openai.js +447 -0
  12. package/dist/cjs/integrations/vercel-ai.js +261 -0
  13. package/dist/cjs/types/index.js +5 -0
  14. package/dist/cjs/utils.js +122 -0
  15. package/dist/esm/api.js +87 -0
  16. package/dist/esm/client.js +238 -0
  17. package/dist/esm/index.js +209 -0
  18. package/dist/esm/integrations/anthropic.js +272 -0
  19. package/dist/esm/integrations/base.js +28 -0
  20. package/dist/esm/integrations/bedrock.js +438 -0
  21. package/dist/esm/integrations/group-context.js +7 -0
  22. package/dist/esm/integrations/langchain.js +269 -0
  23. package/dist/esm/integrations/langgraph.js +168 -0
  24. package/dist/esm/integrations/openai.js +442 -0
  25. package/dist/esm/integrations/vercel-ai.js +258 -0
  26. package/dist/esm/types/index.js +4 -0
  27. package/dist/esm/utils.js +116 -0
  28. package/dist/types/api.d.ts +27 -0
  29. package/dist/types/client.d.ts +50 -0
  30. package/dist/types/index.d.ts +7 -0
  31. package/dist/types/integrations/anthropic.d.ts +9 -0
  32. package/dist/types/integrations/base.d.ts +17 -0
  33. package/dist/types/integrations/bedrock.d.ts +11 -0
  34. package/dist/types/integrations/group-context.d.ts +12 -0
  35. package/dist/types/integrations/langchain.d.ts +40 -0
  36. package/dist/types/integrations/langgraph.d.ts +13 -0
  37. package/dist/types/integrations/openai.d.ts +11 -0
  38. package/dist/types/integrations/vercel-ai.d.ts +2 -0
  39. package/dist/types/types/index.d.ts +21 -0
  40. package/dist/types/utils.d.ts +23 -0
  41. package/package.json +80 -0
@@ -0,0 +1,272 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { BaseIntegration } from './base';
3
+ import { activeGroupTraceStorage } from './group-context';
4
+ import { calculateCost } from '../utils';
5
+ export class AnthropicIntegration extends BaseIntegration {
6
+ constructor() {
7
+ super(...arguments);
8
+ // Guard against re-entry: messages.stream() internally calls messages.create({stream:true}).
9
+ // When this flag is true we're already inside _wrapStream — pass create() calls through.
10
+ this._insideStream = false;
11
+ }
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ patchClient(client, agentName) {
14
+ const originalCreate = client.messages.create.bind(client.messages);
15
+ const originalStream = client.messages.stream?.bind(client.messages);
16
+ // --- messages.create ---
17
+ // NOTE: this must NOT be an async function — the Anthropic SDK's MessageStream
18
+ // calls messages.create({stream:true}) and uses .withResponse() on the return
19
+ // value, which only exists on APIPromise (not on a regular Promise). Keeping
20
+ // this function synchronous lets us return the APIPromise directly on re-entry.
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ client.messages.create = (params, options) => {
23
+ // Re-entry guard: messages.stream() calls messages.create({stream:true}) internally.
24
+ // Return the original APIPromise synchronously so .withResponse() is available.
25
+ if (params?.stream && this._insideStream) {
26
+ return originalCreate(params, options);
27
+ }
28
+ // Streaming via messages.create with stream:true is handled the same way
29
+ // as messages.stream — both return a stream object in the Anthropic SDK.
30
+ if (params?.stream) {
31
+ return this._wrapStream(
32
+ // When stream:true is passed to create(), the SDK returns a stream.
33
+ // We forward to the same stream wrapper.
34
+ (...a) => originalCreate(...a), params, options, agentName);
35
+ }
36
+ return this._wrapCreate(originalCreate, params, options, agentName);
37
+ };
38
+ // --- messages.stream (explicit stream helper) ---
39
+ if (originalStream) {
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ client.messages.stream = (params, options) => {
42
+ return this._wrapStream(originalStream, params, options, agentName);
43
+ };
44
+ }
45
+ return () => {
46
+ client.messages.create = originalCreate;
47
+ if (originalStream)
48
+ client.messages.stream = originalStream;
49
+ };
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Non-streaming
53
+ // ---------------------------------------------------------------------------
54
+ async _wrapCreate(
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ original,
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ params,
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ options, agentName) {
61
+ const groupCtx = activeGroupTraceStorage.getStore();
62
+ const traceId = groupCtx?.traceId ?? randomUUID();
63
+ const startedAt = new Date().toISOString();
64
+ const startMs = Date.now();
65
+ if (!groupCtx) {
66
+ await this.visibe.apiClient.createTrace({
67
+ trace_id: traceId,
68
+ name: agentName,
69
+ framework: 'anthropic',
70
+ started_at: startedAt,
71
+ ...(this.visibe.sessionId ? { session_id: this.visibe.sessionId } : {}),
72
+ });
73
+ }
74
+ const spanId = this.nextSpanId();
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ let response;
77
+ let spanStatus = 'success';
78
+ try {
79
+ response = await original(params, options);
80
+ }
81
+ catch (err) {
82
+ spanStatus = 'failed';
83
+ this.visibe.batcher.add(traceId, this.visibe.buildErrorSpan({
84
+ spanId: this.nextSpanId(),
85
+ errorType: err?.constructor?.name ?? 'Error',
86
+ errorMessage: err?.message ?? String(err),
87
+ }));
88
+ if (!groupCtx) {
89
+ this.visibe.batcher.flush();
90
+ await this.visibe.apiClient.completeTrace(traceId, {
91
+ status: 'failed', ended_at: new Date().toISOString(), duration_ms: Date.now() - startMs,
92
+ });
93
+ }
94
+ throw err;
95
+ }
96
+ const model = response.model ?? params.model ?? 'unknown';
97
+ const inputTokens = response.usage?.input_tokens ?? 0;
98
+ const outputTokens = response.usage?.output_tokens ?? 0;
99
+ const cost = calculateCost(model, inputTokens, outputTokens);
100
+ // Extract text; fall back to tool_use formatting when content is text-free.
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ const textBlock = response.content?.find((b) => b.type === 'text');
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const toolUse = response.content?.filter((b) => b.type === 'tool_use') ?? [];
105
+ const outputText = textBlock?.text
106
+ ?? toolUse.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join('; ');
107
+ const inputText = _extractAnthropicInputText(params);
108
+ this.visibe.batcher.add(traceId, this.visibe.buildLLMSpan({
109
+ spanId,
110
+ agentName,
111
+ model,
112
+ status: spanStatus,
113
+ inputTokens,
114
+ outputTokens,
115
+ inputText,
116
+ outputText,
117
+ durationMs: Date.now() - startMs,
118
+ }));
119
+ // Notify the group tracker (if inside track()) about this LLM span.
120
+ groupCtx?.onLLMSpan(inputTokens, outputTokens, cost);
121
+ if (!groupCtx) {
122
+ this.visibe.batcher.flush();
123
+ const sent = await this.visibe.apiClient.completeTrace(traceId, {
124
+ status: 'completed',
125
+ ended_at: new Date().toISOString(),
126
+ duration_ms: Date.now() - startMs,
127
+ llm_call_count: 1,
128
+ prompt: inputText,
129
+ model,
130
+ total_cost: cost,
131
+ total_tokens: inputTokens + outputTokens,
132
+ total_input_tokens: inputTokens,
133
+ total_output_tokens: outputTokens,
134
+ });
135
+ _printSummary(agentName, model, inputTokens, outputTokens, cost, Date.now() - startMs, sent);
136
+ }
137
+ return response;
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Streaming — listen for the final 'message' event which carries full usage.
141
+ // ---------------------------------------------------------------------------
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ _wrapStream(
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ original,
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ params,
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ options, agentName) {
150
+ // Capture group context at call time so the event handler closure can use it.
151
+ const groupCtx = activeGroupTraceStorage.getStore();
152
+ const traceId = groupCtx?.traceId ?? randomUUID();
153
+ const startedAt = new Date().toISOString();
154
+ const startMs = Date.now();
155
+ const spanId = this.nextSpanId();
156
+ // Create trace asynchronously — fire-and-forget is fine for the stream case.
157
+ if (!groupCtx) {
158
+ this.visibe.apiClient.createTrace({
159
+ trace_id: traceId,
160
+ name: agentName,
161
+ framework: 'anthropic',
162
+ started_at: startedAt,
163
+ ...(this.visibe.sessionId ? { session_id: this.visibe.sessionId } : {}),
164
+ }).catch(() => { });
165
+ }
166
+ // Set flag before calling original so that any re-entrant messages.create({stream:true})
167
+ // calls (triggered internally by the Anthropic SDK's stream helper) are passed through.
168
+ this._insideStream = true;
169
+ let stream;
170
+ try {
171
+ stream = original(params, options);
172
+ }
173
+ finally {
174
+ this._insideStream = false;
175
+ }
176
+ // 'message' fires once at the end with the complete message + usage.
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ stream.on('message', (msg) => {
179
+ const model = msg.model ?? params.model ?? 'unknown';
180
+ const inputTokens = msg.usage?.input_tokens ?? 0;
181
+ const outputTokens = msg.usage?.output_tokens ?? 0;
182
+ const cost = calculateCost(model, inputTokens, outputTokens);
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ const textBlock = msg.content?.find((b) => b.type === 'text');
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ const toolUse = msg.content?.filter((b) => b.type === 'tool_use') ?? [];
187
+ const outputText = textBlock?.text
188
+ ?? toolUse.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join('; ');
189
+ const inputText = _extractAnthropicInputText(params);
190
+ this.visibe.batcher.add(traceId, this.visibe.buildLLMSpan({
191
+ spanId,
192
+ agentName,
193
+ model,
194
+ status: 'success',
195
+ inputTokens,
196
+ outputTokens,
197
+ inputText,
198
+ outputText,
199
+ durationMs: Date.now() - startMs,
200
+ }));
201
+ // Notify the group tracker (if inside track()) about this LLM span.
202
+ groupCtx?.onLLMSpan(inputTokens, outputTokens, cost);
203
+ if (!groupCtx) {
204
+ this.visibe.batcher.flush();
205
+ this.visibe.apiClient.completeTrace(traceId, {
206
+ status: 'completed',
207
+ ended_at: new Date().toISOString(),
208
+ duration_ms: Date.now() - startMs,
209
+ llm_call_count: 1,
210
+ prompt: inputText,
211
+ model,
212
+ total_cost: cost,
213
+ total_tokens: inputTokens + outputTokens,
214
+ total_input_tokens: inputTokens,
215
+ total_output_tokens: outputTokens,
216
+ }).then(sent => {
217
+ _printSummary(agentName, model, inputTokens, outputTokens, cost, Date.now() - startMs, sent);
218
+ }).catch(() => { });
219
+ }
220
+ });
221
+ stream.on('error', (err) => {
222
+ this.visibe.batcher.add(traceId, this.visibe.buildErrorSpan({
223
+ spanId: this.nextSpanId(),
224
+ errorType: err?.constructor?.name ?? 'Error',
225
+ errorMessage: err?.message ?? String(err),
226
+ durationMs: Date.now() - startMs,
227
+ }));
228
+ if (!groupCtx) {
229
+ this.visibe.batcher.flush();
230
+ this.visibe.apiClient.completeTrace(traceId, {
231
+ status: 'failed', ended_at: new Date().toISOString(), duration_ms: Date.now() - startMs,
232
+ }).catch(() => { });
233
+ }
234
+ });
235
+ return stream;
236
+ }
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Module-level factory
240
+ // ---------------------------------------------------------------------------
241
+ export function patchAnthropicClient(
242
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
243
+ client, agentName, visibe) {
244
+ const integration = new AnthropicIntegration(visibe);
245
+ return integration.patchClient(client, agentName);
246
+ }
247
+ // ---------------------------------------------------------------------------
248
+ // Private helpers
249
+ // ---------------------------------------------------------------------------
250
+ // Extract the last user message text from Anthropic messages params.
251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
+ function _extractAnthropicInputText(params) {
253
+ const messages = params?.messages ?? [];
254
+ for (let i = messages.length - 1; i >= 0; i--) {
255
+ const msg = messages[i];
256
+ if (msg?.role === 'user') {
257
+ if (typeof msg.content === 'string')
258
+ return msg.content;
259
+ if (Array.isArray(msg.content)) {
260
+ const part = msg.content.find((b) => b.type === 'text');
261
+ return part?.text ?? '';
262
+ }
263
+ }
264
+ }
265
+ return '';
266
+ }
267
+ function _printSummary(name, model, inputTokens, outputTokens, cost, durationMs, sent) {
268
+ const durationSec = (durationMs / 1000).toFixed(1);
269
+ const tokens = (inputTokens + outputTokens).toLocaleString();
270
+ const sentStr = sent ? 'OK' : 'FAILED';
271
+ console.log(`[Visibe] Trace: ${name} | 1 LLM calls | ${tokens} tokens | $${cost.toFixed(6)} | ${durationSec}s | 0 tool calls | status: completed | model: ${model} | sent: ${sentStr}`);
272
+ }
@@ -0,0 +1,28 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Shared base for all framework integrations.
4
+ *
5
+ * Each integration subclass implements `patchClient()` which patches a specific
6
+ * client instance and returns a restore function that undoes the patch.
7
+ * The restore function is stored by `Visibe.instrument()` and called on
8
+ * `Visibe.uninstrument()`.
9
+ */
10
+ export class BaseIntegration {
11
+ constructor(visibe) {
12
+ // ---------------------------------------------------------------------------
13
+ // Shared span-ID counter
14
+ // Each integration instance has its own counter — they do not share state.
15
+ // ---------------------------------------------------------------------------
16
+ this._stepCounter = 0;
17
+ this.visibe = visibe;
18
+ }
19
+ nextSpanId() {
20
+ return `step_${++this._stepCounter}`;
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Shared trace lifecycle helpers
24
+ // ---------------------------------------------------------------------------
25
+ newTraceId() {
26
+ return randomUUID();
27
+ }
28
+ }