retrace-sdk 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +26 -42
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interceptors/anthropic.d.ts +3 -0
- package/dist/interceptors/anthropic.js +102 -0
- package/dist/interceptors/gemini.js +7 -1
- package/dist/interceptors/index.d.ts +2 -0
- package/dist/interceptors/index.js +2 -0
- package/dist/interceptors/openai.d.ts +3 -0
- package/dist/interceptors/openai.js +115 -0
- package/dist/recorder.js +6 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +68 -11
- package/package.json +30 -6
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -15
- package/dist/sdk.test.d.ts +0 -1
- package/dist/sdk.test.js +0 -132
- package/src/config.test.ts +0 -16
- package/src/config.ts +0 -31
- package/src/index.ts +0 -6
- package/src/interceptors/gemini.ts +0 -86
- package/src/interceptors/index.ts +0 -1
- package/src/recorder.ts +0 -136
- package/src/sdk.test.ts +0 -152
- package/src/trace.ts +0 -135
- package/src/transport.ts +0 -133
- package/src/utils.ts +0 -23
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yash Bogam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# retrace
|
|
1
|
+
# retrace-sdk
|
|
2
2
|
|
|
3
|
-
Record, replay, fork & share AI agent executions — TypeScript SDK.
|
|
3
|
+
The execution replay engine for AI agents. Record, replay, fork & share AI agent executions — TypeScript SDK.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,26 +8,36 @@ Record, replay, fork & share AI agent executions — TypeScript SDK.
|
|
|
8
8
|
npm install retrace-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Node.js 20+. ESM-only package.
|
|
12
|
+
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
15
|
```typescript
|
|
14
|
-
import { configure,
|
|
16
|
+
import { configure, trace } from "retrace-sdk";
|
|
15
17
|
|
|
16
|
-
configure({ apiKey: "rt_live_..."
|
|
18
|
+
configure({ apiKey: "rt_live_..." }); // Get your key at retrace.yashbogam.me/settings
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
const myAgent = trace((prompt: string) => {
|
|
20
|
-
// OpenAI calls are auto-captured
|
|
20
|
+
const myAgent = trace(async (prompt: string) => {
|
|
21
21
|
const response = await openai.chat.completions.create({
|
|
22
|
-
model: "gpt-
|
|
22
|
+
model: "gpt-5.5",
|
|
23
23
|
messages: [{ role: "user", content: prompt }],
|
|
24
24
|
});
|
|
25
25
|
return response.choices[0].message.content;
|
|
26
26
|
}, { name: "my-agent" });
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
await myAgent("What is quantum computing?");
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Auto-Instrumentation
|
|
32
|
+
|
|
33
|
+
LLM calls from all major providers are captured automatically:
|
|
34
|
+
|
|
35
|
+
- **OpenAI** — `openai.chat.completions.create()` captured
|
|
36
|
+
- **Anthropic** — `anthropic.messages.create()` captured
|
|
37
|
+
- **Google Gemini** — `ai.models.generateContent()` captured
|
|
38
|
+
|
|
39
|
+
No extra setup needed. Install the provider SDK alongside `retrace-sdk`.
|
|
40
|
+
|
|
31
41
|
## Configuration
|
|
32
42
|
|
|
33
43
|
```typescript
|
|
@@ -35,32 +45,13 @@ import { configure } from "retrace-sdk";
|
|
|
35
45
|
|
|
36
46
|
configure({
|
|
37
47
|
apiKey: "rt_live_...", // or RETRACE_API_KEY env var
|
|
38
|
-
baseUrl: "
|
|
48
|
+
baseUrl: "https://api.retrace.yashbogam.me",
|
|
39
49
|
projectId: "...", // or RETRACE_PROJECT_ID env var
|
|
40
50
|
});
|
|
41
51
|
```
|
|
42
52
|
|
|
43
53
|
Set `RETRACE_ENABLED=false` to disable recording without changing code.
|
|
44
54
|
|
|
45
|
-
## OpenAI Auto-Capture
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import { record, TraceStatus } from "retrace-sdk";
|
|
49
|
-
import OpenAI from "openai";
|
|
50
|
-
|
|
51
|
-
const openai = new OpenAI();
|
|
52
|
-
const recorder = record({ name: "chat-agent" });
|
|
53
|
-
recorder.start();
|
|
54
|
-
|
|
55
|
-
const response = await openai.chat.completions.create({
|
|
56
|
-
model: "gpt-4o",
|
|
57
|
-
messages: [{ role: "user", content: "Hello!" }],
|
|
58
|
-
});
|
|
59
|
-
// LLM call is automatically captured as a span
|
|
60
|
-
|
|
61
|
-
recorder.end(response.choices[0].message.content);
|
|
62
|
-
```
|
|
63
|
-
|
|
64
55
|
## Manual Span Creation
|
|
65
56
|
|
|
66
57
|
```typescript
|
|
@@ -76,19 +67,12 @@ recorder.endSpan(span, { results: ["..."] });
|
|
|
76
67
|
recorder.end("Done");
|
|
77
68
|
```
|
|
78
69
|
|
|
79
|
-
##
|
|
70
|
+
## API Key
|
|
80
71
|
|
|
81
|
-
|
|
72
|
+
A Retrace API key (`rt_live_...`) is required. Get yours free at [retrace.yashbogam.me/settings](https://retrace.yashbogam.me/settings).
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
import { createTransport } from "retrace/transport";
|
|
85
|
-
|
|
86
|
-
// Force HTTP transport
|
|
87
|
-
const transport = createTransport("http");
|
|
74
|
+
## Links
|
|
88
75
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Auto-detect (default)
|
|
93
|
-
const transport = createTransport("auto");
|
|
94
|
-
```
|
|
76
|
+
- [Documentation](https://retrace.yashbogam.me/docs)
|
|
77
|
+
- [GitHub](https://github.com/yash1511-bogam/retrace)
|
|
78
|
+
- [npm](https://www.npmjs.com/package/retrace-sdk)
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@ const config = {
|
|
|
8
8
|
config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
9
9
|
export function configure(opts) {
|
|
10
10
|
if (opts.apiKey && !opts.apiKey.startsWith("rt_live_")) {
|
|
11
|
-
|
|
11
|
+
throw new Error("Invalid Retrace API key. Keys must start with 'rt_live_'. Get yours at https://retrace.yashbogam.me/settings");
|
|
12
12
|
}
|
|
13
13
|
Object.assign(config, opts);
|
|
14
14
|
if (opts.baseUrl && !opts.wsUrl) {
|
|
@@ -16,6 +16,12 @@ export function configure(opts) {
|
|
|
16
16
|
}
|
|
17
17
|
return config;
|
|
18
18
|
}
|
|
19
|
+
export function requireApiKey() {
|
|
20
|
+
if (!config.apiKey) {
|
|
21
|
+
throw new Error("Retrace API key required. Call configure({ apiKey: 'rt_live_...' }) or set RETRACE_API_KEY. Get yours at https://retrace.yashbogam.me/settings");
|
|
22
|
+
}
|
|
23
|
+
return config.apiKey;
|
|
24
|
+
}
|
|
19
25
|
export function getConfig() {
|
|
20
26
|
return config;
|
|
21
27
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export { SpanBuilder, TraceBuilder } from "./trace.js";
|
|
|
4
4
|
export type { SpanData, TraceData } from "./trace.js";
|
|
5
5
|
export { SpanType, TraceStatus } from "./trace.js";
|
|
6
6
|
export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
|
|
7
|
+
export { installOpenAIInterceptor, uninstallOpenAIInterceptor } from "./interceptors/openai.js";
|
|
8
|
+
export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./interceptors/anthropic.js";
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,5 @@ export { record, trace, TraceRecorder } from "./recorder.js";
|
|
|
3
3
|
export { SpanBuilder, TraceBuilder } from "./trace.js";
|
|
4
4
|
export { SpanType, TraceStatus } from "./trace.js";
|
|
5
5
|
export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
|
|
6
|
+
export { installOpenAIInterceptor, uninstallOpenAIInterceptor } from "./interceptors/openai.js";
|
|
7
|
+
export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./interceptors/anthropic.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SpanType } from "../trace.js";
|
|
2
|
+
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
3
|
+
const PRICING = {
|
|
4
|
+
"claude-opus-4.7": [5.0, 25.0],
|
|
5
|
+
"claude-opus-4.6": [5.0, 25.0],
|
|
6
|
+
"claude-sonnet-4.6": [3.0, 15.0],
|
|
7
|
+
"claude-sonnet-4": [3.0, 15.0],
|
|
8
|
+
"claude-haiku-4.5": [1.0, 5.0],
|
|
9
|
+
"claude-3-7-sonnet": [3.0, 15.0],
|
|
10
|
+
"claude-3-5-sonnet": [3.0, 15.0],
|
|
11
|
+
"claude-3-5-haiku": [0.80, 4.0],
|
|
12
|
+
"claude-3-opus": [15.0, 75.0],
|
|
13
|
+
};
|
|
14
|
+
function calcCost(model, inputTokens, outputTokens) {
|
|
15
|
+
for (const [key, p] of Object.entries(PRICING)) {
|
|
16
|
+
if (model.includes(key))
|
|
17
|
+
return (inputTokens * p[0] + outputTokens * p[1]) / 1_000_000;
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
let originalCreate = null;
|
|
22
|
+
let installed = false;
|
|
23
|
+
let onSpanCallback = null;
|
|
24
|
+
export function installAnthropicInterceptor(onSpan) {
|
|
25
|
+
if (installed) {
|
|
26
|
+
onSpanCallback = onSpan;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onSpanCallback = onSpan;
|
|
30
|
+
import("@anthropic-ai/sdk").then((anthropicMod) => {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const mod = anthropicMod;
|
|
33
|
+
const Anthropic = mod?.Anthropic || mod?.default;
|
|
34
|
+
if (!Anthropic)
|
|
35
|
+
return;
|
|
36
|
+
const proto = Anthropic.Messages?.prototype || Object.getPrototypeOf(new Anthropic({ apiKey: "dummy" }).messages);
|
|
37
|
+
if (!proto?.create)
|
|
38
|
+
return;
|
|
39
|
+
originalCreate = proto.create;
|
|
40
|
+
proto.create = createPatchedCreate();
|
|
41
|
+
installed = true;
|
|
42
|
+
}).catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
function createPatchedCreate() {
|
|
45
|
+
return async function (...args) {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const opts = args[0] || {};
|
|
48
|
+
const model = opts.model || "unknown";
|
|
49
|
+
const messages = opts.messages || [];
|
|
50
|
+
const spanId = genId();
|
|
51
|
+
const startedAt = nowIso();
|
|
52
|
+
const startMs = Date.now();
|
|
53
|
+
try {
|
|
54
|
+
const result = await originalCreate.apply(this, args);
|
|
55
|
+
const durationMs = Date.now() - startMs;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
const res = result;
|
|
58
|
+
const inputTokens = res?.usage?.input_tokens || 0;
|
|
59
|
+
const outputTokens = res?.usage?.output_tokens || 0;
|
|
60
|
+
const output = res?.content?.[0]?.text || "";
|
|
61
|
+
const span = {
|
|
62
|
+
id: spanId, trace_id: "", parent_id: null,
|
|
63
|
+
span_type: SpanType.LLM_CALL, name: "anthropic.messages.create", model,
|
|
64
|
+
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
65
|
+
output: truncateJson(output),
|
|
66
|
+
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
67
|
+
cost: calcCost(model, inputTokens, outputTokens),
|
|
68
|
+
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
69
|
+
};
|
|
70
|
+
onSpanCallback?.(span);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const span = {
|
|
75
|
+
id: spanId, trace_id: "", parent_id: null,
|
|
76
|
+
span_type: SpanType.LLM_CALL, name: "anthropic.messages.create", model,
|
|
77
|
+
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
78
|
+
started_at: startedAt, ended_at: nowIso(),
|
|
79
|
+
duration_ms: Date.now() - startMs,
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
};
|
|
82
|
+
onSpanCallback?.(span);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function uninstallAnthropicInterceptor() {
|
|
88
|
+
if (!installed || !originalCreate)
|
|
89
|
+
return;
|
|
90
|
+
import("@anthropic-ai/sdk").then((anthropicMod) => {
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
const mod = anthropicMod;
|
|
93
|
+
const Anthropic = mod?.Anthropic || mod?.default;
|
|
94
|
+
if (!Anthropic)
|
|
95
|
+
return;
|
|
96
|
+
const proto = Anthropic.Messages?.prototype;
|
|
97
|
+
if (proto)
|
|
98
|
+
proto.create = originalCreate;
|
|
99
|
+
}).catch(() => { });
|
|
100
|
+
installed = false;
|
|
101
|
+
onSpanCallback = null;
|
|
102
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { SpanType } from "../trace.js";
|
|
2
2
|
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
3
3
|
const PRICING = {
|
|
4
|
+
"gemini-3.1-flash-lite": [0.10, 0.40],
|
|
5
|
+
"gemini-3.1-flash": [0.50, 3.0],
|
|
6
|
+
"gemini-3-flash": [0.50, 3.0],
|
|
7
|
+
"gemini-3-pro": [2.0, 12.0],
|
|
4
8
|
"gemini-3.1-pro-preview": [2.0, 12.0],
|
|
5
9
|
"gemini-2.5-pro": [1.25, 10.0],
|
|
6
|
-
"gemini-2.5-flash": [0.
|
|
10
|
+
"gemini-2.5-flash": [0.30, 2.50],
|
|
11
|
+
"gemini-2.5-flash-lite": [0.10, 0.40],
|
|
7
12
|
"gemini-2.0-flash": [0.10, 0.40],
|
|
13
|
+
"gemini-2.0-flash-lite": [0.05, 0.20],
|
|
8
14
|
};
|
|
9
15
|
function calcCost(model, inputTokens, outputTokens) {
|
|
10
16
|
const p = PRICING[model] || [0, 0];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SpanType } from "../trace.js";
|
|
2
|
+
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
3
|
+
const PRICING = {
|
|
4
|
+
"gpt-5.5-pro": [30.0, 180.0],
|
|
5
|
+
"gpt-5.5": [5.0, 30.0],
|
|
6
|
+
"gpt-5.4-pro": [15.0, 90.0],
|
|
7
|
+
"gpt-5.4-mini": [0.75, 4.50],
|
|
8
|
+
"gpt-5.4-nano": [0.20, 1.20],
|
|
9
|
+
"gpt-5.4": [2.50, 15.0],
|
|
10
|
+
"gpt-5-mini": [0.50, 3.0],
|
|
11
|
+
"gpt-5-nano": [0.10, 0.60],
|
|
12
|
+
"gpt-5": [1.25, 10.0],
|
|
13
|
+
"gpt-4.1-mini": [0.40, 1.60],
|
|
14
|
+
"gpt-4.1-nano": [0.10, 0.40],
|
|
15
|
+
"gpt-4.1": [2.0, 8.0],
|
|
16
|
+
"gpt-4o-mini": [0.15, 0.60],
|
|
17
|
+
"gpt-4o": [2.50, 10.0],
|
|
18
|
+
"o3": [10.0, 40.0],
|
|
19
|
+
"o4-mini": [1.10, 4.40],
|
|
20
|
+
"o3-mini": [1.10, 4.40],
|
|
21
|
+
};
|
|
22
|
+
function calcCost(model, inputTokens, outputTokens) {
|
|
23
|
+
for (const [key, p] of Object.entries(PRICING)) {
|
|
24
|
+
if (model.includes(key))
|
|
25
|
+
return (inputTokens * p[0] + outputTokens * p[1]) / 1_000_000;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
let originalCreate = null;
|
|
30
|
+
let installed = false;
|
|
31
|
+
let onSpanCallback = null;
|
|
32
|
+
export function installOpenAIInterceptor(onSpan) {
|
|
33
|
+
if (installed) {
|
|
34
|
+
onSpanCallback = onSpan;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
onSpanCallback = onSpan;
|
|
38
|
+
import("openai").then((openaiMod) => {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
const mod = openaiMod;
|
|
41
|
+
const proto = mod?.OpenAI?.Chat?.Completions?.prototype || mod?.default?.Chat?.Completions?.prototype;
|
|
42
|
+
if (!proto?.create) {
|
|
43
|
+
// Try alternative path
|
|
44
|
+
const OpenAI = mod?.OpenAI || mod?.default;
|
|
45
|
+
if (OpenAI) {
|
|
46
|
+
const instance = Object.getPrototypeOf(new OpenAI({ apiKey: "dummy" }).chat.completions);
|
|
47
|
+
if (instance?.create) {
|
|
48
|
+
originalCreate = instance.create;
|
|
49
|
+
instance.create = createPatchedCreate();
|
|
50
|
+
installed = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
originalCreate = proto.create;
|
|
56
|
+
proto.create = createPatchedCreate();
|
|
57
|
+
installed = true;
|
|
58
|
+
}).catch(() => { });
|
|
59
|
+
}
|
|
60
|
+
function createPatchedCreate() {
|
|
61
|
+
return async function (...args) {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
const opts = args[0] || {};
|
|
64
|
+
const model = opts.model || "unknown";
|
|
65
|
+
const messages = opts.messages || [];
|
|
66
|
+
const spanId = genId();
|
|
67
|
+
const startedAt = nowIso();
|
|
68
|
+
const startMs = Date.now();
|
|
69
|
+
try {
|
|
70
|
+
const result = await originalCreate.apply(this, args);
|
|
71
|
+
const durationMs = Date.now() - startMs;
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
const res = result;
|
|
74
|
+
const inputTokens = res?.usage?.prompt_tokens || 0;
|
|
75
|
+
const outputTokens = res?.usage?.completion_tokens || 0;
|
|
76
|
+
const output = res?.choices?.[0]?.message?.content || "";
|
|
77
|
+
const span = {
|
|
78
|
+
id: spanId, trace_id: "", parent_id: null,
|
|
79
|
+
span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
|
|
80
|
+
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
81
|
+
output: truncateJson(output),
|
|
82
|
+
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
83
|
+
cost: calcCost(model, inputTokens, outputTokens),
|
|
84
|
+
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
85
|
+
};
|
|
86
|
+
onSpanCallback?.(span);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const span = {
|
|
91
|
+
id: spanId, trace_id: "", parent_id: null,
|
|
92
|
+
span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
|
|
93
|
+
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
94
|
+
started_at: startedAt, ended_at: nowIso(),
|
|
95
|
+
duration_ms: Date.now() - startMs,
|
|
96
|
+
error: err instanceof Error ? err.message : String(err),
|
|
97
|
+
};
|
|
98
|
+
onSpanCallback?.(span);
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function uninstallOpenAIInterceptor() {
|
|
104
|
+
if (!installed || !originalCreate)
|
|
105
|
+
return;
|
|
106
|
+
import("openai").then((openaiMod) => {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const mod = openaiMod;
|
|
109
|
+
const proto = mod?.OpenAI?.Chat?.Completions?.prototype || mod?.default?.Chat?.Completions?.prototype;
|
|
110
|
+
if (proto)
|
|
111
|
+
proto.create = originalCreate;
|
|
112
|
+
}).catch(() => { });
|
|
113
|
+
installed = false;
|
|
114
|
+
onSpanCallback = null;
|
|
115
|
+
}
|
package/dist/recorder.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { getConfig } from "./config.js";
|
|
1
|
+
import { getConfig, requireApiKey } from "./config.js";
|
|
2
2
|
import { SpanBuilder, SpanType, TraceBuilder, TraceStatus } from "./trace.js";
|
|
3
3
|
import { createTransport } from "./transport.js";
|
|
4
4
|
import { installGeminiInterceptor } from "./interceptors/gemini.js";
|
|
5
|
+
import { installOpenAIInterceptor } from "./interceptors/openai.js";
|
|
6
|
+
import { installAnthropicInterceptor } from "./interceptors/anthropic.js";
|
|
5
7
|
export class TraceRecorder {
|
|
6
8
|
builder;
|
|
7
9
|
transport;
|
|
8
10
|
interceptorsInstalled = false;
|
|
9
11
|
output = undefined;
|
|
10
12
|
constructor(opts) {
|
|
13
|
+
requireApiKey();
|
|
11
14
|
this.builder = new TraceBuilder();
|
|
12
15
|
this.transport = createTransport();
|
|
13
16
|
const cfg = getConfig();
|
|
@@ -84,6 +87,8 @@ export class TraceRecorder {
|
|
|
84
87
|
if (this.interceptorsInstalled)
|
|
85
88
|
return;
|
|
86
89
|
installGeminiInterceptor((span) => this.addSpan(span));
|
|
90
|
+
installOpenAIInterceptor((span) => this.addSpan(span));
|
|
91
|
+
installAnthropicInterceptor((span) => this.addSpan(span));
|
|
87
92
|
this.interceptorsInstalled = true;
|
|
88
93
|
}
|
|
89
94
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ export interface Transport {
|
|
|
5
5
|
export declare class WSTransport implements Transport {
|
|
6
6
|
private ws;
|
|
7
7
|
private connected;
|
|
8
|
+
private closed;
|
|
8
9
|
private backoff;
|
|
9
10
|
private queue;
|
|
11
|
+
get isConnected(): boolean;
|
|
10
12
|
connect(): void;
|
|
11
13
|
private reconnect;
|
|
12
14
|
private flushQueue;
|
package/dist/transport.js
CHANGED
|
@@ -3,9 +3,13 @@ import { getConfig } from "./config.js";
|
|
|
3
3
|
export class WSTransport {
|
|
4
4
|
ws = null;
|
|
5
5
|
connected = false;
|
|
6
|
+
closed = false;
|
|
6
7
|
backoff = 1000;
|
|
7
8
|
queue = [];
|
|
9
|
+
get isConnected() { return this.connected; }
|
|
8
10
|
connect() {
|
|
11
|
+
if (this.closed)
|
|
12
|
+
return;
|
|
9
13
|
const cfg = getConfig();
|
|
10
14
|
const url = `${cfg.wsUrl}/ws/v1/stream`;
|
|
11
15
|
this.ws = new WebSocket(url);
|
|
@@ -26,15 +30,17 @@ export class WSTransport {
|
|
|
26
30
|
this.ws.on("close", () => {
|
|
27
31
|
this.connected = false;
|
|
28
32
|
this.ws = null;
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
if (!this.closed) {
|
|
34
|
+
setTimeout(() => this.reconnect(), this.backoff);
|
|
35
|
+
this.backoff = Math.min(this.backoff * 2, 30000);
|
|
36
|
+
}
|
|
31
37
|
});
|
|
32
38
|
this.ws.on("error", () => {
|
|
33
39
|
this.ws?.close();
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
42
|
reconnect() {
|
|
37
|
-
if (!this.connected && !this.ws)
|
|
43
|
+
if (!this.closed && !this.connected && !this.ws)
|
|
38
44
|
this.connect();
|
|
39
45
|
}
|
|
40
46
|
flushQueue() {
|
|
@@ -49,11 +55,12 @@ export class WSTransport {
|
|
|
49
55
|
}
|
|
50
56
|
else {
|
|
51
57
|
this.queue.push(msg);
|
|
52
|
-
if (!this.ws)
|
|
58
|
+
if (!this.ws && !this.closed)
|
|
53
59
|
this.connect();
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
close() {
|
|
63
|
+
this.closed = true;
|
|
57
64
|
if (this.ws) {
|
|
58
65
|
this.ws.close();
|
|
59
66
|
this.ws = null;
|
|
@@ -114,11 +121,61 @@ export function createTransport(mode = "auto") {
|
|
|
114
121
|
return new HTTPTransport();
|
|
115
122
|
if (mode === "ws")
|
|
116
123
|
return new WSTransport();
|
|
117
|
-
// Auto:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
// Auto: start with WS, fall back to HTTP if connection fails within timeout
|
|
125
|
+
const ws = new WSTransport();
|
|
126
|
+
const http = new HTTPTransport();
|
|
127
|
+
let useWs = true;
|
|
128
|
+
let decided = false;
|
|
129
|
+
const buffer = [];
|
|
130
|
+
const fallbackTimer = setTimeout(() => {
|
|
131
|
+
if (!decided && !ws.isConnected) {
|
|
132
|
+
decided = true;
|
|
133
|
+
useWs = false;
|
|
134
|
+
ws.close();
|
|
135
|
+
for (const item of buffer.splice(0))
|
|
136
|
+
http.send(item.eventType, item.data);
|
|
137
|
+
}
|
|
138
|
+
}, 5000);
|
|
139
|
+
ws.connect();
|
|
140
|
+
const originalSend = ws.send.bind(ws);
|
|
141
|
+
const checkConnected = () => {
|
|
142
|
+
if (!decided && ws.isConnected) {
|
|
143
|
+
decided = true;
|
|
144
|
+
clearTimeout(fallbackTimer);
|
|
145
|
+
for (const item of buffer.splice(0))
|
|
146
|
+
originalSend(item.eventType, item.data);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
send(eventType, data) {
|
|
151
|
+
checkConnected();
|
|
152
|
+
if (decided) {
|
|
153
|
+
if (useWs)
|
|
154
|
+
originalSend(eventType, data);
|
|
155
|
+
else
|
|
156
|
+
http.send(eventType, data);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
buffer.push({ eventType, data });
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
close() {
|
|
163
|
+
clearTimeout(fallbackTimer);
|
|
164
|
+
if (!decided) {
|
|
165
|
+
// WS not yet connected — force HTTP fallback to avoid data loss
|
|
166
|
+
decided = true;
|
|
167
|
+
useWs = false;
|
|
168
|
+
ws.close();
|
|
169
|
+
for (const item of buffer.splice(0))
|
|
170
|
+
http.send(item.eventType, item.data);
|
|
171
|
+
http.close();
|
|
172
|
+
}
|
|
173
|
+
else if (useWs) {
|
|
174
|
+
ws.close();
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
http.close();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
124
181
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retrace-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "The execution replay engine for AI agents. Record, replay, fork, and share agent executions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Yash Bogam",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/yash1511-bogam/retrace",
|
|
20
|
+
"directory": "packages/sdk-typescript"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://retrace.yashbogam.me/docs/sdk-typescript",
|
|
23
|
+
"keywords": ["ai", "agent", "tracing", "observability", "replay", "llm", "openai", "anthropic", "gemini"],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
14
27
|
"scripts": {
|
|
15
28
|
"dev": "tsc --watch",
|
|
16
29
|
"build": "tsc",
|
|
17
30
|
"lint": "eslint src/",
|
|
18
|
-
"test": "vitest run"
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
19
33
|
},
|
|
20
34
|
"dependencies": {
|
|
21
35
|
"ws": "8.20.0"
|
|
22
36
|
},
|
|
23
37
|
"peerDependencies": {
|
|
24
|
-
"@google/genai": ">=1.52.0"
|
|
38
|
+
"@google/genai": ">=1.52.0",
|
|
39
|
+
"openai": ">=4.0.0",
|
|
40
|
+
"@anthropic-ai/sdk": ">=0.30.0"
|
|
25
41
|
},
|
|
26
42
|
"peerDependenciesMeta": {
|
|
27
43
|
"@google/genai": {
|
|
28
44
|
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"openai": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@anthropic-ai/sdk": {
|
|
50
|
+
"optional": true
|
|
29
51
|
}
|
|
30
52
|
},
|
|
31
53
|
"devDependencies": {
|
|
32
54
|
"@google/genai": "^1.52.0",
|
|
33
55
|
"@types/node": "22.15.3",
|
|
34
56
|
"@types/ws": "8.18.1",
|
|
35
|
-
"typescript": "6.0.3"
|
|
57
|
+
"typescript": "6.0.3",
|
|
58
|
+
"openai": "^4.90.0",
|
|
59
|
+
"@anthropic-ai/sdk": "^0.95.0"
|
|
36
60
|
}
|
|
37
61
|
}
|
package/dist/config.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|