observeos 0.1.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.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # observeos
2
+
3
+ **Zero-config LLM observability SDK.** Automatically trace your OpenAI, Anthropic, Ollama, and Hugging Face API calls. Calculate costs, measure latency, detect errors.
4
+
5
+ ## Features
6
+
7
+ - Zero-config drop-in wrappers for OpenAI, Anthropic, Ollama, Hugging Face
8
+ - Automatic token cost calculation
9
+ - Batch trace export (non-blocking)
10
+ - Optional OpenTelemetry export
11
+ - PII scrubbing (optional)
12
+ - TypeScript-first (strict mode)
13
+ - Minimal overhead (< 1ms per call)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install observeos
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import OpenAI from 'openai'
25
+ import { createObserveOS } from 'observeos'
26
+
27
+ // Initialize once at app startup
28
+ const obs = createObserveOS({
29
+ apiKey: process.env.OBSERVEOS_API_KEY!,
30
+ baseUrl: process.env.OBSERVEOS_BASE_URL!,
31
+ tenantId: 'my_org',
32
+ })
33
+
34
+ // Wrap your clients — drop-in replacements
35
+ const openai = obs.wrapOpenAI(new OpenAI())
36
+
37
+ // Use exactly as before — tracing is automatic
38
+ const response = await openai.chat.completions.create({
39
+ model: 'gpt-4o',
40
+ messages: [{ role: 'user', content: 'Hello!' }],
41
+ })
42
+ ```
43
+
44
+ Every call is now traced and sent to your observability backend automatically.
45
+
46
+ ## Configuration
47
+
48
+ ```typescript
49
+ createObserveOS({
50
+ apiKey: string, // required: your API key
51
+ baseUrl: string, // optional: worker URL (default: http://localhost:8787)
52
+ tenantId: string, // optional: tenant identifier (default: 'default')
53
+ environment: 'production', // optional: environment name
54
+ enabled: true, // optional: disable tracing without code changes
55
+ sampleRate: 1, // optional: trace 100% of calls (0-1)
56
+ capturePromptPreview: true, // optional: capture first 200 chars of prompt
57
+ captureResponsePreview: true, // optional: capture first 200 chars of response
58
+ piiScrubbing: false, // optional: mask PII before sending
59
+ flushInterval: 5000, // optional: ms between batch flushes
60
+ maxBatchSize: 50, // optional: traces per batch
61
+ otelEndpoint: undefined, // optional: OTEL collector endpoint
62
+ debug: false, // optional: log SDK activity
63
+ })
64
+ ```
65
+
66
+ ## Supported Providers
67
+
68
+ ### OpenAI
69
+
70
+ ```typescript
71
+ import OpenAI from 'openai'
72
+ const openai = obs.wrapOpenAI(new OpenAI())
73
+ ```
74
+
75
+ ### Anthropic
76
+
77
+ ```typescript
78
+ import Anthropic from '@anthropic-ai/sdk'
79
+ const anthropic = obs.wrapAnthropic(new Anthropic())
80
+ ```
81
+
82
+ ### Ollama
83
+
84
+ ```typescript
85
+ const fetchWithTracing = obs.wrapOllama(fetch)
86
+ const response = await fetchWithTracing('http://localhost:11434/api/chat', {
87
+ method: 'POST',
88
+ body: JSON.stringify({ model: 'llama3', messages: [] }),
89
+ })
90
+ ```
91
+
92
+ ### Hugging Face
93
+
94
+ ```typescript
95
+ import { HfInference } from '@huggingface/inference'
96
+ const hf = obs.wrapHuggingFace(new HfInference())
97
+ ```
98
+
99
+ ## Graceful Shutdown
100
+
101
+ Flush pending traces before exiting:
102
+
103
+ ```typescript
104
+ process.on('SIGTERM', async () => {
105
+ await obs.tracer.shutdown()
106
+ process.exit(0)
107
+ })
108
+ ```
109
+
110
+ ## Pricing
111
+
112
+ Cost is calculated automatically using current provider pricing. See `MODEL_PRICING` for details.
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,98 @@
1
+ import * as Anthropic from '@anthropic-ai/sdk';
2
+ import Anthropic__default from '@anthropic-ai/sdk';
3
+ import * as OpenAI from 'openai';
4
+ import OpenAI__default from 'openai';
5
+
6
+ type LLMProvider = 'openai' | 'anthropic' | 'ollama' | 'huggingface';
7
+ type FinishReason = 'stop' | 'length' | 'tool_calls' | 'error' | 'content_filter' | string;
8
+ interface LLMTrace {
9
+ traceId: string;
10
+ spanId: string;
11
+ parentSpanId?: string;
12
+ tenantId: string;
13
+ provider: LLMProvider;
14
+ model: string;
15
+ promptHash: string;
16
+ promptPreview?: string;
17
+ promptTokens?: number;
18
+ completionTokens?: number;
19
+ totalTokens?: number;
20
+ responsePreview?: string;
21
+ finishReason?: FinishReason;
22
+ latencyMs: number;
23
+ ttfbMs?: number;
24
+ costUsd?: number;
25
+ error: boolean;
26
+ errorMessage?: string;
27
+ statusCode?: number;
28
+ tags: Record<string, string>;
29
+ metadata: Record<string, unknown>;
30
+ environment: string;
31
+ createdAt: Date;
32
+ }
33
+ interface ObserveOSConfig {
34
+ apiKey: string;
35
+ baseUrl?: string;
36
+ tenantId?: string;
37
+ environment?: string;
38
+ enabled?: boolean;
39
+ sampleRate?: number;
40
+ capturePromptPreview?: boolean;
41
+ captureResponsePreview?: boolean;
42
+ piiScrubbing?: boolean;
43
+ flushInterval?: number;
44
+ maxBatchSize?: number;
45
+ otelEndpoint?: string;
46
+ debug?: boolean;
47
+ }
48
+ type TraceInput = Omit<LLMTrace, 'traceId' | 'spanId' | 'createdAt'>;
49
+
50
+ declare class ObserveOSTracer {
51
+ readonly config: Required<ObserveOSConfig>;
52
+ private readonly exporter;
53
+ private queue;
54
+ private flushTimer;
55
+ private flushing;
56
+ constructor(config: ObserveOSConfig);
57
+ record(input: TraceInput): Promise<void>;
58
+ flush(): Promise<void>;
59
+ shutdown(): Promise<void>;
60
+ private startFlushTimer;
61
+ }
62
+
63
+ declare class Exporter {
64
+ private config;
65
+ constructor(config: Required<ObserveOSConfig>);
66
+ export(traces: LLMTrace[]): Promise<void>;
67
+ private sendToWorker;
68
+ private sendToOtel;
69
+ }
70
+
71
+ declare function wrapOpenAI<T extends OpenAI__default>(client: T, tracer: ObserveOSTracer): T;
72
+
73
+ declare function wrapAnthropic<T extends Anthropic__default>(client: T, tracer: ObserveOSTracer): T;
74
+
75
+ declare function wrapOllama(fetchFn: typeof fetch, tracer: ObserveOSTracer): typeof fetch;
76
+
77
+ declare function wrapHuggingFace<T extends object>(client: T, tracer: ObserveOSTracer): T;
78
+
79
+ declare function hashPrompt(prompt: string): string;
80
+
81
+ interface ModelPricing {
82
+ input: number;
83
+ output: number;
84
+ }
85
+ declare const MODEL_PRICING: Record<string, ModelPricing>;
86
+ declare function calculateCost(model: string, inputTokens: number, outputTokens: number): number;
87
+ declare function getModelPricing(model: string): ModelPricing | undefined;
88
+
89
+ interface ObserveOSInstance {
90
+ tracer: ObserveOSTracer;
91
+ wrapOpenAI: <T extends OpenAI.default>(client: T) => T;
92
+ wrapAnthropic: <T extends Anthropic.default>(client: T) => T;
93
+ wrapOllama: (fetchFn: typeof fetch) => typeof fetch;
94
+ wrapHuggingFace: <T extends object>(client: T) => T;
95
+ }
96
+ declare function createObserveOS(config: ObserveOSConfig): ObserveOSInstance;
97
+
98
+ export { Exporter, type FinishReason, type LLMProvider, type LLMTrace, MODEL_PRICING, type ObserveOSConfig, type ObserveOSInstance, ObserveOSTracer, type TraceInput, calculateCost, createObserveOS, getModelPricing, hashPrompt, wrapAnthropic, wrapHuggingFace, wrapOllama, wrapOpenAI };
@@ -0,0 +1,98 @@
1
+ import * as Anthropic from '@anthropic-ai/sdk';
2
+ import Anthropic__default from '@anthropic-ai/sdk';
3
+ import * as OpenAI from 'openai';
4
+ import OpenAI__default from 'openai';
5
+
6
+ type LLMProvider = 'openai' | 'anthropic' | 'ollama' | 'huggingface';
7
+ type FinishReason = 'stop' | 'length' | 'tool_calls' | 'error' | 'content_filter' | string;
8
+ interface LLMTrace {
9
+ traceId: string;
10
+ spanId: string;
11
+ parentSpanId?: string;
12
+ tenantId: string;
13
+ provider: LLMProvider;
14
+ model: string;
15
+ promptHash: string;
16
+ promptPreview?: string;
17
+ promptTokens?: number;
18
+ completionTokens?: number;
19
+ totalTokens?: number;
20
+ responsePreview?: string;
21
+ finishReason?: FinishReason;
22
+ latencyMs: number;
23
+ ttfbMs?: number;
24
+ costUsd?: number;
25
+ error: boolean;
26
+ errorMessage?: string;
27
+ statusCode?: number;
28
+ tags: Record<string, string>;
29
+ metadata: Record<string, unknown>;
30
+ environment: string;
31
+ createdAt: Date;
32
+ }
33
+ interface ObserveOSConfig {
34
+ apiKey: string;
35
+ baseUrl?: string;
36
+ tenantId?: string;
37
+ environment?: string;
38
+ enabled?: boolean;
39
+ sampleRate?: number;
40
+ capturePromptPreview?: boolean;
41
+ captureResponsePreview?: boolean;
42
+ piiScrubbing?: boolean;
43
+ flushInterval?: number;
44
+ maxBatchSize?: number;
45
+ otelEndpoint?: string;
46
+ debug?: boolean;
47
+ }
48
+ type TraceInput = Omit<LLMTrace, 'traceId' | 'spanId' | 'createdAt'>;
49
+
50
+ declare class ObserveOSTracer {
51
+ readonly config: Required<ObserveOSConfig>;
52
+ private readonly exporter;
53
+ private queue;
54
+ private flushTimer;
55
+ private flushing;
56
+ constructor(config: ObserveOSConfig);
57
+ record(input: TraceInput): Promise<void>;
58
+ flush(): Promise<void>;
59
+ shutdown(): Promise<void>;
60
+ private startFlushTimer;
61
+ }
62
+
63
+ declare class Exporter {
64
+ private config;
65
+ constructor(config: Required<ObserveOSConfig>);
66
+ export(traces: LLMTrace[]): Promise<void>;
67
+ private sendToWorker;
68
+ private sendToOtel;
69
+ }
70
+
71
+ declare function wrapOpenAI<T extends OpenAI__default>(client: T, tracer: ObserveOSTracer): T;
72
+
73
+ declare function wrapAnthropic<T extends Anthropic__default>(client: T, tracer: ObserveOSTracer): T;
74
+
75
+ declare function wrapOllama(fetchFn: typeof fetch, tracer: ObserveOSTracer): typeof fetch;
76
+
77
+ declare function wrapHuggingFace<T extends object>(client: T, tracer: ObserveOSTracer): T;
78
+
79
+ declare function hashPrompt(prompt: string): string;
80
+
81
+ interface ModelPricing {
82
+ input: number;
83
+ output: number;
84
+ }
85
+ declare const MODEL_PRICING: Record<string, ModelPricing>;
86
+ declare function calculateCost(model: string, inputTokens: number, outputTokens: number): number;
87
+ declare function getModelPricing(model: string): ModelPricing | undefined;
88
+
89
+ interface ObserveOSInstance {
90
+ tracer: ObserveOSTracer;
91
+ wrapOpenAI: <T extends OpenAI.default>(client: T) => T;
92
+ wrapAnthropic: <T extends Anthropic.default>(client: T) => T;
93
+ wrapOllama: (fetchFn: typeof fetch) => typeof fetch;
94
+ wrapHuggingFace: <T extends object>(client: T) => T;
95
+ }
96
+ declare function createObserveOS(config: ObserveOSConfig): ObserveOSInstance;
97
+
98
+ export { Exporter, type FinishReason, type LLMProvider, type LLMTrace, MODEL_PRICING, type ObserveOSConfig, type ObserveOSInstance, ObserveOSTracer, type TraceInput, calculateCost, createObserveOS, getModelPricing, hashPrompt, wrapAnthropic, wrapHuggingFace, wrapOllama, wrapOpenAI };
package/dist/index.js ADDED
@@ -0,0 +1,487 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Exporter: () => Exporter,
24
+ MODEL_PRICING: () => MODEL_PRICING,
25
+ ObserveOSTracer: () => ObserveOSTracer,
26
+ calculateCost: () => calculateCost,
27
+ createObserveOS: () => createObserveOS,
28
+ getModelPricing: () => getModelPricing,
29
+ hashPrompt: () => hashPrompt,
30
+ wrapAnthropic: () => wrapAnthropic,
31
+ wrapHuggingFace: () => wrapHuggingFace,
32
+ wrapOllama: () => wrapOllama,
33
+ wrapOpenAI: () => wrapOpenAI
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/core/tracer.ts
38
+ var import_crypto = require("crypto");
39
+
40
+ // src/core/exporter.ts
41
+ var Exporter = class {
42
+ config;
43
+ constructor(config) {
44
+ this.config = config;
45
+ }
46
+ async export(traces) {
47
+ await this.sendToWorker(traces);
48
+ if (this.config.otelEndpoint) {
49
+ await this.sendToOtel(traces).catch((err) => {
50
+ if (this.config.debug) console.error("[ObserveOS] OTEL export failed:", err.message);
51
+ });
52
+ }
53
+ }
54
+ async sendToWorker(traces) {
55
+ const payload = traces.map((t) => ({
56
+ ...t,
57
+ createdAt: t.createdAt.toISOString()
58
+ }));
59
+ const res = await fetch(`${this.config.baseUrl}/v1/traces`, {
60
+ method: "POST",
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ "Authorization": `Bearer ${this.config.apiKey}`,
64
+ "X-Tenant-Id": this.config.tenantId
65
+ },
66
+ body: JSON.stringify(payload)
67
+ });
68
+ if (!res.ok) {
69
+ const text = await res.text().catch(() => "unknown error");
70
+ if (this.config.debug) {
71
+ console.error(`[ObserveOS] Ingestion failed: ${res.status} ${text}`);
72
+ }
73
+ } else if (this.config.debug) {
74
+ console.log(`[ObserveOS] Flushed ${traces.length} traces`);
75
+ }
76
+ }
77
+ async sendToOtel(traces) {
78
+ const spans = traces.map((t) => ({
79
+ traceId: t.traceId,
80
+ spanId: t.spanId,
81
+ parentSpanId: t.parentSpanId,
82
+ name: `${t.provider}.chat`,
83
+ kind: 3,
84
+ // CLIENT
85
+ startTimeUnixNano: (t.createdAt.getTime() - t.latencyMs) * 1e6,
86
+ endTimeUnixNano: t.createdAt.getTime() * 1e6,
87
+ attributes: [
88
+ { key: "gen_ai.system", value: { stringValue: t.provider } },
89
+ { key: "gen_ai.request.model", value: { stringValue: t.model } },
90
+ { key: "gen_ai.usage.input_tokens", value: { intValue: t.promptTokens ?? 0 } },
91
+ { key: "gen_ai.usage.output_tokens", value: { intValue: t.completionTokens ?? 0 } },
92
+ { key: "observeos.cost_usd", value: { doubleValue: t.costUsd ?? 0 } },
93
+ { key: "observeos.tenant_id", value: { stringValue: t.tenantId } },
94
+ { key: "observeos.prompt_hash", value: { stringValue: t.promptHash } },
95
+ { key: "error", value: { boolValue: t.error } }
96
+ ],
97
+ status: t.error ? { code: 2 } : { code: 1 }
98
+ }));
99
+ await fetch(`${this.config.otelEndpoint}/v1/traces`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ resourceSpans: [{ scopeSpans: [{ spans }] }] })
103
+ });
104
+ }
105
+ };
106
+
107
+ // src/core/tracer.ts
108
+ var DEFAULTS = {
109
+ apiKey: "",
110
+ baseUrl: "http://localhost:8787",
111
+ tenantId: "default",
112
+ environment: "production",
113
+ enabled: true,
114
+ sampleRate: 1,
115
+ capturePromptPreview: true,
116
+ captureResponsePreview: true,
117
+ piiScrubbing: false,
118
+ flushInterval: 5e3,
119
+ maxBatchSize: 50,
120
+ otelEndpoint: "",
121
+ debug: false
122
+ };
123
+ var ObserveOSTracer = class {
124
+ config;
125
+ exporter;
126
+ queue = [];
127
+ flushTimer = null;
128
+ flushing = false;
129
+ constructor(config) {
130
+ this.config = { ...DEFAULTS, ...config };
131
+ this.exporter = new Exporter(this.config);
132
+ this.startFlushTimer();
133
+ }
134
+ async record(input) {
135
+ if (!this.config.enabled) return;
136
+ if (Math.random() > this.config.sampleRate) return;
137
+ const trace = {
138
+ ...input,
139
+ traceId: (0, import_crypto.randomUUID)().replace(/-/g, ""),
140
+ spanId: (0, import_crypto.randomUUID)().replace(/-/g, "").slice(0, 16),
141
+ createdAt: /* @__PURE__ */ new Date()
142
+ };
143
+ this.queue.push(trace);
144
+ if (this.config.debug) {
145
+ console.log(`[ObserveOS] Queued trace: ${trace.provider}/${trace.model} (${input.latencyMs}ms)`);
146
+ }
147
+ if (this.queue.length >= this.config.maxBatchSize) {
148
+ this.flush().catch((err) => {
149
+ if (this.config.debug) console.error("[ObserveOS] Flush error:", err.message);
150
+ });
151
+ }
152
+ }
153
+ async flush() {
154
+ if (this.flushing || this.queue.length === 0) return;
155
+ this.flushing = true;
156
+ const batch = this.queue.splice(0);
157
+ try {
158
+ await this.exporter.export(batch);
159
+ } finally {
160
+ this.flushing = false;
161
+ }
162
+ }
163
+ async shutdown() {
164
+ if (this.flushTimer) {
165
+ clearInterval(this.flushTimer);
166
+ this.flushTimer = null;
167
+ }
168
+ await this.flush();
169
+ }
170
+ startFlushTimer() {
171
+ this.flushTimer = setInterval(() => {
172
+ this.flush().catch(() => {
173
+ });
174
+ }, this.config.flushInterval);
175
+ if (this.flushTimer.unref) this.flushTimer.unref();
176
+ }
177
+ };
178
+
179
+ // src/utils/hash.ts
180
+ var import_crypto2 = require("crypto");
181
+ function hashPrompt(prompt) {
182
+ const normalized = prompt.trim().toLowerCase().replace(/\s+/g, " ");
183
+ return (0, import_crypto2.createHash)("sha256").update(normalized, "utf8").digest("hex");
184
+ }
185
+
186
+ // src/utils/cost.ts
187
+ var MODEL_PRICING = {
188
+ // OpenAI
189
+ "gpt-4o": { input: 2.5, output: 10 },
190
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
191
+ "gpt-4o-2024-11-20": { input: 2.5, output: 10 },
192
+ "gpt-4-turbo": { input: 10, output: 30 },
193
+ "gpt-4-turbo-preview": { input: 10, output: 30 },
194
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
195
+ "gpt-3.5-turbo-0125": { input: 0.5, output: 1.5 },
196
+ "o1": { input: 15, output: 60 },
197
+ "o1-mini": { input: 3, output: 12 },
198
+ "o3-mini": { input: 1.1, output: 4.4 },
199
+ // Anthropic
200
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
201
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
202
+ "claude-3-opus-20240229": { input: 15, output: 75 },
203
+ "claude-3-sonnet-20240229": { input: 3, output: 15 },
204
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
205
+ // Ollama (self-hosted — always $0)
206
+ "llama3": { input: 0, output: 0 },
207
+ "llama3.1": { input: 0, output: 0 },
208
+ "llama3.2": { input: 0, output: 0 },
209
+ "mistral": { input: 0, output: 0 },
210
+ "mixtral": { input: 0, output: 0 },
211
+ "phi3": { input: 0, output: 0 },
212
+ "gemma2": { input: 0, output: 0 },
213
+ "qwen2": { input: 0, output: 0 }
214
+ };
215
+ function calculateCost(model, inputTokens, outputTokens) {
216
+ const pricing = MODEL_PRICING[model];
217
+ if (!pricing) return 0;
218
+ const inputCost = inputTokens / 1e6 * pricing.input;
219
+ const outputCost = outputTokens / 1e6 * pricing.output;
220
+ return parseFloat((inputCost + outputCost).toFixed(8));
221
+ }
222
+ function getModelPricing(model) {
223
+ return MODEL_PRICING[model];
224
+ }
225
+
226
+ // src/utils/sanitize.ts
227
+ var EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
228
+ var PHONE_REGEX = /(\+?1?\s?)?(\(?\d{3}\)?[\s.-]?)?\d{3}[\s.-]?\d{4}/g;
229
+ var SSN_REGEX = /\b\d{3}-\d{2}-\d{4}\b/g;
230
+ var CREDIT_REGEX = /\b(?:\d[ -]?){13,16}\b/g;
231
+ var IP_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
232
+ function scrubPII(text) {
233
+ return text.replace(EMAIL_REGEX, "[EMAIL]").replace(PHONE_REGEX, "[PHONE]").replace(SSN_REGEX, "[SSN]").replace(CREDIT_REGEX, "[CARD]").replace(IP_REGEX, "[IP]");
234
+ }
235
+
236
+ // src/providers/openai.ts
237
+ function wrapOpenAI(client, tracer) {
238
+ const original = client.chat.completions.create.bind(client.chat.completions);
239
+ client.chat.completions.create = async function(params, options) {
240
+ if (params.stream) {
241
+ return original(params, options);
242
+ }
243
+ const startMs = Date.now();
244
+ const rawPrompt = (params.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
245
+ const prompt = tracer.config.piiScrubbing ? scrubPII(rawPrompt) : rawPrompt;
246
+ try {
247
+ const response = await original(params, options);
248
+ const latencyMs = Date.now() - startMs;
249
+ const res = response;
250
+ const usage = res.usage;
251
+ const promptTokens = usage?.prompt_tokens;
252
+ const completionTokens = usage?.completion_tokens;
253
+ const totalTokens = usage?.total_tokens;
254
+ const costUsd = promptTokens != null && completionTokens != null ? calculateCost(params.model, promptTokens, completionTokens) : void 0;
255
+ let responseText = res.choices?.[0]?.message?.content ?? "";
256
+ if (tracer.config.piiScrubbing) responseText = scrubPII(responseText);
257
+ await tracer.record({
258
+ tenantId: tracer.config.tenantId,
259
+ provider: "openai",
260
+ model: params.model,
261
+ promptHash: hashPrompt(rawPrompt),
262
+ promptPreview: tracer.config.capturePromptPreview ? prompt.slice(0, 200) : void 0,
263
+ promptTokens,
264
+ completionTokens,
265
+ totalTokens,
266
+ responsePreview: tracer.config.captureResponsePreview ? responseText.slice(0, 200) : void 0,
267
+ finishReason: res.choices?.[0]?.finish_reason,
268
+ latencyMs,
269
+ costUsd,
270
+ error: false,
271
+ tags: {},
272
+ metadata: {},
273
+ environment: tracer.config.environment
274
+ });
275
+ return response;
276
+ } catch (err) {
277
+ await tracer.record({
278
+ tenantId: tracer.config.tenantId,
279
+ provider: "openai",
280
+ model: params.model,
281
+ promptHash: hashPrompt(rawPrompt),
282
+ latencyMs: Date.now() - startMs,
283
+ error: true,
284
+ errorMessage: err.message,
285
+ statusCode: err.status,
286
+ tags: {},
287
+ metadata: {},
288
+ environment: tracer.config.environment
289
+ });
290
+ throw err;
291
+ }
292
+ };
293
+ return client;
294
+ }
295
+
296
+ // src/providers/anthropic.ts
297
+ function wrapAnthropic(client, tracer) {
298
+ const original = client.messages.create.bind(client.messages);
299
+ client.messages.create = async function(params, options) {
300
+ if (params.stream) return original(params, options);
301
+ const startMs = Date.now();
302
+ const rawPrompt = (params.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
303
+ const prompt = tracer.config.piiScrubbing ? scrubPII(rawPrompt) : rawPrompt;
304
+ try {
305
+ const response = await original(params, options);
306
+ const latencyMs = Date.now() - startMs;
307
+ const res = response;
308
+ const usage = res.usage;
309
+ const promptTokens = usage?.input_tokens;
310
+ const completionTokens = usage?.output_tokens;
311
+ const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
312
+ const costUsd = promptTokens != null && completionTokens != null ? calculateCost(params.model, promptTokens, completionTokens) : void 0;
313
+ const rawResponse = res.content?.filter((b) => b.type === "text").map((b) => b.text).join("") ?? "";
314
+ const responseText = tracer.config.piiScrubbing ? scrubPII(rawResponse) : rawResponse;
315
+ await tracer.record({
316
+ tenantId: tracer.config.tenantId,
317
+ provider: "anthropic",
318
+ model: params.model,
319
+ promptHash: hashPrompt(rawPrompt),
320
+ promptPreview: tracer.config.capturePromptPreview ? prompt.slice(0, 200) : void 0,
321
+ promptTokens,
322
+ completionTokens,
323
+ totalTokens,
324
+ responsePreview: tracer.config.captureResponsePreview ? responseText.slice(0, 200) : void 0,
325
+ finishReason: res.stop_reason,
326
+ latencyMs,
327
+ costUsd,
328
+ error: false,
329
+ tags: {},
330
+ metadata: {},
331
+ environment: tracer.config.environment
332
+ });
333
+ return response;
334
+ } catch (err) {
335
+ await tracer.record({
336
+ tenantId: tracer.config.tenantId,
337
+ provider: "anthropic",
338
+ model: params.model,
339
+ promptHash: hashPrompt(rawPrompt),
340
+ latencyMs: Date.now() - startMs,
341
+ error: true,
342
+ errorMessage: err.message,
343
+ statusCode: err.status,
344
+ tags: {},
345
+ metadata: {},
346
+ environment: tracer.config.environment
347
+ });
348
+ throw err;
349
+ }
350
+ };
351
+ return client;
352
+ }
353
+
354
+ // src/providers/ollama.ts
355
+ function wrapOllama(fetchFn, tracer) {
356
+ return async function wrappedFetch(input, init) {
357
+ const url = input.toString();
358
+ const isOllamaChat = url.includes("/api/chat") || url.includes("/api/generate");
359
+ if (!isOllamaChat) return fetchFn(input, init);
360
+ const startMs = Date.now();
361
+ let body = {};
362
+ try {
363
+ body = JSON.parse(init?.body ?? "{}");
364
+ } catch {
365
+ }
366
+ const rawPrompt = (body.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n") || body.prompt || "";
367
+ try {
368
+ const response = await fetchFn(input, init);
369
+ const latencyMs = Date.now() - startMs;
370
+ const cloned = response.clone();
371
+ let data = {};
372
+ try {
373
+ data = await cloned.json();
374
+ } catch {
375
+ }
376
+ const promptTokens = data.prompt_eval_count;
377
+ const completionTokens = data.eval_count;
378
+ const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
379
+ await tracer.record({
380
+ tenantId: tracer.config.tenantId,
381
+ provider: "ollama",
382
+ model: body.model ?? "unknown",
383
+ promptHash: hashPrompt(rawPrompt),
384
+ promptPreview: tracer.config.capturePromptPreview ? rawPrompt.slice(0, 200) : void 0,
385
+ promptTokens,
386
+ completionTokens,
387
+ totalTokens,
388
+ responsePreview: tracer.config.captureResponsePreview ? (data.message?.content ?? data.response ?? "").slice(0, 200) : void 0,
389
+ finishReason: data.done ? "stop" : void 0,
390
+ latencyMs,
391
+ costUsd: 0,
392
+ error: false,
393
+ tags: {},
394
+ metadata: {},
395
+ environment: tracer.config.environment
396
+ });
397
+ return response;
398
+ } catch (err) {
399
+ await tracer.record({
400
+ tenantId: tracer.config.tenantId,
401
+ provider: "ollama",
402
+ model: body.model ?? "unknown",
403
+ promptHash: hashPrompt(rawPrompt),
404
+ latencyMs: Date.now() - startMs,
405
+ error: true,
406
+ errorMessage: err.message,
407
+ tags: {},
408
+ metadata: {},
409
+ environment: tracer.config.environment
410
+ });
411
+ throw err;
412
+ }
413
+ };
414
+ }
415
+
416
+ // src/providers/huggingface.ts
417
+ function wrapHuggingFace(client, tracer) {
418
+ const methods = ["textGeneration", "chatCompletion", "textClassification", "summarization"];
419
+ for (const method of methods) {
420
+ const original = client[method]?.bind(client);
421
+ if (!original) continue;
422
+ client[method] = async function(params) {
423
+ const startMs = Date.now();
424
+ const rawPrompt = params.inputs ?? params.messages?.map((m) => m.content).join("\n") ?? "";
425
+ try {
426
+ const response = await original(params);
427
+ const latencyMs = Date.now() - startMs;
428
+ const responseText = response?.generated_text ?? response?.choices?.[0]?.message?.content ?? (Array.isArray(response) ? response[0]?.generated_text : "") ?? "";
429
+ await tracer.record({
430
+ tenantId: tracer.config.tenantId,
431
+ provider: "huggingface",
432
+ model: params.model ?? "unknown",
433
+ promptHash: hashPrompt(rawPrompt),
434
+ promptPreview: tracer.config.capturePromptPreview ? rawPrompt.slice(0, 200) : void 0,
435
+ responsePreview: tracer.config.captureResponsePreview ? String(responseText).slice(0, 200) : void 0,
436
+ latencyMs,
437
+ error: false,
438
+ tags: { hf_method: method },
439
+ metadata: {},
440
+ environment: tracer.config.environment
441
+ });
442
+ return response;
443
+ } catch (err) {
444
+ await tracer.record({
445
+ tenantId: tracer.config.tenantId,
446
+ provider: "huggingface",
447
+ model: params.model ?? "unknown",
448
+ promptHash: hashPrompt(rawPrompt),
449
+ latencyMs: Date.now() - startMs,
450
+ error: true,
451
+ errorMessage: err.message,
452
+ tags: { hf_method: method },
453
+ metadata: {},
454
+ environment: tracer.config.environment
455
+ });
456
+ throw err;
457
+ }
458
+ };
459
+ }
460
+ return client;
461
+ }
462
+
463
+ // src/index.ts
464
+ function createObserveOS(config) {
465
+ const tracer = new ObserveOSTracer(config);
466
+ return {
467
+ tracer,
468
+ wrapOpenAI: (client) => wrapOpenAI(client, tracer),
469
+ wrapAnthropic: (client) => wrapAnthropic(client, tracer),
470
+ wrapOllama: (fetchFn) => wrapOllama(fetchFn, tracer),
471
+ wrapHuggingFace: (client) => wrapHuggingFace(client, tracer)
472
+ };
473
+ }
474
+ // Annotate the CommonJS export names for ESM import in node:
475
+ 0 && (module.exports = {
476
+ Exporter,
477
+ MODEL_PRICING,
478
+ ObserveOSTracer,
479
+ calculateCost,
480
+ createObserveOS,
481
+ getModelPricing,
482
+ hashPrompt,
483
+ wrapAnthropic,
484
+ wrapHuggingFace,
485
+ wrapOllama,
486
+ wrapOpenAI
487
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,450 @@
1
+ // src/core/tracer.ts
2
+ import { randomUUID } from "crypto";
3
+
4
+ // src/core/exporter.ts
5
+ var Exporter = class {
6
+ config;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ async export(traces) {
11
+ await this.sendToWorker(traces);
12
+ if (this.config.otelEndpoint) {
13
+ await this.sendToOtel(traces).catch((err) => {
14
+ if (this.config.debug) console.error("[ObserveOS] OTEL export failed:", err.message);
15
+ });
16
+ }
17
+ }
18
+ async sendToWorker(traces) {
19
+ const payload = traces.map((t) => ({
20
+ ...t,
21
+ createdAt: t.createdAt.toISOString()
22
+ }));
23
+ const res = await fetch(`${this.config.baseUrl}/v1/traces`, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ "Authorization": `Bearer ${this.config.apiKey}`,
28
+ "X-Tenant-Id": this.config.tenantId
29
+ },
30
+ body: JSON.stringify(payload)
31
+ });
32
+ if (!res.ok) {
33
+ const text = await res.text().catch(() => "unknown error");
34
+ if (this.config.debug) {
35
+ console.error(`[ObserveOS] Ingestion failed: ${res.status} ${text}`);
36
+ }
37
+ } else if (this.config.debug) {
38
+ console.log(`[ObserveOS] Flushed ${traces.length} traces`);
39
+ }
40
+ }
41
+ async sendToOtel(traces) {
42
+ const spans = traces.map((t) => ({
43
+ traceId: t.traceId,
44
+ spanId: t.spanId,
45
+ parentSpanId: t.parentSpanId,
46
+ name: `${t.provider}.chat`,
47
+ kind: 3,
48
+ // CLIENT
49
+ startTimeUnixNano: (t.createdAt.getTime() - t.latencyMs) * 1e6,
50
+ endTimeUnixNano: t.createdAt.getTime() * 1e6,
51
+ attributes: [
52
+ { key: "gen_ai.system", value: { stringValue: t.provider } },
53
+ { key: "gen_ai.request.model", value: { stringValue: t.model } },
54
+ { key: "gen_ai.usage.input_tokens", value: { intValue: t.promptTokens ?? 0 } },
55
+ { key: "gen_ai.usage.output_tokens", value: { intValue: t.completionTokens ?? 0 } },
56
+ { key: "observeos.cost_usd", value: { doubleValue: t.costUsd ?? 0 } },
57
+ { key: "observeos.tenant_id", value: { stringValue: t.tenantId } },
58
+ { key: "observeos.prompt_hash", value: { stringValue: t.promptHash } },
59
+ { key: "error", value: { boolValue: t.error } }
60
+ ],
61
+ status: t.error ? { code: 2 } : { code: 1 }
62
+ }));
63
+ await fetch(`${this.config.otelEndpoint}/v1/traces`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({ resourceSpans: [{ scopeSpans: [{ spans }] }] })
67
+ });
68
+ }
69
+ };
70
+
71
+ // src/core/tracer.ts
72
+ var DEFAULTS = {
73
+ apiKey: "",
74
+ baseUrl: "http://localhost:8787",
75
+ tenantId: "default",
76
+ environment: "production",
77
+ enabled: true,
78
+ sampleRate: 1,
79
+ capturePromptPreview: true,
80
+ captureResponsePreview: true,
81
+ piiScrubbing: false,
82
+ flushInterval: 5e3,
83
+ maxBatchSize: 50,
84
+ otelEndpoint: "",
85
+ debug: false
86
+ };
87
+ var ObserveOSTracer = class {
88
+ config;
89
+ exporter;
90
+ queue = [];
91
+ flushTimer = null;
92
+ flushing = false;
93
+ constructor(config) {
94
+ this.config = { ...DEFAULTS, ...config };
95
+ this.exporter = new Exporter(this.config);
96
+ this.startFlushTimer();
97
+ }
98
+ async record(input) {
99
+ if (!this.config.enabled) return;
100
+ if (Math.random() > this.config.sampleRate) return;
101
+ const trace = {
102
+ ...input,
103
+ traceId: randomUUID().replace(/-/g, ""),
104
+ spanId: randomUUID().replace(/-/g, "").slice(0, 16),
105
+ createdAt: /* @__PURE__ */ new Date()
106
+ };
107
+ this.queue.push(trace);
108
+ if (this.config.debug) {
109
+ console.log(`[ObserveOS] Queued trace: ${trace.provider}/${trace.model} (${input.latencyMs}ms)`);
110
+ }
111
+ if (this.queue.length >= this.config.maxBatchSize) {
112
+ this.flush().catch((err) => {
113
+ if (this.config.debug) console.error("[ObserveOS] Flush error:", err.message);
114
+ });
115
+ }
116
+ }
117
+ async flush() {
118
+ if (this.flushing || this.queue.length === 0) return;
119
+ this.flushing = true;
120
+ const batch = this.queue.splice(0);
121
+ try {
122
+ await this.exporter.export(batch);
123
+ } finally {
124
+ this.flushing = false;
125
+ }
126
+ }
127
+ async shutdown() {
128
+ if (this.flushTimer) {
129
+ clearInterval(this.flushTimer);
130
+ this.flushTimer = null;
131
+ }
132
+ await this.flush();
133
+ }
134
+ startFlushTimer() {
135
+ this.flushTimer = setInterval(() => {
136
+ this.flush().catch(() => {
137
+ });
138
+ }, this.config.flushInterval);
139
+ if (this.flushTimer.unref) this.flushTimer.unref();
140
+ }
141
+ };
142
+
143
+ // src/utils/hash.ts
144
+ import { createHash } from "crypto";
145
+ function hashPrompt(prompt) {
146
+ const normalized = prompt.trim().toLowerCase().replace(/\s+/g, " ");
147
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
148
+ }
149
+
150
+ // src/utils/cost.ts
151
+ var MODEL_PRICING = {
152
+ // OpenAI
153
+ "gpt-4o": { input: 2.5, output: 10 },
154
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
155
+ "gpt-4o-2024-11-20": { input: 2.5, output: 10 },
156
+ "gpt-4-turbo": { input: 10, output: 30 },
157
+ "gpt-4-turbo-preview": { input: 10, output: 30 },
158
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
159
+ "gpt-3.5-turbo-0125": { input: 0.5, output: 1.5 },
160
+ "o1": { input: 15, output: 60 },
161
+ "o1-mini": { input: 3, output: 12 },
162
+ "o3-mini": { input: 1.1, output: 4.4 },
163
+ // Anthropic
164
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
165
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
166
+ "claude-3-opus-20240229": { input: 15, output: 75 },
167
+ "claude-3-sonnet-20240229": { input: 3, output: 15 },
168
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
169
+ // Ollama (self-hosted — always $0)
170
+ "llama3": { input: 0, output: 0 },
171
+ "llama3.1": { input: 0, output: 0 },
172
+ "llama3.2": { input: 0, output: 0 },
173
+ "mistral": { input: 0, output: 0 },
174
+ "mixtral": { input: 0, output: 0 },
175
+ "phi3": { input: 0, output: 0 },
176
+ "gemma2": { input: 0, output: 0 },
177
+ "qwen2": { input: 0, output: 0 }
178
+ };
179
+ function calculateCost(model, inputTokens, outputTokens) {
180
+ const pricing = MODEL_PRICING[model];
181
+ if (!pricing) return 0;
182
+ const inputCost = inputTokens / 1e6 * pricing.input;
183
+ const outputCost = outputTokens / 1e6 * pricing.output;
184
+ return parseFloat((inputCost + outputCost).toFixed(8));
185
+ }
186
+ function getModelPricing(model) {
187
+ return MODEL_PRICING[model];
188
+ }
189
+
190
+ // src/utils/sanitize.ts
191
+ var EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
192
+ var PHONE_REGEX = /(\+?1?\s?)?(\(?\d{3}\)?[\s.-]?)?\d{3}[\s.-]?\d{4}/g;
193
+ var SSN_REGEX = /\b\d{3}-\d{2}-\d{4}\b/g;
194
+ var CREDIT_REGEX = /\b(?:\d[ -]?){13,16}\b/g;
195
+ var IP_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
196
+ function scrubPII(text) {
197
+ return text.replace(EMAIL_REGEX, "[EMAIL]").replace(PHONE_REGEX, "[PHONE]").replace(SSN_REGEX, "[SSN]").replace(CREDIT_REGEX, "[CARD]").replace(IP_REGEX, "[IP]");
198
+ }
199
+
200
+ // src/providers/openai.ts
201
+ function wrapOpenAI(client, tracer) {
202
+ const original = client.chat.completions.create.bind(client.chat.completions);
203
+ client.chat.completions.create = async function(params, options) {
204
+ if (params.stream) {
205
+ return original(params, options);
206
+ }
207
+ const startMs = Date.now();
208
+ const rawPrompt = (params.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
209
+ const prompt = tracer.config.piiScrubbing ? scrubPII(rawPrompt) : rawPrompt;
210
+ try {
211
+ const response = await original(params, options);
212
+ const latencyMs = Date.now() - startMs;
213
+ const res = response;
214
+ const usage = res.usage;
215
+ const promptTokens = usage?.prompt_tokens;
216
+ const completionTokens = usage?.completion_tokens;
217
+ const totalTokens = usage?.total_tokens;
218
+ const costUsd = promptTokens != null && completionTokens != null ? calculateCost(params.model, promptTokens, completionTokens) : void 0;
219
+ let responseText = res.choices?.[0]?.message?.content ?? "";
220
+ if (tracer.config.piiScrubbing) responseText = scrubPII(responseText);
221
+ await tracer.record({
222
+ tenantId: tracer.config.tenantId,
223
+ provider: "openai",
224
+ model: params.model,
225
+ promptHash: hashPrompt(rawPrompt),
226
+ promptPreview: tracer.config.capturePromptPreview ? prompt.slice(0, 200) : void 0,
227
+ promptTokens,
228
+ completionTokens,
229
+ totalTokens,
230
+ responsePreview: tracer.config.captureResponsePreview ? responseText.slice(0, 200) : void 0,
231
+ finishReason: res.choices?.[0]?.finish_reason,
232
+ latencyMs,
233
+ costUsd,
234
+ error: false,
235
+ tags: {},
236
+ metadata: {},
237
+ environment: tracer.config.environment
238
+ });
239
+ return response;
240
+ } catch (err) {
241
+ await tracer.record({
242
+ tenantId: tracer.config.tenantId,
243
+ provider: "openai",
244
+ model: params.model,
245
+ promptHash: hashPrompt(rawPrompt),
246
+ latencyMs: Date.now() - startMs,
247
+ error: true,
248
+ errorMessage: err.message,
249
+ statusCode: err.status,
250
+ tags: {},
251
+ metadata: {},
252
+ environment: tracer.config.environment
253
+ });
254
+ throw err;
255
+ }
256
+ };
257
+ return client;
258
+ }
259
+
260
+ // src/providers/anthropic.ts
261
+ function wrapAnthropic(client, tracer) {
262
+ const original = client.messages.create.bind(client.messages);
263
+ client.messages.create = async function(params, options) {
264
+ if (params.stream) return original(params, options);
265
+ const startMs = Date.now();
266
+ const rawPrompt = (params.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
267
+ const prompt = tracer.config.piiScrubbing ? scrubPII(rawPrompt) : rawPrompt;
268
+ try {
269
+ const response = await original(params, options);
270
+ const latencyMs = Date.now() - startMs;
271
+ const res = response;
272
+ const usage = res.usage;
273
+ const promptTokens = usage?.input_tokens;
274
+ const completionTokens = usage?.output_tokens;
275
+ const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
276
+ const costUsd = promptTokens != null && completionTokens != null ? calculateCost(params.model, promptTokens, completionTokens) : void 0;
277
+ const rawResponse = res.content?.filter((b) => b.type === "text").map((b) => b.text).join("") ?? "";
278
+ const responseText = tracer.config.piiScrubbing ? scrubPII(rawResponse) : rawResponse;
279
+ await tracer.record({
280
+ tenantId: tracer.config.tenantId,
281
+ provider: "anthropic",
282
+ model: params.model,
283
+ promptHash: hashPrompt(rawPrompt),
284
+ promptPreview: tracer.config.capturePromptPreview ? prompt.slice(0, 200) : void 0,
285
+ promptTokens,
286
+ completionTokens,
287
+ totalTokens,
288
+ responsePreview: tracer.config.captureResponsePreview ? responseText.slice(0, 200) : void 0,
289
+ finishReason: res.stop_reason,
290
+ latencyMs,
291
+ costUsd,
292
+ error: false,
293
+ tags: {},
294
+ metadata: {},
295
+ environment: tracer.config.environment
296
+ });
297
+ return response;
298
+ } catch (err) {
299
+ await tracer.record({
300
+ tenantId: tracer.config.tenantId,
301
+ provider: "anthropic",
302
+ model: params.model,
303
+ promptHash: hashPrompt(rawPrompt),
304
+ latencyMs: Date.now() - startMs,
305
+ error: true,
306
+ errorMessage: err.message,
307
+ statusCode: err.status,
308
+ tags: {},
309
+ metadata: {},
310
+ environment: tracer.config.environment
311
+ });
312
+ throw err;
313
+ }
314
+ };
315
+ return client;
316
+ }
317
+
318
+ // src/providers/ollama.ts
319
+ function wrapOllama(fetchFn, tracer) {
320
+ return async function wrappedFetch(input, init) {
321
+ const url = input.toString();
322
+ const isOllamaChat = url.includes("/api/chat") || url.includes("/api/generate");
323
+ if (!isOllamaChat) return fetchFn(input, init);
324
+ const startMs = Date.now();
325
+ let body = {};
326
+ try {
327
+ body = JSON.parse(init?.body ?? "{}");
328
+ } catch {
329
+ }
330
+ const rawPrompt = (body.messages ?? []).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n") || body.prompt || "";
331
+ try {
332
+ const response = await fetchFn(input, init);
333
+ const latencyMs = Date.now() - startMs;
334
+ const cloned = response.clone();
335
+ let data = {};
336
+ try {
337
+ data = await cloned.json();
338
+ } catch {
339
+ }
340
+ const promptTokens = data.prompt_eval_count;
341
+ const completionTokens = data.eval_count;
342
+ const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
343
+ await tracer.record({
344
+ tenantId: tracer.config.tenantId,
345
+ provider: "ollama",
346
+ model: body.model ?? "unknown",
347
+ promptHash: hashPrompt(rawPrompt),
348
+ promptPreview: tracer.config.capturePromptPreview ? rawPrompt.slice(0, 200) : void 0,
349
+ promptTokens,
350
+ completionTokens,
351
+ totalTokens,
352
+ responsePreview: tracer.config.captureResponsePreview ? (data.message?.content ?? data.response ?? "").slice(0, 200) : void 0,
353
+ finishReason: data.done ? "stop" : void 0,
354
+ latencyMs,
355
+ costUsd: 0,
356
+ error: false,
357
+ tags: {},
358
+ metadata: {},
359
+ environment: tracer.config.environment
360
+ });
361
+ return response;
362
+ } catch (err) {
363
+ await tracer.record({
364
+ tenantId: tracer.config.tenantId,
365
+ provider: "ollama",
366
+ model: body.model ?? "unknown",
367
+ promptHash: hashPrompt(rawPrompt),
368
+ latencyMs: Date.now() - startMs,
369
+ error: true,
370
+ errorMessage: err.message,
371
+ tags: {},
372
+ metadata: {},
373
+ environment: tracer.config.environment
374
+ });
375
+ throw err;
376
+ }
377
+ };
378
+ }
379
+
380
+ // src/providers/huggingface.ts
381
+ function wrapHuggingFace(client, tracer) {
382
+ const methods = ["textGeneration", "chatCompletion", "textClassification", "summarization"];
383
+ for (const method of methods) {
384
+ const original = client[method]?.bind(client);
385
+ if (!original) continue;
386
+ client[method] = async function(params) {
387
+ const startMs = Date.now();
388
+ const rawPrompt = params.inputs ?? params.messages?.map((m) => m.content).join("\n") ?? "";
389
+ try {
390
+ const response = await original(params);
391
+ const latencyMs = Date.now() - startMs;
392
+ const responseText = response?.generated_text ?? response?.choices?.[0]?.message?.content ?? (Array.isArray(response) ? response[0]?.generated_text : "") ?? "";
393
+ await tracer.record({
394
+ tenantId: tracer.config.tenantId,
395
+ provider: "huggingface",
396
+ model: params.model ?? "unknown",
397
+ promptHash: hashPrompt(rawPrompt),
398
+ promptPreview: tracer.config.capturePromptPreview ? rawPrompt.slice(0, 200) : void 0,
399
+ responsePreview: tracer.config.captureResponsePreview ? String(responseText).slice(0, 200) : void 0,
400
+ latencyMs,
401
+ error: false,
402
+ tags: { hf_method: method },
403
+ metadata: {},
404
+ environment: tracer.config.environment
405
+ });
406
+ return response;
407
+ } catch (err) {
408
+ await tracer.record({
409
+ tenantId: tracer.config.tenantId,
410
+ provider: "huggingface",
411
+ model: params.model ?? "unknown",
412
+ promptHash: hashPrompt(rawPrompt),
413
+ latencyMs: Date.now() - startMs,
414
+ error: true,
415
+ errorMessage: err.message,
416
+ tags: { hf_method: method },
417
+ metadata: {},
418
+ environment: tracer.config.environment
419
+ });
420
+ throw err;
421
+ }
422
+ };
423
+ }
424
+ return client;
425
+ }
426
+
427
+ // src/index.ts
428
+ function createObserveOS(config) {
429
+ const tracer = new ObserveOSTracer(config);
430
+ return {
431
+ tracer,
432
+ wrapOpenAI: (client) => wrapOpenAI(client, tracer),
433
+ wrapAnthropic: (client) => wrapAnthropic(client, tracer),
434
+ wrapOllama: (fetchFn) => wrapOllama(fetchFn, tracer),
435
+ wrapHuggingFace: (client) => wrapHuggingFace(client, tracer)
436
+ };
437
+ }
438
+ export {
439
+ Exporter,
440
+ MODEL_PRICING,
441
+ ObserveOSTracer,
442
+ calculateCost,
443
+ createObserveOS,
444
+ getModelPricing,
445
+ hashPrompt,
446
+ wrapAnthropic,
447
+ wrapHuggingFace,
448
+ wrapOllama,
449
+ wrapOpenAI
450
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "observeos",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript-native LLM observability. Trace OpenAI, Anthropic, Ollama, Hugging Face with zero config.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": ["dist", "README.md", "LICENSE"],
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
19
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:ui": "vitest --ui",
23
+ "type-check": "tsc --noEmit",
24
+ "lint": "eslint src --ext .ts",
25
+ "prepublishOnly": "npm run build && npm test && npm run type-check"
26
+ },
27
+ "peerDependencies": {
28
+ "openai": ">=4.0.0",
29
+ "@anthropic-ai/sdk": ">=0.20.0",
30
+ "@huggingface/inference": ">=2.0.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "openai": { "optional": true },
34
+ "@anthropic-ai/sdk": { "optional": true },
35
+ "@huggingface/inference": { "optional": true }
36
+ },
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "tsup": "^8.0.0",
40
+ "vitest": "^1.6.0",
41
+ "typescript": "^5.4.0",
42
+ "openai": "^4.0.0",
43
+ "@anthropic-ai/sdk": "^0.20.0",
44
+ "@huggingface/inference": "^2.8.0",
45
+ "@types/node": "^20.0.0",
46
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
47
+ "@typescript-eslint/parser": "^7.0.0",
48
+ "eslint": "^8.0.0"
49
+ },
50
+ "keywords": [
51
+ "llm", "observability", "monitoring", "tracing", "opentelemetry",
52
+ "openai", "anthropic", "ollama", "huggingface",
53
+ "ai", "typescript", "sdk", "self-hosted"
54
+ ],
55
+ "author": "Abdul Mueez <abdulmoiz3140@gmail.com>",
56
+ "license": "MIT",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/Abdul-Moiz31/ObserveOS.git"
60
+ },
61
+ "homepage": "https://github.com/Abdul-Moiz31/ObserveOS#readme",
62
+ "bugs": {
63
+ "url": "https://github.com/Abdul-Moiz31/ObserveOS/issues"
64
+ },
65
+ "engines": {
66
+ "node": ">=20"
67
+ }
68
+ }