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 +116 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +487 -0
- package/dist/index.mjs +450 -0
- package/package.json +68 -0
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
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|