seerlens 0.2.0

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 (3) hide show
  1. package/README.md +32 -0
  2. package/index.js +103 -0
  3. package/package.json +16 -0
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # seerlens (JavaScript)
2
+
3
+ Send your Node app's LLM calls to [Seerlens](https://github.com/eladser/seerlens). Traces go out as OpenTelemetry GenAI spans, so they land in the same dashboard as the .NET and Python ones.
4
+
5
+ ```bash
6
+ npm install seerlens
7
+ ```
8
+
9
+ ```js
10
+ import * as seerlens from 'seerlens'
11
+
12
+ seerlens.configure('http://localhost:5005')
13
+
14
+ const span = seerlens.trace('answer ticket', { model: 'gpt-4o' })
15
+ const reply = await myLlm(prompt)
16
+ span.complete({ prompt, completion: reply, inputTokens: 40, outputTokens: 12 })
17
+
18
+ await seerlens.flush() // before a short script exits
19
+ ```
20
+
21
+ Or record a call you already made:
22
+
23
+ ```js
24
+ seerlens.record({ model: 'gpt-4o', prompt: 'hi', completion: 'hello', inputTokens: 10, outputTokens: 5, durationMs: 820 })
25
+ ```
26
+
27
+ Sends are fire-and-forget and errors are swallowed, so this never blocks or throws into your app. No dependencies, needs Node 18+.
28
+
29
+ ```bash
30
+ node example.js # against a running collector
31
+ npm test
32
+ ```
package/index.js ADDED
@@ -0,0 +1,103 @@
1
+ // Send your JS app's LLM calls to Seerlens.
2
+ //
3
+ // import * as seerlens from 'seerlens'
4
+ // seerlens.configure('http://localhost:5005')
5
+ //
6
+ // const span = seerlens.trace('answer ticket', { model: 'gpt-4o' })
7
+ // const reply = await myLlm(prompt)
8
+ // span.complete({ prompt, completion: reply, inputTokens: 40, outputTokens: 12 })
9
+ //
10
+ // await seerlens.flush() // before a short script exits
11
+ //
12
+ // Traces go out as OpenTelemetry GenAI spans. Sends are fire-and-forget and
13
+ // errors are swallowed, so this never blocks or breaks your app.
14
+
15
+ import { randomBytes } from 'node:crypto'
16
+
17
+ let endpoint = null
18
+ const pending = new Set()
19
+
20
+ export function configure(collectorUrl) {
21
+ endpoint = collectorUrl.replace(/\/+$/, '') + '/v1/traces'
22
+ }
23
+
24
+ export function record({
25
+ model,
26
+ prompt = '',
27
+ completion = '',
28
+ inputTokens,
29
+ outputTokens,
30
+ durationMs = 0,
31
+ system,
32
+ name,
33
+ } = {}) {
34
+ send(buildPayload({ model, prompt, completion, inputTokens, outputTokens, durationMs, system, name }))
35
+ }
36
+
37
+ // Times a call. Call complete() once you have the response.
38
+ export function trace(name, { model, system } = {}) {
39
+ const start = performance.now()
40
+ return {
41
+ complete({ prompt = '', completion = '', inputTokens, outputTokens } = {}) {
42
+ record({ model, system, name, prompt, completion, inputTokens, outputTokens, durationMs: performance.now() - start })
43
+ },
44
+ }
45
+ }
46
+
47
+ export async function flush() {
48
+ await Promise.allSettled([...pending])
49
+ }
50
+
51
+ export function buildPayload({ model, prompt, completion, inputTokens, outputTokens, durationMs, system, name }) {
52
+ const end = BigInt(Date.now()) * 1_000_000n
53
+ const start = end - BigInt(Math.round((durationMs || 0) * 1e6))
54
+ const span = {
55
+ traceId: hexId(16),
56
+ spanId: hexId(8),
57
+ parentSpanId: '',
58
+ name: name || `chat: ${model}`,
59
+ startTimeUnixNano: start.toString(),
60
+ endTimeUnixNano: end.toString(),
61
+ attributes: attrs({ model, prompt, completion, inputTokens, outputTokens, system }),
62
+ status: { code: 1 },
63
+ }
64
+ return { resourceSpans: [{ scopeSpans: [{ spans: [span] }] }] }
65
+ }
66
+
67
+ function send(payload) {
68
+ if (!endpoint) return
69
+ const p = fetch(endpoint, {
70
+ method: 'POST',
71
+ headers: { 'content-type': 'application/json' },
72
+ body: JSON.stringify(payload),
73
+ })
74
+ .then(() => {})
75
+ .catch(() => {}) // never break the host
76
+ .finally(() => pending.delete(p))
77
+ pending.add(p)
78
+ }
79
+
80
+ function attrs({ model, prompt, completion, inputTokens, outputTokens, system }) {
81
+ const out = []
82
+ const str = (k, v) => { if (v) out.push({ key: k, value: { stringValue: String(v) } }) }
83
+ const int = (k, v) => { if (v != null) out.push({ key: k, value: { intValue: String(v) } }) }
84
+ str('gen_ai.system', system || provider(model))
85
+ str('gen_ai.request.model', model)
86
+ str('gen_ai.prompt', prompt)
87
+ str('gen_ai.completion', completion)
88
+ int('gen_ai.usage.input_tokens', inputTokens)
89
+ int('gen_ai.usage.output_tokens', outputTokens)
90
+ return out
91
+ }
92
+
93
+ function provider(model) {
94
+ const m = (model || '').toLowerCase()
95
+ if (m.startsWith('gpt') || m.startsWith('o1') || m.startsWith('o3')) return 'openai'
96
+ if (m.includes('claude')) return 'anthropic'
97
+ if (m.includes('gemini')) return 'google'
98
+ return null
99
+ }
100
+
101
+ function hexId(n) {
102
+ return randomBytes(n).toString('hex')
103
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "seerlens",
3
+ "version": "0.2.0",
4
+ "description": "Send your JS app's LLM calls to Seerlens, the local DevTools for AI calls.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": "./index.js",
8
+ "scripts": {
9
+ "test": "node --test"
10
+ },
11
+ "keywords": ["llm", "ai", "observability", "tracing", "opentelemetry"],
12
+ "author": "Elad Sertshuk",
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/eladser/seerlens",
15
+ "files": ["index.js", "README.md"]
16
+ }