@seanhogg/builderforce-memory 2026.7.14 → 2026.9.27
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 +10 -0
- package/dist/bridges/AnthropicBridge.d.ts +9 -1
- package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
- package/dist/bridges/AnthropicBridge.js +58 -2
- package/dist/bridges/AnthropicBridge.js.map +1 -1
- package/dist/bridges/CachingBridge.d.ts +9 -1
- package/dist/bridges/CachingBridge.d.ts.map +1 -1
- package/dist/bridges/CachingBridge.js +24 -1
- package/dist/bridges/CachingBridge.js.map +1 -1
- package/dist/bridges/OpenAIBridge.d.ts +4 -1
- package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
- package/dist/bridges/OpenAIBridge.js +40 -2
- package/dist/bridges/OpenAIBridge.js.map +1 -1
- package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
- package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
- package/dist/bridges/SemanticCachingBridge.js +26 -1
- package/dist/bridges/SemanticCachingBridge.js.map +1 -1
- package/dist/bridges/TransformerBridge.d.ts +13 -0
- package/dist/bridges/TransformerBridge.d.ts.map +1 -1
- package/dist/bridges/VertexAIBridge.d.ts +102 -0
- package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
- package/dist/bridges/VertexAIBridge.js +224 -0
- package/dist/bridges/VertexAIBridge.js.map +1 -0
- package/dist/bridges/index.d.ts +3 -1
- package/dist/bridges/index.d.ts.map +1 -1
- package/dist/bridges/index.js +1 -0
- package/dist/bridges/index.js.map +1 -1
- package/dist/eval/EvalHarness.d.ts +37 -0
- package/dist/eval/EvalHarness.d.ts.map +1 -0
- package/dist/eval/EvalHarness.js +178 -0
- package/dist/eval/EvalHarness.js.map +1 -0
- package/dist/eval/graders.d.ts +76 -0
- package/dist/eval/graders.d.ts.map +1 -0
- package/dist/eval/graders.js +168 -0
- package/dist/eval/graders.js.map +1 -0
- package/dist/eval/index.d.ts +26 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +26 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/types.d.ts +97 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +5 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/evermind/contract.d.ts +40 -0
- package/dist/evermind/contract.d.ts.map +1 -0
- package/dist/evermind/contract.js +13 -0
- package/dist/evermind/contract.js.map +1 -0
- package/dist/evermind/index.d.ts +15 -0
- package/dist/evermind/index.d.ts.map +1 -0
- package/dist/evermind/index.js +4 -0
- package/dist/evermind/index.js.map +1 -0
- package/dist/evermind/memoryBlock.d.ts +7 -0
- package/dist/evermind/memoryBlock.d.ts.map +1 -0
- package/dist/evermind/memoryBlock.js +37 -0
- package/dist/evermind/memoryBlock.js.map +1 -0
- package/dist/evermind/recall.d.ts +55 -0
- package/dist/evermind/recall.d.ts.map +1 -0
- package/dist/evermind/recall.js +105 -0
- package/dist/evermind/recall.js.map +1 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/IngestionPipeline.d.ts +95 -0
- package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
- package/dist/ingest/IngestionPipeline.js +295 -0
- package/dist/ingest/IngestionPipeline.js.map +1 -0
- package/dist/ingest/index.d.ts +10 -0
- package/dist/ingest/index.d.ts.map +1 -0
- package/dist/ingest/index.js +8 -0
- package/dist/ingest/index.js.map +1 -0
- package/dist/ingest/parsers.d.ts +53 -0
- package/dist/ingest/parsers.d.ts.map +1 -0
- package/dist/ingest/parsers.js +306 -0
- package/dist/ingest/parsers.js.map +1 -0
- package/dist/ingest/types.d.ts +114 -0
- package/dist/ingest/types.d.ts.map +1 -0
- package/dist/ingest/types.js +19 -0
- package/dist/ingest/types.js.map +1 -0
- package/dist/memory/MemoryStore.d.ts +33 -0
- package/dist/memory/MemoryStore.d.ts.map +1 -1
- package/dist/memory/MemoryStore.js +29 -3
- package/dist/memory/MemoryStore.js.map +1 -1
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/orchestration/AgentGraph.d.ts +104 -0
- package/dist/orchestration/AgentGraph.d.ts.map +1 -0
- package/dist/orchestration/AgentGraph.js +364 -0
- package/dist/orchestration/AgentGraph.js.map +1 -0
- package/dist/orchestration/checkpoint.d.ts +58 -0
- package/dist/orchestration/checkpoint.d.ts.map +1 -0
- package/dist/orchestration/checkpoint.js +92 -0
- package/dist/orchestration/checkpoint.js.map +1 -0
- package/dist/orchestration/index.d.ts +14 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +10 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/patterns.d.ts +159 -0
- package/dist/orchestration/patterns.d.ts.map +1 -0
- package/dist/orchestration/patterns.js +371 -0
- package/dist/orchestration/patterns.js.map +1 -0
- package/dist/orchestration/types.d.ts +146 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +20 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/rag/EnterpriseRetriever.d.ts +114 -0
- package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
- package/dist/rag/EnterpriseRetriever.js +240 -0
- package/dist/rag/EnterpriseRetriever.js.map +1 -0
- package/dist/rag/index.d.ts +6 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/index.js +5 -0
- package/dist/rag/index.js.map +1 -0
- package/dist/retrieval/bm25.d.ts +48 -1
- package/dist/retrieval/bm25.d.ts.map +1 -1
- package/dist/retrieval/bm25.js +59 -13
- package/dist/retrieval/bm25.js.map +1 -1
- package/dist/retrieval/index.d.ts +2 -2
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/index.js +1 -1
- package/dist/retrieval/index.js.map +1 -1
- package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
- package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
- package/dist/telemetry/InstrumentedBridge.js +120 -0
- package/dist/telemetry/InstrumentedBridge.js.map +1 -0
- package/dist/telemetry/MetricsRegistry.d.ts +88 -0
- package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
- package/dist/telemetry/MetricsRegistry.js +167 -0
- package/dist/telemetry/MetricsRegistry.js.map +1 -0
- package/dist/telemetry/Tracer.d.ts +137 -0
- package/dist/telemetry/Tracer.d.ts.map +1 -0
- package/dist/telemetry/Tracer.js +244 -0
- package/dist/telemetry/Tracer.js.map +1 -0
- package/dist/telemetry/exporters.d.ts +68 -0
- package/dist/telemetry/exporters.d.ts.map +1 -0
- package/dist/telemetry/exporters.js +145 -0
- package/dist/telemetry/exporters.js.map +1 -0
- package/dist/telemetry/index.d.ts +19 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +12 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/pricing.d.ts +74 -0
- package/dist/telemetry/pricing.d.ts.map +1 -0
- package/dist/telemetry/pricing.js +98 -0
- package/dist/telemetry/pricing.js.map +1 -0
- package/dist/telemetry/traceparent.d.ts +50 -0
- package/dist/telemetry/traceparent.d.ts.map +1 -0
- package/dist/telemetry/traceparent.js +77 -0
- package/dist/telemetry/traceparent.js.map +1 -0
- package/dist/telemetry/types.d.ts +90 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +17 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
- package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
- package/dist/vectorstore/MemoryVectorStore.js +205 -0
- package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
- package/dist/vectorstore/RestVectorStore.d.ts +79 -0
- package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
- package/dist/vectorstore/RestVectorStore.js +170 -0
- package/dist/vectorstore/RestVectorStore.js.map +1 -0
- package/dist/vectorstore/dialects.d.ts +101 -0
- package/dist/vectorstore/dialects.d.ts.map +1 -0
- package/dist/vectorstore/dialects.js +618 -0
- package/dist/vectorstore/dialects.js.map +1 -0
- package/dist/vectorstore/filter.d.ts +42 -0
- package/dist/vectorstore/filter.d.ts.map +1 -0
- package/dist/vectorstore/filter.js +132 -0
- package/dist/vectorstore/filter.js.map +1 -0
- package/dist/vectorstore/index.d.ts +15 -0
- package/dist/vectorstore/index.d.ts.map +1 -0
- package/dist/vectorstore/index.js +11 -0
- package/dist/vectorstore/index.js.map +1 -0
- package/dist/vectorstore/types.d.ts +151 -0
- package/dist/vectorstore/types.d.ts.map +1 -0
- package/dist/vectorstore/types.js +30 -0
- package/dist/vectorstore/types.js.map +1 -0
- package/package.json +39 -5
- package/src/bridges/AnthropicBridge.ts +68 -3
- package/src/bridges/CachingBridge.ts +28 -2
- package/src/bridges/OpenAIBridge.ts +48 -3
- package/src/bridges/SemanticCachingBridge.ts +29 -2
- package/src/bridges/TransformerBridge.ts +16 -0
- package/src/bridges/VertexAIBridge.ts +297 -0
- package/src/bridges/index.ts +3 -1
- package/src/eval/EvalHarness.ts +214 -0
- package/src/eval/graders.ts +218 -0
- package/src/eval/index.ts +26 -0
- package/src/eval/types.ts +107 -0
- package/src/evermind/contract.ts +39 -0
- package/src/evermind/index.ts +21 -0
- package/src/evermind/memoryBlock.ts +38 -0
- package/src/evermind/recall.ts +122 -0
- package/src/index.ts +232 -0
- package/src/ingest/IngestionPipeline.ts +378 -0
- package/src/ingest/index.ts +46 -0
- package/src/ingest/parsers.ts +318 -0
- package/src/ingest/types.ts +116 -0
- package/src/memory/MemoryStore.ts +55 -3
- package/src/memory/index.ts +1 -1
- package/src/orchestration/AgentGraph.ts +462 -0
- package/src/orchestration/checkpoint.ts +129 -0
- package/src/orchestration/index.ts +63 -0
- package/src/orchestration/patterns.ts +532 -0
- package/src/orchestration/types.ts +128 -0
- package/src/rag/EnterpriseRetriever.ts +348 -0
- package/src/rag/index.ts +18 -0
- package/src/retrieval/bm25.ts +71 -13
- package/src/retrieval/index.ts +2 -2
- package/src/telemetry/InstrumentedBridge.ts +152 -0
- package/src/telemetry/MetricsRegistry.ts +263 -0
- package/src/telemetry/Tracer.ts +310 -0
- package/src/telemetry/exporters.ts +187 -0
- package/src/telemetry/index.ts +56 -0
- package/src/telemetry/pricing.ts +129 -0
- package/src/telemetry/traceparent.ts +89 -0
- package/src/telemetry/types.ts +105 -0
- package/src/vectorstore/MemoryVectorStore.ts +250 -0
- package/src/vectorstore/RestVectorStore.ts +236 -0
- package/src/vectorstore/dialects.ts +702 -0
- package/src/vectorstore/filter.ts +148 -0
- package/src/vectorstore/index.ts +53 -0
- package/src/vectorstore/types.ts +149 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry/exporters.ts — where finished spans go.
|
|
3
|
+
*
|
|
4
|
+
* Three sinks cover the deployment spectrum without a vendor dependency:
|
|
5
|
+
* • {@link InMemorySpanExporter} — tests and in-process dashboards.
|
|
6
|
+
* • {@link ConsoleSpanExporter} — local development.
|
|
7
|
+
* • {@link OtlpHttpSpanExporter} — any OTLP/HTTP collector, which includes
|
|
8
|
+
* Google Cloud Trace, Datadog, Honeycomb, Grafana Tempo and the OpenTelemetry
|
|
9
|
+
* Collector. It speaks the protocol over `fetch` rather than importing the
|
|
10
|
+
* OpenTelemetry SDK, so the package stays zero-dependency and still runs in a
|
|
11
|
+
* browser or a Cloudflare Worker where the Node SDK cannot.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AttributeValue, SpanData, SpanExporter } from './types.js';
|
|
15
|
+
|
|
16
|
+
export class InMemorySpanExporter implements SpanExporter {
|
|
17
|
+
private readonly _spans: SpanData[] = [];
|
|
18
|
+
|
|
19
|
+
export(spans: readonly SpanData[]): void {
|
|
20
|
+
this._spans.push(...spans);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get spans(): readonly SpanData[] { return this._spans; }
|
|
24
|
+
|
|
25
|
+
/** Spans belonging to one trace, oldest first. */
|
|
26
|
+
trace(traceId: string): SpanData[] {
|
|
27
|
+
return this._spans.filter((s) => s.traceId === traceId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
reset(): void { this._spans.length = 0; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConsoleSpanExporterOptions {
|
|
34
|
+
/** Sink for the rendered lines. Default `console.log`. */
|
|
35
|
+
write?: (line: string) => void;
|
|
36
|
+
/** Include span attributes. Default false — one line per span. */
|
|
37
|
+
verbose?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ConsoleSpanExporter implements SpanExporter {
|
|
41
|
+
private readonly _write: (line: string) => void;
|
|
42
|
+
private readonly _verbose: boolean;
|
|
43
|
+
|
|
44
|
+
constructor(opts: ConsoleSpanExporterOptions = {}) {
|
|
45
|
+
this._write = opts.write ?? ((line) => console.log(line));
|
|
46
|
+
this._verbose = opts.verbose ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export(spans: readonly SpanData[]): void {
|
|
50
|
+
for (const span of spans) {
|
|
51
|
+
const cost = span.costUsd !== undefined ? ` $${span.costUsd.toFixed(6)}` : '';
|
|
52
|
+
const tokens = span.usage ? ` ${span.usage.inputTokens}in/${span.usage.outputTokens}out` : '';
|
|
53
|
+
const status = span.status === 'error' ? ` ERROR: ${span.error ?? ''}` : '';
|
|
54
|
+
this._write(
|
|
55
|
+
`[${span.traceId.slice(0, 8)}] ${span.kind.padEnd(9)} ${span.name} ` +
|
|
56
|
+
`${Math.round(span.durationMs ?? 0)}ms${tokens}${cost}${status}`,
|
|
57
|
+
);
|
|
58
|
+
if (this._verbose && Object.keys(span.attributes).length > 0) {
|
|
59
|
+
this._write(` ${JSON.stringify(span.attributes)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OtlpHttpSpanExporterOptions {
|
|
66
|
+
/** Collector endpoint, e.g. `https://otlp.example.com/v1/traces`. */
|
|
67
|
+
url: string;
|
|
68
|
+
/** Extra headers (auth token, GCP project header, …). */
|
|
69
|
+
headers?: Record<string, string>;
|
|
70
|
+
/** `service.name` reported on the resource. Default `evermind`. */
|
|
71
|
+
serviceName?: string;
|
|
72
|
+
/** Injected for test. Default global `fetch`. */
|
|
73
|
+
fetchImpl?: typeof fetch;
|
|
74
|
+
/**
|
|
75
|
+
* Called when an export request fails. Default: swallow. Telemetry must never
|
|
76
|
+
* take down the request it is measuring, so failures are reported, not thrown.
|
|
77
|
+
*/
|
|
78
|
+
onError?: (err: unknown) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** OTLP span kind codes: UNSPECIFIED 0, INTERNAL 1, SERVER 2, CLIENT 3. */
|
|
82
|
+
const OTLP_CLIENT = 3;
|
|
83
|
+
const OTLP_INTERNAL = 1;
|
|
84
|
+
|
|
85
|
+
export class OtlpHttpSpanExporter implements SpanExporter {
|
|
86
|
+
private readonly _url: string;
|
|
87
|
+
private readonly _headers: Record<string, string>;
|
|
88
|
+
private readonly _serviceName: string;
|
|
89
|
+
private readonly _fetch: typeof fetch;
|
|
90
|
+
private readonly _onError: (err: unknown) => void;
|
|
91
|
+
|
|
92
|
+
constructor(opts: OtlpHttpSpanExporterOptions) {
|
|
93
|
+
this._url = opts.url;
|
|
94
|
+
this._headers = opts.headers ?? {};
|
|
95
|
+
this._serviceName = opts.serviceName ?? 'evermind';
|
|
96
|
+
this._fetch = opts.fetchImpl ?? ((...args) => fetch(...args));
|
|
97
|
+
this._onError = opts.onError ?? (() => {});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async export(spans: readonly SpanData[]): Promise<void> {
|
|
101
|
+
if (spans.length === 0) return;
|
|
102
|
+
try {
|
|
103
|
+
const res = await this._fetch(this._url, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json', ...this._headers },
|
|
106
|
+
body: JSON.stringify(toOtlpPayload(spans, this._serviceName)),
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) this._onError(new Error(`OTLP export failed: ${res.status}`));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this._onError(err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Wraps spans in the OTLP/HTTP JSON envelope. */
|
|
116
|
+
export function toOtlpPayload(spans: readonly SpanData[], serviceName = 'evermind'): unknown {
|
|
117
|
+
return {
|
|
118
|
+
resourceSpans: [{
|
|
119
|
+
resource: { attributes: [otlpAttr('service.name', serviceName)] },
|
|
120
|
+
scopeSpans: [{
|
|
121
|
+
scope: { name: '@seanhogg/builderforce-memory' },
|
|
122
|
+
spans: spans.map(toOtlpSpan),
|
|
123
|
+
}],
|
|
124
|
+
}],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Converts one span to the OTLP wire shape. The mapping is a rename, not a
|
|
130
|
+
* re-model, because {@link SpanData} was designed against this schema.
|
|
131
|
+
*
|
|
132
|
+
* LLM attributes follow the OpenTelemetry GenAI semantic conventions
|
|
133
|
+
* (`gen_ai.*`) so a stock trace viewer renders token counts without config.
|
|
134
|
+
*/
|
|
135
|
+
export function toOtlpSpan(span: SpanData): unknown {
|
|
136
|
+
const attributes = Object.entries(span.attributes).map(([k, v]) => otlpAttr(k, v));
|
|
137
|
+
|
|
138
|
+
if (span.usage) {
|
|
139
|
+
attributes.push(
|
|
140
|
+
otlpAttr('gen_ai.request.model', span.usage.model),
|
|
141
|
+
otlpAttr('gen_ai.usage.input_tokens', span.usage.inputTokens),
|
|
142
|
+
otlpAttr('gen_ai.usage.output_tokens', span.usage.outputTokens),
|
|
143
|
+
);
|
|
144
|
+
if (span.usage.cachedInputTokens !== undefined) {
|
|
145
|
+
attributes.push(otlpAttr('gen_ai.usage.cached_input_tokens', span.usage.cachedInputTokens));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (span.costUsd !== undefined) attributes.push(otlpAttr('gen_ai.usage.cost_usd', span.costUsd));
|
|
149
|
+
if (span.timeToFirstTokenMs !== undefined) {
|
|
150
|
+
attributes.push(otlpAttr('gen_ai.server.time_to_first_token_ms', span.timeToFirstTokenMs));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const out: Record<string, unknown> = {
|
|
154
|
+
traceId: span.traceId,
|
|
155
|
+
spanId: span.spanId,
|
|
156
|
+
name: span.name,
|
|
157
|
+
kind: span.kind === 'llm' || span.kind === 'embedding' ? OTLP_CLIENT : OTLP_INTERNAL,
|
|
158
|
+
startTimeUnixNano: msToNano(span.startedAt),
|
|
159
|
+
endTimeUnixNano: msToNano(span.endedAt ?? span.startedAt),
|
|
160
|
+
attributes,
|
|
161
|
+
events: span.events.map((e) => ({
|
|
162
|
+
name: e.name,
|
|
163
|
+
timeUnixNano: msToNano(e.at),
|
|
164
|
+
attributes: Object.entries(e.attributes ?? {}).map(([k, v]) => otlpAttr(k, v)),
|
|
165
|
+
})),
|
|
166
|
+
status: span.status === 'error'
|
|
167
|
+
? { code: 2, message: span.error ?? '' }
|
|
168
|
+
: { code: span.status === 'ok' ? 1 : 0 },
|
|
169
|
+
};
|
|
170
|
+
if (span.parentSpanId) out['parentSpanId'] = span.parentSpanId;
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function otlpAttr(key: string, value: AttributeValue): unknown {
|
|
175
|
+
if (typeof value === 'number') {
|
|
176
|
+
return Number.isInteger(value)
|
|
177
|
+
? { key, value: { intValue: value } }
|
|
178
|
+
: { key, value: { doubleValue: value } };
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'boolean') return { key, value: { boolValue: value } };
|
|
181
|
+
return { key, value: { stringValue: value } };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** OTLP timestamps are nanoseconds since the epoch, as a decimal string. */
|
|
185
|
+
function msToNano(ms: number): string {
|
|
186
|
+
return `${Math.round(ms)}000000`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry — granular tracing plus LLM-native metrics (tokens/sec, TTFT,
|
|
3
|
+
* cost-per-request, cache-hit rate), built on ONE span stream so the trace view
|
|
4
|
+
* and the metric view can never disagree.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { Tracer, Span } from './Tracer.js';
|
|
8
|
+
export type { TracerOptions, SpanOptions } from './Tracer.js';
|
|
9
|
+
|
|
10
|
+
export { parseTraceparent, formatTraceparent } from './traceparent.js';
|
|
11
|
+
export type { TraceContext } from './traceparent.js';
|
|
12
|
+
|
|
13
|
+
export { MetricsRegistry, percentile } from './MetricsRegistry.js';
|
|
14
|
+
export type {
|
|
15
|
+
LlmMetricSnapshot,
|
|
16
|
+
KindMetricSnapshot,
|
|
17
|
+
MetricsRegistryOptions,
|
|
18
|
+
} from './MetricsRegistry.js';
|
|
19
|
+
|
|
20
|
+
export { InstrumentedBridge } from './InstrumentedBridge.js';
|
|
21
|
+
export type { InstrumentedBridgeOptions } from './InstrumentedBridge.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
InMemorySpanExporter,
|
|
25
|
+
ConsoleSpanExporter,
|
|
26
|
+
OtlpHttpSpanExporter,
|
|
27
|
+
toOtlpPayload,
|
|
28
|
+
toOtlpSpan,
|
|
29
|
+
} from './exporters.js';
|
|
30
|
+
export type {
|
|
31
|
+
ConsoleSpanExporterOptions,
|
|
32
|
+
OtlpHttpSpanExporterOptions,
|
|
33
|
+
} from './exporters.js';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
estimateCostUsd,
|
|
37
|
+
estimateTokens,
|
|
38
|
+
resolveRate,
|
|
39
|
+
DEFAULT_PRICE_BOOK,
|
|
40
|
+
ANTHROPIC_PRICE_BOOK,
|
|
41
|
+
EVERMIND_PRICE_BOOK,
|
|
42
|
+
DEFAULT_CACHE_READ_MULTIPLIER,
|
|
43
|
+
DEFAULT_CACHE_WRITE_MULTIPLIER,
|
|
44
|
+
} from './pricing.js';
|
|
45
|
+
export type { ModelRate, PriceBook } from './pricing.js';
|
|
46
|
+
|
|
47
|
+
export type {
|
|
48
|
+
AttributeValue,
|
|
49
|
+
SpanKind,
|
|
50
|
+
SpanStatus,
|
|
51
|
+
SpanEvent,
|
|
52
|
+
SpanData,
|
|
53
|
+
SpanExporter,
|
|
54
|
+
LlmUsage,
|
|
55
|
+
BridgeCallInfo,
|
|
56
|
+
} from './types.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry/pricing.ts — cost-per-request, computed from real token counts.
|
|
3
|
+
*
|
|
4
|
+
* "Cost per request" is an LLM-native metric that cannot be derived from latency
|
|
5
|
+
* or call count: it depends on the model, the input/output split, AND how much of
|
|
6
|
+
* the input was a cache read (cheap) versus a cache write (dear). This module is
|
|
7
|
+
* the ONE place that arithmetic lives, so the bridges, the metrics registry, and
|
|
8
|
+
* the eval harness cannot disagree about what a run cost.
|
|
9
|
+
*
|
|
10
|
+
* Rates are DATA, not code: an enterprise on a negotiated rate card, a partner
|
|
11
|
+
* platform (Bedrock/Vertex bill separately from the first-party API), or a model
|
|
12
|
+
* released after this file was written is handled by passing a {@link PriceBook},
|
|
13
|
+
* never by editing a branch. Unknown models cost `undefined` — reported as an
|
|
14
|
+
* `unpricedCalls` count rather than silently billed at zero, because a silent
|
|
15
|
+
* zero is how cost dashboards lie.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { LlmUsage } from './types.js';
|
|
19
|
+
|
|
20
|
+
export interface ModelRate {
|
|
21
|
+
/** USD per 1M input tokens. */
|
|
22
|
+
inputPerMTok: number;
|
|
23
|
+
/** USD per 1M output tokens. */
|
|
24
|
+
outputPerMTok: number;
|
|
25
|
+
/**
|
|
26
|
+
* Multiplier applied to `inputPerMTok` for tokens READ from the provider's
|
|
27
|
+
* prompt cache. Anthropic bills cache reads at 10% of input. Default 0.1.
|
|
28
|
+
*/
|
|
29
|
+
cacheReadMultiplier?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Multiplier applied to `inputPerMTok` for tokens WRITTEN to the prompt
|
|
32
|
+
* cache. Anthropic bills a 5-minute cache write at 125% of input. Default 1.25.
|
|
33
|
+
*/
|
|
34
|
+
cacheWriteMultiplier?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Model id → rate. Keys match exactly, or by longest prefix (see {@link resolveRate}). */
|
|
38
|
+
export type PriceBook = Record<string, ModelRate>;
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
41
|
+
export const DEFAULT_CACHE_WRITE_MULTIPLIER = 1.25;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* First-party Anthropic API rates (USD / 1M tokens), current as of 2026-08-20.
|
|
45
|
+
*
|
|
46
|
+
* Bedrock and Vertex are partner-operated and priced separately — supply those
|
|
47
|
+
* rates via a custom {@link PriceBook} rather than assuming these apply.
|
|
48
|
+
*/
|
|
49
|
+
export const ANTHROPIC_PRICE_BOOK: PriceBook = {
|
|
50
|
+
'claude-fable-5': { inputPerMTok: 10, outputPerMTok: 50 },
|
|
51
|
+
'claude-mythos-5': { inputPerMTok: 10, outputPerMTok: 50 },
|
|
52
|
+
'claude-opus-5': { inputPerMTok: 5, outputPerMTok: 25 },
|
|
53
|
+
'claude-opus-4-8': { inputPerMTok: 5, outputPerMTok: 25 },
|
|
54
|
+
'claude-opus-4-7': { inputPerMTok: 5, outputPerMTok: 25 },
|
|
55
|
+
'claude-opus-4-6': { inputPerMTok: 5, outputPerMTok: 25 },
|
|
56
|
+
'claude-sonnet-5': { inputPerMTok: 3, outputPerMTok: 15 },
|
|
57
|
+
'claude-sonnet-4-6': { inputPerMTok: 3, outputPerMTok: 15 },
|
|
58
|
+
'claude-haiku-4-5': { inputPerMTok: 1, outputPerMTok: 5 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Evermind serves `evermind/<ref>` traffic on-device, so it has no per-token
|
|
63
|
+
* vendor bill. It is priced explicitly at zero rather than left unknown — the
|
|
64
|
+
* whole point of routing to Evermind is that the saving is *measurable*.
|
|
65
|
+
*/
|
|
66
|
+
export const EVERMIND_PRICE_BOOK: PriceBook = {
|
|
67
|
+
'evermind': { inputPerMTok: 0, outputPerMTok: 0 },
|
|
68
|
+
'ssm': { inputPerMTok: 0, outputPerMTok: 0 },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_PRICE_BOOK: PriceBook = { ...ANTHROPIC_PRICE_BOOK, ...EVERMIND_PRICE_BOOK };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a model id to a rate: exact match first, then the longest key the
|
|
75
|
+
* model id starts with. Prefix matching is what makes dated snapshots
|
|
76
|
+
* (`claude-opus-4-5@20251101`) and platform-prefixed ids (`anthropic.claude-opus-5`)
|
|
77
|
+
* resolve without a per-variant entry.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveRate(model: string, book: PriceBook = DEFAULT_PRICE_BOOK): ModelRate | undefined {
|
|
80
|
+
const exact = book[model];
|
|
81
|
+
if (exact) return exact;
|
|
82
|
+
|
|
83
|
+
let best: ModelRate | undefined;
|
|
84
|
+
let bestLen = 0;
|
|
85
|
+
for (const [key, rate] of Object.entries(book)) {
|
|
86
|
+
if (key.length > bestLen && (model.startsWith(key) || model.includes(`/${key}`))) {
|
|
87
|
+
best = rate;
|
|
88
|
+
bestLen = key.length;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return best;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cost in USD for one call, or `undefined` when the model is not in the book.
|
|
96
|
+
*
|
|
97
|
+
* A locally-served call (`localCacheHit`) is free by construction — it never
|
|
98
|
+
* reached a provider — so it costs 0 even for an unpriced model.
|
|
99
|
+
*/
|
|
100
|
+
export function estimateCostUsd(usage: LlmUsage, book: PriceBook = DEFAULT_PRICE_BOOK): number | undefined {
|
|
101
|
+
if (usage.localCacheHit) return 0;
|
|
102
|
+
|
|
103
|
+
const rate = resolveRate(usage.model, book);
|
|
104
|
+
if (!rate) return undefined;
|
|
105
|
+
|
|
106
|
+
const cacheRead = rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER;
|
|
107
|
+
const cacheWrite = rate.cacheWriteMultiplier ?? DEFAULT_CACHE_WRITE_MULTIPLIER;
|
|
108
|
+
|
|
109
|
+
const perToken = (perMTok: number) => perMTok / 1_000_000;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
usage.inputTokens * perToken(rate.inputPerMTok) +
|
|
113
|
+
usage.outputTokens * perToken(rate.outputPerMTok) +
|
|
114
|
+
(usage.cachedInputTokens ?? 0) * perToken(rate.inputPerMTok * cacheRead) +
|
|
115
|
+
(usage.cacheWriteTokens ?? 0) * perToken(rate.inputPerMTok * cacheWrite)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Character-based token estimate — the fallback when a provider reports no usage.
|
|
121
|
+
*
|
|
122
|
+
* ~4 characters per token is the standard English approximation. Estimates are
|
|
123
|
+
* flagged (`estimated: true`) all the way through to the metric snapshot so a
|
|
124
|
+
* cost figure never silently mixes measured and guessed numbers.
|
|
125
|
+
*/
|
|
126
|
+
export function estimateTokens(text: string): number {
|
|
127
|
+
if (!text) return 0;
|
|
128
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
129
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry/traceparent.ts — W3C Trace Context propagation.
|
|
3
|
+
*
|
|
4
|
+
* A {@link Tracer} on its own produces one span tree per PROCESS. A cloud run is not
|
|
5
|
+
* one process: portal → api → relay → runtime is four, and without propagation each
|
|
6
|
+
* hop opens its own root trace, so the operator gets four unrelated trees and has to
|
|
7
|
+
* correlate them by timestamp. Head-based propagation is what makes them one tree —
|
|
8
|
+
* the caller serialises `{ traceId, spanId, sampled }` into a `traceparent` header,
|
|
9
|
+
* the callee parses it and starts its root span as a CHILD of the remote span.
|
|
10
|
+
*
|
|
11
|
+
* The sampling flag travels with it on purpose: a sampling decision taken per-hop
|
|
12
|
+
* would record hop 1 and drop hop 3, which is a tree with holes in it — the same
|
|
13
|
+
* failure the in-process `_sampled` map exists to prevent, one network hop out.
|
|
14
|
+
*
|
|
15
|
+
* Pure string work, zero dependencies, no OpenTelemetry SDK — the wire format is a
|
|
16
|
+
* 55-character ASCII string and re-implementing it costs less than the dependency.
|
|
17
|
+
*
|
|
18
|
+
* @see https://www.w3.org/TR/trace-context/
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A trace position received from (or sent to) another process. */
|
|
22
|
+
export interface TraceContext {
|
|
23
|
+
/** 32 lowercase hex chars, never all-zero. */
|
|
24
|
+
traceId: string;
|
|
25
|
+
/** 16 lowercase hex chars, never all-zero — the REMOTE span, parent of ours. */
|
|
26
|
+
spanId: string;
|
|
27
|
+
/** The `sampled` flag (bit 0x01). False means the caller decided to drop the trace. */
|
|
28
|
+
sampled: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The only trace-flag bit W3C defines today. */
|
|
32
|
+
const FLAG_SAMPLED = 0x01;
|
|
33
|
+
|
|
34
|
+
const TRACE_ID_LENGTH = 32;
|
|
35
|
+
const SPAN_ID_LENGTH = 16;
|
|
36
|
+
|
|
37
|
+
const HEX = /^[0-9a-f]+$/;
|
|
38
|
+
|
|
39
|
+
/** True when `value` is `length` lowercase hex chars and not entirely zeros. */
|
|
40
|
+
function isUsableId(value: string, length: number): boolean {
|
|
41
|
+
return value.length === length && HEX.test(value) && /[1-9a-f]/.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a `traceparent` header, or `null` when it is absent or unusable.
|
|
46
|
+
*
|
|
47
|
+
* Rejecting rather than repairing is deliberate: a malformed header means the caller
|
|
48
|
+
* is not actually in the trace we would be joining, and inventing a link to it
|
|
49
|
+
* produces a tree that misattributes latency. `null` makes the callee start a fresh
|
|
50
|
+
* root, which is at least true.
|
|
51
|
+
*
|
|
52
|
+
* Unknown future versions are accepted by reading the first four fields — that is
|
|
53
|
+
* what the spec requires, so a later spec revision does not silently break every
|
|
54
|
+
* hop. Version `ff` is invalid by definition and is rejected.
|
|
55
|
+
*/
|
|
56
|
+
export function parseTraceparent(header: string | null | undefined): TraceContext | null {
|
|
57
|
+
if (typeof header !== 'string') return null;
|
|
58
|
+
const parts = header.trim().toLowerCase().split('-');
|
|
59
|
+
if (parts.length < 4) return null;
|
|
60
|
+
|
|
61
|
+
const [version, traceId, spanId, flags] = parts as [string, string, string, string];
|
|
62
|
+
if (version.length !== 2 || !HEX.test(version) || version === 'ff') return null;
|
|
63
|
+
// Version 00 is exactly four fields; a longer header at version 00 is malformed.
|
|
64
|
+
if (version === '00' && parts.length !== 4) return null;
|
|
65
|
+
if (!isUsableId(traceId, TRACE_ID_LENGTH) || !isUsableId(spanId, SPAN_ID_LENGTH)) return null;
|
|
66
|
+
if (flags.length !== 2 || !HEX.test(flags)) return null;
|
|
67
|
+
|
|
68
|
+
return { traceId, spanId, sampled: (parseInt(flags, 16) & FLAG_SAMPLED) !== 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Serialise a trace position as a version-`00` `traceparent` header.
|
|
73
|
+
*
|
|
74
|
+
* Ids are normalised to the wire widths — left-padded when short, right-trimmed when
|
|
75
|
+
* long — so a tracer configured with a non-standard id factory still emits a legal
|
|
76
|
+
* header rather than one every downstream collector drops.
|
|
77
|
+
*/
|
|
78
|
+
export function formatTraceparent(ctx: TraceContext): string {
|
|
79
|
+
const traceId = normaliseId(ctx.traceId, TRACE_ID_LENGTH);
|
|
80
|
+
const spanId = normaliseId(ctx.spanId, SPAN_ID_LENGTH);
|
|
81
|
+
const flags = (ctx.sampled ? FLAG_SAMPLED : 0).toString(16).padStart(2, '0');
|
|
82
|
+
return `00-${traceId}-${spanId}-${flags}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Pad/trim an id to the wire width, dropping any non-hex byte to `0`. */
|
|
86
|
+
function normaliseId(value: string, length: number): string {
|
|
87
|
+
const hex = (value ?? '').toLowerCase().replace(/[^0-9a-f]/g, '');
|
|
88
|
+
return hex.length >= length ? hex.slice(0, length) : hex.padStart(length, '0');
|
|
89
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry/types.ts — the observability vocabulary shared by every enterprise
|
|
3
|
+
* subsystem (ingest, retrieval, orchestration, eval, bridges).
|
|
4
|
+
*
|
|
5
|
+
* Enterprise buyers do not ask "is it fast", they ask "show me the span tree for
|
|
6
|
+
* this request, the token count per model, and what it cost". That needs one
|
|
7
|
+
* canonical record, not per-subsystem logging. A {@link SpanData} is that record:
|
|
8
|
+
* a tracing span that ALSO carries LLM-native usage and cost, so a single
|
|
9
|
+
* exporter pipeline feeds both a trace viewer and the metric aggregates.
|
|
10
|
+
*
|
|
11
|
+
* The shape is deliberately OTLP-compatible (trace/span ids, parent linkage,
|
|
12
|
+
* attributes, events, status) so `toOtlpSpan()` is a pure rename rather than a
|
|
13
|
+
* re-model — but nothing here depends on the OpenTelemetry SDK, which keeps the
|
|
14
|
+
* package zero-dependency and browser-safe.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Attribute values are kept primitive so a span serializes to JSON losslessly. */
|
|
18
|
+
export type AttributeValue = string | number | boolean;
|
|
19
|
+
|
|
20
|
+
export type SpanKind =
|
|
21
|
+
| 'llm'
|
|
22
|
+
| 'embedding'
|
|
23
|
+
| 'retrieval'
|
|
24
|
+
| 'ingest'
|
|
25
|
+
| 'agent'
|
|
26
|
+
| 'tool'
|
|
27
|
+
| 'graph'
|
|
28
|
+
| 'internal';
|
|
29
|
+
|
|
30
|
+
export type SpanStatus = 'unset' | 'ok' | 'error';
|
|
31
|
+
|
|
32
|
+
export interface SpanEvent {
|
|
33
|
+
name: string;
|
|
34
|
+
/** Epoch millis. */
|
|
35
|
+
at: number;
|
|
36
|
+
attributes?: Record<string, AttributeValue>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Token accounting for one model call.
|
|
41
|
+
*
|
|
42
|
+
* `cachedInputTokens` and `cacheWriteTokens` are split out because they are
|
|
43
|
+
* billed at different multipliers than plain input (see {@link PriceBook}); a
|
|
44
|
+
* cost model that folds them into `inputTokens` overstates spend by ~10x on a
|
|
45
|
+
* cache-heavy workload, which is exactly the workload enterprises run.
|
|
46
|
+
*/
|
|
47
|
+
export interface LlmUsage {
|
|
48
|
+
model: string;
|
|
49
|
+
inputTokens: number;
|
|
50
|
+
outputTokens: number;
|
|
51
|
+
/** Input tokens served from the provider's prompt cache (billed at a discount). */
|
|
52
|
+
cachedInputTokens?: number;
|
|
53
|
+
/** Input tokens written INTO the provider's prompt cache (billed at a premium). */
|
|
54
|
+
cacheWriteTokens?: number;
|
|
55
|
+
/** True when the whole call was served locally (exact/semantic cache) — cost 0. */
|
|
56
|
+
localCacheHit?: boolean;
|
|
57
|
+
/** True when token counts are inferred rather than reported by the provider. */
|
|
58
|
+
estimated?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A completed (or in-flight) span, ready to export. */
|
|
62
|
+
export interface SpanData {
|
|
63
|
+
traceId: string;
|
|
64
|
+
spanId: string;
|
|
65
|
+
parentSpanId?: string;
|
|
66
|
+
name: string;
|
|
67
|
+
kind: SpanKind;
|
|
68
|
+
/** Epoch millis. */
|
|
69
|
+
startedAt: number;
|
|
70
|
+
/** Epoch millis; absent while the span is still open. */
|
|
71
|
+
endedAt?: number;
|
|
72
|
+
durationMs?: number;
|
|
73
|
+
attributes: Record<string, AttributeValue>;
|
|
74
|
+
events: SpanEvent[];
|
|
75
|
+
status: SpanStatus;
|
|
76
|
+
error?: string;
|
|
77
|
+
/** Present on `llm` spans. */
|
|
78
|
+
usage?: LlmUsage;
|
|
79
|
+
/** Derived from `usage` + the tracer's price book, in USD. */
|
|
80
|
+
costUsd?: number;
|
|
81
|
+
/** Millis from request start to the first streamed token, when streaming. */
|
|
82
|
+
timeToFirstTokenMs?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Sink for finished spans. Implementations must not throw into the caller. */
|
|
86
|
+
export interface SpanExporter {
|
|
87
|
+
export(spans: readonly SpanData[]): void | Promise<void>;
|
|
88
|
+
/** Optional flush hook for buffered/remote exporters. */
|
|
89
|
+
shutdown?(): void | Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A bridge that reports what its last call actually consumed.
|
|
94
|
+
*
|
|
95
|
+
* Optional on {@link TransformerBridge} so third-party bridges stay valid; when
|
|
96
|
+
* present, {@link InstrumentedBridge} prefers these provider-reported numbers
|
|
97
|
+
* over its own character-based estimate.
|
|
98
|
+
*/
|
|
99
|
+
export interface BridgeCallInfo {
|
|
100
|
+
usage?: LlmUsage;
|
|
101
|
+
/** True when the call never reached the provider (exact or semantic cache hit). */
|
|
102
|
+
cacheHit?: boolean;
|
|
103
|
+
/** Which cache tier served it, when `cacheHit`. */
|
|
104
|
+
cacheTier?: string;
|
|
105
|
+
}
|