@cendor/core 0.2.0 → 0.3.1
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 +201 -201
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +229 -46
- package/dist/instrument.js.map +1 -1
- package/dist/otel.d.ts +37 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +132 -0
- package/dist/otel.js.map +1 -0
- package/package.json +10 -2
package/dist/otel.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry GenAI span helpers (optional) + a dependency-free `gen_ai.*` ingest path. The TS
|
|
3
|
+
* mirror of `cendor.core.otel`. Emits/reads `gen_ai.*` following the OpenTelemetry GenAI semantic
|
|
4
|
+
* conventions, so the whole stack speaks the standard everyone is converging on — no proprietary
|
|
5
|
+
* telemetry format.
|
|
6
|
+
*
|
|
7
|
+
* `span()` needs `@opentelemetry/api` (an optional peer dep); it is a **no-op when the package is
|
|
8
|
+
* absent** (never throws). `ingest()` has no OTel dependency at all — it just reads a `gen_ai.*`
|
|
9
|
+
* attribute bag. Neither touches the event bus except `ingest`, which emits the normalized `LLMCall`.
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { emit } from './bus.js';
|
|
13
|
+
import { estimate } from './prices.js';
|
|
14
|
+
import { LLMCall, Usage } from './types.js';
|
|
15
|
+
function loadTracer() {
|
|
16
|
+
try {
|
|
17
|
+
const req = createRequire(import.meta.url);
|
|
18
|
+
// Loaded synchronously (mirrors Python's `from opentelemetry import trace`); no-op if absent.
|
|
19
|
+
const otel = req('@opentelemetry/api');
|
|
20
|
+
return otel.trace.getTracer('cendor.core');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null; // OpenTelemetry not installed — stay in no-op mode
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run `fn` inside a `gen_ai` span named `chat {model}`, setting `gen_ai.request.model`,
|
|
28
|
+
* `gen_ai.system` (= provider), and any extra attributes. The span is passed to `fn`, or **`null`
|
|
29
|
+
* when `@opentelemetry/api` is absent** — in which case `fn(null)` still runs and its value is
|
|
30
|
+
* returned (a no-op that never raises). The span is ended when `fn` returns (awaiting a returned
|
|
31
|
+
* promise first). Callback form (a `@contextmanager` in Python); `ingest()` is the bus path.
|
|
32
|
+
*/
|
|
33
|
+
export function span(model, opts, fn) {
|
|
34
|
+
const tracer = loadTracer();
|
|
35
|
+
if (tracer === null)
|
|
36
|
+
return fn(null);
|
|
37
|
+
const { provider, ...attributes } = opts;
|
|
38
|
+
const current = tracer.startSpan(`chat ${model}`);
|
|
39
|
+
let ended = false;
|
|
40
|
+
const end = () => {
|
|
41
|
+
if (!ended) {
|
|
42
|
+
ended = true;
|
|
43
|
+
current.end();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
current.setAttribute('gen_ai.request.model', model);
|
|
48
|
+
if (provider != null)
|
|
49
|
+
current.setAttribute('gen_ai.system', provider);
|
|
50
|
+
for (const [key, value] of Object.entries(attributes))
|
|
51
|
+
current.setAttribute(key, value);
|
|
52
|
+
const result = fn(current);
|
|
53
|
+
if (result != null && typeof result.then === 'function') {
|
|
54
|
+
return result.then((v) => {
|
|
55
|
+
end();
|
|
56
|
+
return v;
|
|
57
|
+
}, (e) => {
|
|
58
|
+
end();
|
|
59
|
+
throw e;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
end();
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
end();
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function uuidHex() {
|
|
71
|
+
return globalThis.crypto.randomUUID().replace(/-/g, '');
|
|
72
|
+
}
|
|
73
|
+
function attr(bag, ...keys) {
|
|
74
|
+
for (const k of keys) {
|
|
75
|
+
const v = bag[k];
|
|
76
|
+
if (v !== undefined && v !== null)
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Turn OpenTelemetry GenAI (`gen_ai.*`) span attributes into a normalized `LLMCall`. This is the
|
|
83
|
+
* managed-runtime capture path: when a server-side runtime owns the loop and only emits `gen_ai.*`
|
|
84
|
+
* spans, feed those attributes here and the call joins the same event bus — so `@cendor/tokenguard`
|
|
85
|
+
* and `@cendor/acttrace` consume it exactly as if it had been instrumented locally. No OTel dependency.
|
|
86
|
+
*
|
|
87
|
+
* Reads `gen_ai.request.model` (or `response.model`), `gen_ai.system`, and the usage attributes;
|
|
88
|
+
* prices it via `@cendor/core` `prices` (missing price → `cost = null`); emits on the bus unless
|
|
89
|
+
* `emit: false`. Returns the call.
|
|
90
|
+
*/
|
|
91
|
+
export function ingest(attributes, opts = {}) {
|
|
92
|
+
const { messages, emit: doEmit = true } = opts;
|
|
93
|
+
const model = String(attr(attributes, 'gen_ai.request.model', 'gen_ai.response.model') ?? '');
|
|
94
|
+
const provider = String(attributes['gen_ai.system'] ?? '');
|
|
95
|
+
const inp = attr(attributes, 'gen_ai.usage.input_tokens', 'gen_ai.usage.prompt_tokens');
|
|
96
|
+
const out = attr(attributes, 'gen_ai.usage.output_tokens', 'gen_ai.usage.completion_tokens') ?? 0;
|
|
97
|
+
// Cached / reasoning breakdowns, if the runtime reports them (a couple of common spellings).
|
|
98
|
+
const cached = attr(attributes, 'gen_ai.usage.cached_tokens', 'gen_ai.usage.cache_read_input_tokens') ?? 0;
|
|
99
|
+
const reasoning = attributes['gen_ai.usage.reasoning_tokens'] ?? 0;
|
|
100
|
+
const usage = inp != null
|
|
101
|
+
? new Usage({
|
|
102
|
+
inputTokens: Math.trunc(Number(inp)),
|
|
103
|
+
outputTokens: Math.trunc(Number(out)),
|
|
104
|
+
cachedTokens: Math.trunc(Number(cached)),
|
|
105
|
+
reasoningTokens: Math.trunc(Number(reasoning)),
|
|
106
|
+
})
|
|
107
|
+
: null;
|
|
108
|
+
const call = new LLMCall({
|
|
109
|
+
id: uuidHex(),
|
|
110
|
+
provider,
|
|
111
|
+
model,
|
|
112
|
+
messages: messages ?? [],
|
|
113
|
+
usage,
|
|
114
|
+
ts: new Date(),
|
|
115
|
+
});
|
|
116
|
+
call.metadata.source = 'otel';
|
|
117
|
+
if (usage !== null) {
|
|
118
|
+
try {
|
|
119
|
+
call.cost = estimate(call.model, usage.inputTokens, {
|
|
120
|
+
outputTokens: usage.outputTokens,
|
|
121
|
+
cachedTokens: usage.cachedTokens,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
call.cost = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (doEmit)
|
|
129
|
+
emit(call);
|
|
130
|
+
return call;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=otel.js.map
|
package/dist/otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cendor/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Wrap your LLM client once and capture exact token counts and cost on every call — the shared foundation the other Cendor tools build on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"decimal.js": "^10.4.3",
|
|
25
25
|
"js-tiktoken": "^1.0.15"
|
|
26
26
|
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@opentelemetry/api": ">=1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@opentelemetry/api": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
27
35
|
"keywords": [
|
|
28
36
|
"llm",
|
|
29
37
|
"openai",
|