@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.
- package/README.md +330 -0
- package/dist/cjs/api.js +92 -0
- package/dist/cjs/client.js +242 -0
- package/dist/cjs/index.js +216 -0
- package/dist/cjs/integrations/anthropic.js +277 -0
- package/dist/cjs/integrations/base.js +32 -0
- package/dist/cjs/integrations/bedrock.js +442 -0
- package/dist/cjs/integrations/group-context.js +10 -0
- package/dist/cjs/integrations/langchain.js +274 -0
- package/dist/cjs/integrations/langgraph.js +173 -0
- package/dist/cjs/integrations/openai.js +447 -0
- package/dist/cjs/integrations/vercel-ai.js +261 -0
- package/dist/cjs/types/index.js +5 -0
- package/dist/cjs/utils.js +122 -0
- package/dist/esm/api.js +87 -0
- package/dist/esm/client.js +238 -0
- package/dist/esm/index.js +209 -0
- package/dist/esm/integrations/anthropic.js +272 -0
- package/dist/esm/integrations/base.js +28 -0
- package/dist/esm/integrations/bedrock.js +438 -0
- package/dist/esm/integrations/group-context.js +7 -0
- package/dist/esm/integrations/langchain.js +269 -0
- package/dist/esm/integrations/langgraph.js +168 -0
- package/dist/esm/integrations/openai.js +442 -0
- package/dist/esm/integrations/vercel-ai.js +258 -0
- package/dist/esm/types/index.js +4 -0
- package/dist/esm/utils.js +116 -0
- package/dist/types/api.d.ts +27 -0
- package/dist/types/client.d.ts +50 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/integrations/anthropic.d.ts +9 -0
- package/dist/types/integrations/base.d.ts +17 -0
- package/dist/types/integrations/bedrock.d.ts +11 -0
- package/dist/types/integrations/group-context.d.ts +12 -0
- package/dist/types/integrations/langchain.d.ts +40 -0
- package/dist/types/integrations/langgraph.d.ts +13 -0
- package/dist/types/integrations/openai.d.ts +11 -0
- package/dist/types/integrations/vercel-ai.d.ts +2 -0
- package/dist/types/types/index.d.ts +21 -0
- package/dist/types/utils.d.ts +23 -0
- package/package.json +80 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Visibe = void 0;
|
|
4
|
+
exports.detectFrameworks = detectFrameworks;
|
|
5
|
+
exports.init = init;
|
|
6
|
+
exports.shutdown = shutdown;
|
|
7
|
+
const client_1 = require("./client");
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Global state
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
let _globalClient = null;
|
|
12
|
+
let _shutdownRegistered = false;
|
|
13
|
+
// Saved original constructors so shutdown() can restore them.
|
|
14
|
+
// Each is typed as `any` because we need to reassign imported class bindings.
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
let _originalOpenAI = null;
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
let _originalBedrockClient = null;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
let _originalCompiledStateGraph = null;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
let _originalAnthropic = null;
|
|
23
|
+
let _vercelAIRestore = null;
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// detectFrameworks()
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function tryRequire(pkg) {
|
|
28
|
+
try {
|
|
29
|
+
require(pkg);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function detectFrameworks() {
|
|
37
|
+
return {
|
|
38
|
+
openai: tryRequire('openai'),
|
|
39
|
+
langchain: tryRequire('@langchain/core'),
|
|
40
|
+
langgraph: tryRequire('@langchain/langgraph'),
|
|
41
|
+
bedrock: tryRequire('@aws-sdk/client-bedrock-runtime'),
|
|
42
|
+
vercel_ai: tryRequire('ai'),
|
|
43
|
+
anthropic: tryRequire('@anthropic-ai/sdk'),
|
|
44
|
+
// crewai and autogen are Python-only — no Node.js equivalent
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// patchFramework() — auto-instruments a framework at the constructor level
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
function patchFramework(framework, client) {
|
|
51
|
+
try {
|
|
52
|
+
switch (framework) {
|
|
53
|
+
case 'openai': {
|
|
54
|
+
const openaiModule = require('openai');
|
|
55
|
+
_originalOpenAI = openaiModule.OpenAI;
|
|
56
|
+
openaiModule.OpenAI = class extends _originalOpenAI {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
constructor(...args) {
|
|
59
|
+
super(...args);
|
|
60
|
+
try {
|
|
61
|
+
client.instrument(this);
|
|
62
|
+
}
|
|
63
|
+
catch { /* never crash new OpenAI() */ }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'anthropic': {
|
|
69
|
+
const anthropicModule = require('@anthropic-ai/sdk');
|
|
70
|
+
_originalAnthropic = anthropicModule.Anthropic;
|
|
71
|
+
anthropicModule.Anthropic = class extends _originalAnthropic {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
constructor(...args) {
|
|
74
|
+
super(...args);
|
|
75
|
+
try {
|
|
76
|
+
client.instrument(this);
|
|
77
|
+
}
|
|
78
|
+
catch { /* never crash new Anthropic() */ }
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'bedrock': {
|
|
84
|
+
const bedrockModule = require('@aws-sdk/client-bedrock-runtime');
|
|
85
|
+
_originalBedrockClient = bedrockModule.BedrockRuntimeClient;
|
|
86
|
+
bedrockModule.BedrockRuntimeClient = class extends _originalBedrockClient {
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
constructor(...args) {
|
|
89
|
+
super(...args);
|
|
90
|
+
try {
|
|
91
|
+
client.instrument(this);
|
|
92
|
+
}
|
|
93
|
+
catch { /* never crash new BedrockRuntimeClient() */ }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'langgraph': {
|
|
99
|
+
const lgModule = require('@langchain/langgraph');
|
|
100
|
+
_originalCompiledStateGraph = lgModule.CompiledStateGraph;
|
|
101
|
+
// LangGraph instrumentation is applied via LangChainCallback at the class level.
|
|
102
|
+
// The actual patching happens inside the langgraph integration module.
|
|
103
|
+
const { patchCompiledStateGraph } = require('./integrations/langgraph');
|
|
104
|
+
patchCompiledStateGraph(lgModule, client);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'langchain': {
|
|
108
|
+
// LangChain is instrumented via RunnableSequence constructor patching.
|
|
109
|
+
const { patchRunnableSequence } = require('./integrations/langchain');
|
|
110
|
+
const lcModule = require('@langchain/core/runnables');
|
|
111
|
+
patchRunnableSequence(lcModule, client);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case 'vercel_ai': {
|
|
115
|
+
const { patchVercelAI } = require('./integrations/vercel-ai');
|
|
116
|
+
const aiModule = require('ai');
|
|
117
|
+
_vercelAIRestore = patchVercelAI(aiModule, client);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Record which frameworks were successfully patched for the startup log.
|
|
122
|
+
_autoPatchedFrameworks.push(framework);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Package not installed or patch failed — skip silently.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let _autoPatchedFrameworks = [];
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// init()
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
function init(options) {
|
|
133
|
+
if (_globalClient !== null) {
|
|
134
|
+
process.emitWarning('[Visibe] Already initialized — call shutdown() first to re-init', { type: 'VisibleSDKWarning' });
|
|
135
|
+
return _globalClient;
|
|
136
|
+
}
|
|
137
|
+
_globalClient = new client_1.Visibe(options ?? {});
|
|
138
|
+
const detected = detectFrameworks();
|
|
139
|
+
const toInstrument = options?.frameworks
|
|
140
|
+
?? Object.keys(detected).filter(k => detected[k]);
|
|
141
|
+
for (const fw of toInstrument) {
|
|
142
|
+
patchFramework(fw, _globalClient);
|
|
143
|
+
}
|
|
144
|
+
// Register graceful shutdown handlers.
|
|
145
|
+
// NOTE: process.on('exit') fires synchronously — async HTTP requests cannot
|
|
146
|
+
// complete there. SIGTERM is what Docker/Kubernetes send before killing a
|
|
147
|
+
// container; without handling it all buffered spans are lost.
|
|
148
|
+
// We await shutdown() so the batcher's 300 ms window completes before exit.
|
|
149
|
+
if (!_shutdownRegistered) {
|
|
150
|
+
const graceful = async () => { await shutdown(); process.exit(0); };
|
|
151
|
+
process.on('SIGTERM', graceful);
|
|
152
|
+
process.on('SIGINT', graceful);
|
|
153
|
+
process.on('beforeExit', () => { shutdown().catch(() => { }); });
|
|
154
|
+
_shutdownRegistered = true;
|
|
155
|
+
}
|
|
156
|
+
if (_autoPatchedFrameworks.length > 0) {
|
|
157
|
+
console.log(`[Visibe] Auto-instrumented: ${_autoPatchedFrameworks.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
return _globalClient;
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// shutdown()
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
async function shutdown() {
|
|
165
|
+
if (_globalClient === null)
|
|
166
|
+
return;
|
|
167
|
+
// Capture the client reference and clear global state immediately so that
|
|
168
|
+
// re-init() calls work without needing to await this function.
|
|
169
|
+
const client = _globalClient;
|
|
170
|
+
_globalClient = null;
|
|
171
|
+
_autoPatchedFrameworks = [];
|
|
172
|
+
// Restore patched constructors so the SDK leaves no trace after shutdown.
|
|
173
|
+
try {
|
|
174
|
+
if (_originalOpenAI) {
|
|
175
|
+
require('openai').OpenAI = _originalOpenAI;
|
|
176
|
+
_originalOpenAI = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { /* package may have been unloaded */ }
|
|
180
|
+
try {
|
|
181
|
+
if (_originalAnthropic) {
|
|
182
|
+
require('@anthropic-ai/sdk').Anthropic = _originalAnthropic;
|
|
183
|
+
_originalAnthropic = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* package may have been unloaded */ }
|
|
187
|
+
try {
|
|
188
|
+
if (_originalBedrockClient) {
|
|
189
|
+
require('@aws-sdk/client-bedrock-runtime').BedrockRuntimeClient = _originalBedrockClient;
|
|
190
|
+
_originalBedrockClient = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* package may have been unloaded */ }
|
|
194
|
+
try {
|
|
195
|
+
if (_originalCompiledStateGraph) {
|
|
196
|
+
require('@langchain/langgraph').CompiledStateGraph = _originalCompiledStateGraph;
|
|
197
|
+
_originalCompiledStateGraph = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch { /* package may have been unloaded */ }
|
|
201
|
+
try {
|
|
202
|
+
if (_vercelAIRestore) {
|
|
203
|
+
_vercelAIRestore();
|
|
204
|
+
_vercelAIRestore = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* package may have been unloaded */ }
|
|
208
|
+
// Flush buffered spans and wait up to 300 ms for in-flight HTTP requests to
|
|
209
|
+
// complete. This prevents spans from being lost on SIGTERM.
|
|
210
|
+
await client.batcher.shutdown();
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Re-export public surface
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
var client_2 = require("./client");
|
|
216
|
+
Object.defineProperty(exports, "Visibe", { enumerable: true, get: function () { return client_2.Visibe; } });
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnthropicIntegration = void 0;
|
|
4
|
+
exports.patchAnthropicClient = patchAnthropicClient;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const base_1 = require("./base");
|
|
7
|
+
const group_context_1 = require("./group-context");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
class AnthropicIntegration extends base_1.BaseIntegration {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
// Guard against re-entry: messages.stream() internally calls messages.create({stream:true}).
|
|
13
|
+
// When this flag is true we're already inside _wrapStream — pass create() calls through.
|
|
14
|
+
this._insideStream = false;
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
patchClient(client, agentName) {
|
|
18
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
19
|
+
const originalStream = client.messages.stream?.bind(client.messages);
|
|
20
|
+
// --- messages.create ---
|
|
21
|
+
// NOTE: this must NOT be an async function — the Anthropic SDK's MessageStream
|
|
22
|
+
// calls messages.create({stream:true}) and uses .withResponse() on the return
|
|
23
|
+
// value, which only exists on APIPromise (not on a regular Promise). Keeping
|
|
24
|
+
// this function synchronous lets us return the APIPromise directly on re-entry.
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
client.messages.create = (params, options) => {
|
|
27
|
+
// Re-entry guard: messages.stream() calls messages.create({stream:true}) internally.
|
|
28
|
+
// Return the original APIPromise synchronously so .withResponse() is available.
|
|
29
|
+
if (params?.stream && this._insideStream) {
|
|
30
|
+
return originalCreate(params, options);
|
|
31
|
+
}
|
|
32
|
+
// Streaming via messages.create with stream:true is handled the same way
|
|
33
|
+
// as messages.stream — both return a stream object in the Anthropic SDK.
|
|
34
|
+
if (params?.stream) {
|
|
35
|
+
return this._wrapStream(
|
|
36
|
+
// When stream:true is passed to create(), the SDK returns a stream.
|
|
37
|
+
// We forward to the same stream wrapper.
|
|
38
|
+
(...a) => originalCreate(...a), params, options, agentName);
|
|
39
|
+
}
|
|
40
|
+
return this._wrapCreate(originalCreate, params, options, agentName);
|
|
41
|
+
};
|
|
42
|
+
// --- messages.stream (explicit stream helper) ---
|
|
43
|
+
if (originalStream) {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
client.messages.stream = (params, options) => {
|
|
46
|
+
return this._wrapStream(originalStream, params, options, agentName);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
client.messages.create = originalCreate;
|
|
51
|
+
if (originalStream)
|
|
52
|
+
client.messages.stream = originalStream;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Non-streaming
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
async _wrapCreate(
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
original,
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
params,
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
options, agentName) {
|
|
65
|
+
const groupCtx = group_context_1.activeGroupTraceStorage.getStore();
|
|
66
|
+
const traceId = groupCtx?.traceId ?? (0, node_crypto_1.randomUUID)();
|
|
67
|
+
const startedAt = new Date().toISOString();
|
|
68
|
+
const startMs = Date.now();
|
|
69
|
+
if (!groupCtx) {
|
|
70
|
+
await this.visibe.apiClient.createTrace({
|
|
71
|
+
trace_id: traceId,
|
|
72
|
+
name: agentName,
|
|
73
|
+
framework: 'anthropic',
|
|
74
|
+
started_at: startedAt,
|
|
75
|
+
...(this.visibe.sessionId ? { session_id: this.visibe.sessionId } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const spanId = this.nextSpanId();
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
let response;
|
|
81
|
+
let spanStatus = 'success';
|
|
82
|
+
try {
|
|
83
|
+
response = await original(params, options);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
spanStatus = 'failed';
|
|
87
|
+
this.visibe.batcher.add(traceId, this.visibe.buildErrorSpan({
|
|
88
|
+
spanId: this.nextSpanId(),
|
|
89
|
+
errorType: err?.constructor?.name ?? 'Error',
|
|
90
|
+
errorMessage: err?.message ?? String(err),
|
|
91
|
+
}));
|
|
92
|
+
if (!groupCtx) {
|
|
93
|
+
this.visibe.batcher.flush();
|
|
94
|
+
await this.visibe.apiClient.completeTrace(traceId, {
|
|
95
|
+
status: 'failed', ended_at: new Date().toISOString(), duration_ms: Date.now() - startMs,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const model = response.model ?? params.model ?? 'unknown';
|
|
101
|
+
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
102
|
+
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
103
|
+
const cost = (0, utils_1.calculateCost)(model, inputTokens, outputTokens);
|
|
104
|
+
// Extract text; fall back to tool_use formatting when content is text-free.
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
const textBlock = response.content?.find((b) => b.type === 'text');
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const toolUse = response.content?.filter((b) => b.type === 'tool_use') ?? [];
|
|
109
|
+
const outputText = textBlock?.text
|
|
110
|
+
?? toolUse.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join('; ');
|
|
111
|
+
const inputText = _extractAnthropicInputText(params);
|
|
112
|
+
this.visibe.batcher.add(traceId, this.visibe.buildLLMSpan({
|
|
113
|
+
spanId,
|
|
114
|
+
agentName,
|
|
115
|
+
model,
|
|
116
|
+
status: spanStatus,
|
|
117
|
+
inputTokens,
|
|
118
|
+
outputTokens,
|
|
119
|
+
inputText,
|
|
120
|
+
outputText,
|
|
121
|
+
durationMs: Date.now() - startMs,
|
|
122
|
+
}));
|
|
123
|
+
// Notify the group tracker (if inside track()) about this LLM span.
|
|
124
|
+
groupCtx?.onLLMSpan(inputTokens, outputTokens, cost);
|
|
125
|
+
if (!groupCtx) {
|
|
126
|
+
this.visibe.batcher.flush();
|
|
127
|
+
const sent = await this.visibe.apiClient.completeTrace(traceId, {
|
|
128
|
+
status: 'completed',
|
|
129
|
+
ended_at: new Date().toISOString(),
|
|
130
|
+
duration_ms: Date.now() - startMs,
|
|
131
|
+
llm_call_count: 1,
|
|
132
|
+
prompt: inputText,
|
|
133
|
+
model,
|
|
134
|
+
total_cost: cost,
|
|
135
|
+
total_tokens: inputTokens + outputTokens,
|
|
136
|
+
total_input_tokens: inputTokens,
|
|
137
|
+
total_output_tokens: outputTokens,
|
|
138
|
+
});
|
|
139
|
+
_printSummary(agentName, model, inputTokens, outputTokens, cost, Date.now() - startMs, sent);
|
|
140
|
+
}
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Streaming — listen for the final 'message' event which carries full usage.
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
+
_wrapStream(
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
original,
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
params,
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
options, agentName) {
|
|
154
|
+
// Capture group context at call time so the event handler closure can use it.
|
|
155
|
+
const groupCtx = group_context_1.activeGroupTraceStorage.getStore();
|
|
156
|
+
const traceId = groupCtx?.traceId ?? (0, node_crypto_1.randomUUID)();
|
|
157
|
+
const startedAt = new Date().toISOString();
|
|
158
|
+
const startMs = Date.now();
|
|
159
|
+
const spanId = this.nextSpanId();
|
|
160
|
+
// Create trace asynchronously — fire-and-forget is fine for the stream case.
|
|
161
|
+
if (!groupCtx) {
|
|
162
|
+
this.visibe.apiClient.createTrace({
|
|
163
|
+
trace_id: traceId,
|
|
164
|
+
name: agentName,
|
|
165
|
+
framework: 'anthropic',
|
|
166
|
+
started_at: startedAt,
|
|
167
|
+
...(this.visibe.sessionId ? { session_id: this.visibe.sessionId } : {}),
|
|
168
|
+
}).catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
// Set flag before calling original so that any re-entrant messages.create({stream:true})
|
|
171
|
+
// calls (triggered internally by the Anthropic SDK's stream helper) are passed through.
|
|
172
|
+
this._insideStream = true;
|
|
173
|
+
let stream;
|
|
174
|
+
try {
|
|
175
|
+
stream = original(params, options);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
this._insideStream = false;
|
|
179
|
+
}
|
|
180
|
+
// 'message' fires once at the end with the complete message + usage.
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
stream.on('message', (msg) => {
|
|
183
|
+
const model = msg.model ?? params.model ?? 'unknown';
|
|
184
|
+
const inputTokens = msg.usage?.input_tokens ?? 0;
|
|
185
|
+
const outputTokens = msg.usage?.output_tokens ?? 0;
|
|
186
|
+
const cost = (0, utils_1.calculateCost)(model, inputTokens, outputTokens);
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
const textBlock = msg.content?.find((b) => b.type === 'text');
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
+
const toolUse = msg.content?.filter((b) => b.type === 'tool_use') ?? [];
|
|
191
|
+
const outputText = textBlock?.text
|
|
192
|
+
?? toolUse.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join('; ');
|
|
193
|
+
const inputText = _extractAnthropicInputText(params);
|
|
194
|
+
this.visibe.batcher.add(traceId, this.visibe.buildLLMSpan({
|
|
195
|
+
spanId,
|
|
196
|
+
agentName,
|
|
197
|
+
model,
|
|
198
|
+
status: 'success',
|
|
199
|
+
inputTokens,
|
|
200
|
+
outputTokens,
|
|
201
|
+
inputText,
|
|
202
|
+
outputText,
|
|
203
|
+
durationMs: Date.now() - startMs,
|
|
204
|
+
}));
|
|
205
|
+
// Notify the group tracker (if inside track()) about this LLM span.
|
|
206
|
+
groupCtx?.onLLMSpan(inputTokens, outputTokens, cost);
|
|
207
|
+
if (!groupCtx) {
|
|
208
|
+
this.visibe.batcher.flush();
|
|
209
|
+
this.visibe.apiClient.completeTrace(traceId, {
|
|
210
|
+
status: 'completed',
|
|
211
|
+
ended_at: new Date().toISOString(),
|
|
212
|
+
duration_ms: Date.now() - startMs,
|
|
213
|
+
llm_call_count: 1,
|
|
214
|
+
prompt: inputText,
|
|
215
|
+
model,
|
|
216
|
+
total_cost: cost,
|
|
217
|
+
total_tokens: inputTokens + outputTokens,
|
|
218
|
+
total_input_tokens: inputTokens,
|
|
219
|
+
total_output_tokens: outputTokens,
|
|
220
|
+
}).then(sent => {
|
|
221
|
+
_printSummary(agentName, model, inputTokens, outputTokens, cost, Date.now() - startMs, sent);
|
|
222
|
+
}).catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
stream.on('error', (err) => {
|
|
226
|
+
this.visibe.batcher.add(traceId, this.visibe.buildErrorSpan({
|
|
227
|
+
spanId: this.nextSpanId(),
|
|
228
|
+
errorType: err?.constructor?.name ?? 'Error',
|
|
229
|
+
errorMessage: err?.message ?? String(err),
|
|
230
|
+
durationMs: Date.now() - startMs,
|
|
231
|
+
}));
|
|
232
|
+
if (!groupCtx) {
|
|
233
|
+
this.visibe.batcher.flush();
|
|
234
|
+
this.visibe.apiClient.completeTrace(traceId, {
|
|
235
|
+
status: 'failed', ended_at: new Date().toISOString(), duration_ms: Date.now() - startMs,
|
|
236
|
+
}).catch(() => { });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return stream;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.AnthropicIntegration = AnthropicIntegration;
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Module-level factory
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
function patchAnthropicClient(
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
248
|
+
client, agentName, visibe) {
|
|
249
|
+
const integration = new AnthropicIntegration(visibe);
|
|
250
|
+
return integration.patchClient(client, agentName);
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Private helpers
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Extract the last user message text from Anthropic messages params.
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
257
|
+
function _extractAnthropicInputText(params) {
|
|
258
|
+
const messages = params?.messages ?? [];
|
|
259
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
260
|
+
const msg = messages[i];
|
|
261
|
+
if (msg?.role === 'user') {
|
|
262
|
+
if (typeof msg.content === 'string')
|
|
263
|
+
return msg.content;
|
|
264
|
+
if (Array.isArray(msg.content)) {
|
|
265
|
+
const part = msg.content.find((b) => b.type === 'text');
|
|
266
|
+
return part?.text ?? '';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return '';
|
|
271
|
+
}
|
|
272
|
+
function _printSummary(name, model, inputTokens, outputTokens, cost, durationMs, sent) {
|
|
273
|
+
const durationSec = (durationMs / 1000).toFixed(1);
|
|
274
|
+
const tokens = (inputTokens + outputTokens).toLocaleString();
|
|
275
|
+
const sentStr = sent ? 'OK' : 'FAILED';
|
|
276
|
+
console.log(`[Visibe] Trace: ${name} | 1 LLM calls | ${tokens} tokens | $${cost.toFixed(6)} | ${durationSec}s | 0 tool calls | status: completed | model: ${model} | sent: ${sentStr}`);
|
|
277
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseIntegration = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Shared base for all framework integrations.
|
|
7
|
+
*
|
|
8
|
+
* Each integration subclass implements `patchClient()` which patches a specific
|
|
9
|
+
* client instance and returns a restore function that undoes the patch.
|
|
10
|
+
* The restore function is stored by `Visibe.instrument()` and called on
|
|
11
|
+
* `Visibe.uninstrument()`.
|
|
12
|
+
*/
|
|
13
|
+
class BaseIntegration {
|
|
14
|
+
constructor(visibe) {
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Shared span-ID counter
|
|
17
|
+
// Each integration instance has its own counter — they do not share state.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
this._stepCounter = 0;
|
|
20
|
+
this.visibe = visibe;
|
|
21
|
+
}
|
|
22
|
+
nextSpanId() {
|
|
23
|
+
return `step_${++this._stepCounter}`;
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Shared trace lifecycle helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
newTraceId() {
|
|
29
|
+
return (0, node_crypto_1.randomUUID)();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.BaseIntegration = BaseIntegration;
|