@syntheticminds/wiretap-llm 0.2.3
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 +176 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/fetch.d.ts +12 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +547 -0
- package/dist/json.d.ts +19 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/types/telemetry.d.ts +56 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# wiretap-llm
|
|
2
|
+
|
|
3
|
+
Telemetry client for sending LLM logs to your Wiretap instance.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install wiretap-llm
|
|
9
|
+
# or
|
|
10
|
+
bun add wiretap-llm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start - Automatic Instrumentation
|
|
14
|
+
|
|
15
|
+
The easiest way to use wiretap-llm is with `createInstrumentedFetch`. It automatically captures all OpenRouter/OpenAI requests:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import OpenAI from "openai";
|
|
19
|
+
import { TelemetryClient, createInstrumentedFetch } from "wiretap-llm";
|
|
20
|
+
|
|
21
|
+
// Create telemetry client
|
|
22
|
+
const telemetry = new TelemetryClient({
|
|
23
|
+
baseUrl: process.env.WIRETAP_BASE_URL!,
|
|
24
|
+
apiKey: process.env.WIRETAP_API_KEY!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create OpenAI client with instrumented fetch
|
|
28
|
+
const openrouter = new OpenAI({
|
|
29
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
30
|
+
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
31
|
+
fetch: createInstrumentedFetch(telemetry),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Use normally - all requests are automatically logged
|
|
35
|
+
const response = await openrouter.chat.completions.create({
|
|
36
|
+
model: "anthropic/claude-sonnet-4",
|
|
37
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Streaming works too
|
|
41
|
+
const stream = await openrouter.chat.completions.create({
|
|
42
|
+
model: "anthropic/claude-sonnet-4",
|
|
43
|
+
messages: [{ role: "user", content: "Count to 5" }],
|
|
44
|
+
stream: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// On app shutdown
|
|
52
|
+
await telemetry.shutdown();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Manual Reporting
|
|
56
|
+
|
|
57
|
+
For custom logging scenarios, you can report logs manually:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { TelemetryClient, createId, toIso } from "wiretap-llm";
|
|
61
|
+
|
|
62
|
+
const client = new TelemetryClient({
|
|
63
|
+
baseUrl: process.env.WIRETAP_BASE_URL!,
|
|
64
|
+
apiKey: process.env.WIRETAP_API_KEY!,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
client.report({
|
|
68
|
+
id: createId(),
|
|
69
|
+
timestamp: toIso(new Date()),
|
|
70
|
+
project: "", // Inferred from API key
|
|
71
|
+
source: "openrouter",
|
|
72
|
+
method: "POST",
|
|
73
|
+
url: "https://openrouter.ai/api/v1/chat/completions",
|
|
74
|
+
path: "/v1/chat/completions",
|
|
75
|
+
model: "anthropic/claude-sonnet-4",
|
|
76
|
+
streamed: false,
|
|
77
|
+
timing: {
|
|
78
|
+
started_at: toIso(startTime),
|
|
79
|
+
ended_at: toIso(endTime),
|
|
80
|
+
duration_ms: endTime.getTime() - startTime.getTime(),
|
|
81
|
+
},
|
|
82
|
+
request: {
|
|
83
|
+
body_json: requestBody,
|
|
84
|
+
},
|
|
85
|
+
response: {
|
|
86
|
+
status: 200,
|
|
87
|
+
body_json: responseBody,
|
|
88
|
+
},
|
|
89
|
+
usage: responseBody.usage,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await client.shutdown();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
### TelemetryClient Options
|
|
98
|
+
|
|
99
|
+
| Option | Type | Default | Description |
|
|
100
|
+
|--------|------|---------|-------------|
|
|
101
|
+
| `baseUrl` | string | required | Your Wiretap instance URL |
|
|
102
|
+
| `apiKey` | string | required | Your Wiretap API key |
|
|
103
|
+
| `project` | string | - | Project slug (usually inferred from API key) |
|
|
104
|
+
| `environment` | string | - | Environment name (e.g., "production") |
|
|
105
|
+
| `defaultMetadata` | object | - | Metadata added to all logs |
|
|
106
|
+
| `timeoutMs` | number | 5000 | Request timeout |
|
|
107
|
+
| `maxBatchSize` | number | 25 | Max logs per batch |
|
|
108
|
+
| `flushIntervalMs` | number | 2000 | Auto-flush interval |
|
|
109
|
+
| `maxLogBytes` | number | 1000000 | Max log size before truncation |
|
|
110
|
+
| `maxQueueSize` | number | 1000 | Max queue size (drops oldest when exceeded) |
|
|
111
|
+
| `retryBaseDelayMs` | number | 1000 | Initial retry delay |
|
|
112
|
+
| `retryMaxDelayMs` | number | 30000 | Maximum retry delay |
|
|
113
|
+
| `maxRetries` | number | 5 | Max retries before dropping batch |
|
|
114
|
+
| `onError` | function | - | Error handler callback |
|
|
115
|
+
|
|
116
|
+
### createInstrumentedFetch Options
|
|
117
|
+
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
|--------|------|---------|-------------|
|
|
120
|
+
| `baseFetch` | fetch | globalThis.fetch | Base fetch function to wrap |
|
|
121
|
+
| `defaultMetadata` | object | - | Metadata added to all logs |
|
|
122
|
+
| `source` | string | "openrouter" | Source identifier in logs |
|
|
123
|
+
|
|
124
|
+
## Reliability Features
|
|
125
|
+
|
|
126
|
+
### Automatic Retry with Exponential Backoff
|
|
127
|
+
|
|
128
|
+
Failed batches are automatically retried with exponential backoff (1s, 2s, 4s, ...) up to 30s max delay.
|
|
129
|
+
|
|
130
|
+
### Circuit Breaker
|
|
131
|
+
|
|
132
|
+
After 5 consecutive failures, the circuit breaker opens for 30 seconds. During this time, flushes are skipped to prevent hammering a dead server.
|
|
133
|
+
|
|
134
|
+
### Queue Limits
|
|
135
|
+
|
|
136
|
+
If the queue exceeds 1000 logs (configurable), oldest logs are dropped to prevent memory exhaustion.
|
|
137
|
+
|
|
138
|
+
### Graceful Shutdown
|
|
139
|
+
|
|
140
|
+
Call `shutdown()` on app exit to flush remaining logs:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
process.on("SIGTERM", async () => {
|
|
144
|
+
await telemetry.shutdown();
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Helper Utilities
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { createId, toIso, sanitizeHeaders, safeJsonParse } from "wiretap-llm";
|
|
153
|
+
|
|
154
|
+
// Generate unique ID
|
|
155
|
+
const id = createId(); // "abc123..."
|
|
156
|
+
|
|
157
|
+
// ISO timestamp
|
|
158
|
+
const ts = toIso(new Date()); // "2024-01-20T..."
|
|
159
|
+
|
|
160
|
+
// Remove sensitive headers
|
|
161
|
+
const headers = sanitizeHeaders(req.headers); // Strips auth headers
|
|
162
|
+
|
|
163
|
+
// Safe JSON parse
|
|
164
|
+
const { ok, value } = safeJsonParse(jsonString);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Environment Variables
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
WIRETAP_BASE_URL=https://your-wiretap-instance.com
|
|
171
|
+
WIRETAP_API_KEY=wt_your_api_key
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { JsonValue } from "./json";
|
|
2
|
+
import type { TelemetryLog } from "./types/telemetry";
|
|
3
|
+
export type TelemetryClientOptions = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Optional - project is automatically inferred from API key on server */
|
|
7
|
+
project?: string;
|
|
8
|
+
environment?: string;
|
|
9
|
+
defaultMetadata?: Record<string, JsonValue>;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
maxBatchSize?: number;
|
|
12
|
+
flushIntervalMs?: number;
|
|
13
|
+
maxLogBytes?: number;
|
|
14
|
+
/** Max queue size before dropping oldest logs (default: 1000) */
|
|
15
|
+
maxQueueSize?: number;
|
|
16
|
+
/** Base delay for retry backoff in ms (default: 1000) */
|
|
17
|
+
retryBaseDelayMs?: number;
|
|
18
|
+
/** Max retry delay in ms (default: 30000) */
|
|
19
|
+
retryMaxDelayMs?: number;
|
|
20
|
+
/** Max retries per batch before dropping (default: 5) */
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
onError?: (err: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
export declare class TelemetryClient {
|
|
25
|
+
readonly baseUrl: string;
|
|
26
|
+
readonly apiKey: string;
|
|
27
|
+
readonly project?: string;
|
|
28
|
+
readonly environment?: string;
|
|
29
|
+
readonly defaultMetadata?: Record<string, JsonValue>;
|
|
30
|
+
private timeoutMs;
|
|
31
|
+
private maxBatchSize;
|
|
32
|
+
private flushIntervalMs;
|
|
33
|
+
private maxLogBytes;
|
|
34
|
+
private maxQueueSize;
|
|
35
|
+
private retryBaseDelayMs;
|
|
36
|
+
private retryMaxDelayMs;
|
|
37
|
+
private maxRetries;
|
|
38
|
+
private queue;
|
|
39
|
+
private flushing;
|
|
40
|
+
private timer;
|
|
41
|
+
private retryCount;
|
|
42
|
+
private retryDelayMs;
|
|
43
|
+
private circuitOpen;
|
|
44
|
+
private circuitOpenUntil;
|
|
45
|
+
private consecutiveFailures;
|
|
46
|
+
private readonly circuitBreakerThreshold;
|
|
47
|
+
private readonly circuitBreakerDurationMs;
|
|
48
|
+
private onError?;
|
|
49
|
+
constructor(opts: TelemetryClientOptions);
|
|
50
|
+
report(log: TelemetryLog): void;
|
|
51
|
+
private enforceQueueLimit;
|
|
52
|
+
flush(): Promise<void>;
|
|
53
|
+
private handleSuccess;
|
|
54
|
+
private handleFailure;
|
|
55
|
+
shutdown(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAyB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,qBAAa,eAAe;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAErD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAA+C;IAG5D,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAK;IAC7C,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAEnD,OAAO,CAAC,OAAO,CAAC,CAAyB;gBAE7B,IAAI,EAAE,sBAAsB;IA4BxC,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAsC/B,OAAO,CAAC,iBAAiB;IAQnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuF5B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;IAkCf,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAUhC"}
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TelemetryClient } from "./client";
|
|
2
|
+
import type { JsonValue } from "./json";
|
|
3
|
+
export type InstrumentedFetchOptions = {
|
|
4
|
+
/** Base fetch to wrap (defaults to globalThis.fetch) */
|
|
5
|
+
baseFetch?: typeof fetch;
|
|
6
|
+
/** Additional metadata to include in all logs */
|
|
7
|
+
defaultMetadata?: Record<string, JsonValue>;
|
|
8
|
+
/** Source identifier (default: "openrouter") */
|
|
9
|
+
source?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function createInstrumentedFetch(client: TelemetryClient, options?: InstrumentedFetchOptions): typeof fetch;
|
|
12
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKxC,MAAM,MAAM,wBAAwB,GAAG;IACrC,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAoBF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,KAAK,CA8Td"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { TelemetryClient } from "./client";
|
|
2
|
+
export type { TelemetryClientOptions } from "./client";
|
|
3
|
+
export { createInstrumentedFetch } from "./fetch";
|
|
4
|
+
export type { InstrumentedFetchOptions } from "./fetch";
|
|
5
|
+
export { parseSSE } from "./sse";
|
|
6
|
+
export type { SSEEvent } from "./sse";
|
|
7
|
+
export type { TelemetryBatchPayload, TelemetryLog, TelemetryTrace, } from "./types/telemetry";
|
|
8
|
+
export { createId, safeJsonParse, sanitizeHeaders, toIso, truncateUtf8 } from "./json";
|
|
9
|
+
export type { JsonPrimitive, JsonValue } from "./json";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,YAAY,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAGxD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,YAAY,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,cAAc,GACf,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACvF,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// src/json.ts
|
|
2
|
+
function toIso(d) {
|
|
3
|
+
return d.toISOString();
|
|
4
|
+
}
|
|
5
|
+
function createId() {
|
|
6
|
+
const c = globalThis.crypto;
|
|
7
|
+
if (c?.randomUUID)
|
|
8
|
+
return c.randomUUID();
|
|
9
|
+
return `id_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
}
|
|
11
|
+
function safeJsonParse(text) {
|
|
12
|
+
try {
|
|
13
|
+
const v = JSON.parse(text);
|
|
14
|
+
return { ok: true, value: v };
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return { ok: false, error: e instanceof Error ? e.message : "Invalid JSON" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var SENSITIVE_HEADERS = new Set([
|
|
20
|
+
"authorization",
|
|
21
|
+
"cookie",
|
|
22
|
+
"set-cookie",
|
|
23
|
+
"x-api-key",
|
|
24
|
+
"api-key"
|
|
25
|
+
]);
|
|
26
|
+
function sanitizeHeaders(headers) {
|
|
27
|
+
const out = {};
|
|
28
|
+
if (!headers)
|
|
29
|
+
return out;
|
|
30
|
+
const set = (k, v) => {
|
|
31
|
+
const key = k.toLowerCase();
|
|
32
|
+
if (SENSITIVE_HEADERS.has(key))
|
|
33
|
+
return;
|
|
34
|
+
out[key] = v;
|
|
35
|
+
};
|
|
36
|
+
if (headers instanceof Headers) {
|
|
37
|
+
headers.forEach((v, k) => set(k, v));
|
|
38
|
+
} else if (Array.isArray(headers)) {
|
|
39
|
+
for (const [k, v] of headers)
|
|
40
|
+
set(k, v);
|
|
41
|
+
} else {
|
|
42
|
+
for (const [k, v] of Object.entries(headers))
|
|
43
|
+
set(k, String(v));
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function truncateUtf8(input, maxBytes) {
|
|
48
|
+
const enc = new TextEncoder;
|
|
49
|
+
const bytes = enc.encode(input);
|
|
50
|
+
if (bytes.length <= maxBytes)
|
|
51
|
+
return { text: input, truncated: false };
|
|
52
|
+
const sliced = bytes.slice(0, maxBytes);
|
|
53
|
+
const dec = new TextDecoder("utf-8", { fatal: false });
|
|
54
|
+
return { text: dec.decode(sliced), truncated: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/client.ts
|
|
58
|
+
class TelemetryClient {
|
|
59
|
+
baseUrl;
|
|
60
|
+
apiKey;
|
|
61
|
+
project;
|
|
62
|
+
environment;
|
|
63
|
+
defaultMetadata;
|
|
64
|
+
timeoutMs;
|
|
65
|
+
maxBatchSize;
|
|
66
|
+
flushIntervalMs;
|
|
67
|
+
maxLogBytes;
|
|
68
|
+
maxQueueSize;
|
|
69
|
+
retryBaseDelayMs;
|
|
70
|
+
retryMaxDelayMs;
|
|
71
|
+
maxRetries;
|
|
72
|
+
queue = [];
|
|
73
|
+
flushing = false;
|
|
74
|
+
timer = null;
|
|
75
|
+
retryCount = 0;
|
|
76
|
+
retryDelayMs;
|
|
77
|
+
circuitOpen = false;
|
|
78
|
+
circuitOpenUntil = 0;
|
|
79
|
+
consecutiveFailures = 0;
|
|
80
|
+
circuitBreakerThreshold = 5;
|
|
81
|
+
circuitBreakerDurationMs = 30000;
|
|
82
|
+
onError;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
85
|
+
this.apiKey = opts.apiKey;
|
|
86
|
+
this.project = opts.project;
|
|
87
|
+
this.environment = opts.environment;
|
|
88
|
+
this.defaultMetadata = opts.defaultMetadata;
|
|
89
|
+
this.timeoutMs = opts.timeoutMs ?? 5000;
|
|
90
|
+
this.maxBatchSize = opts.maxBatchSize ?? 25;
|
|
91
|
+
this.flushIntervalMs = opts.flushIntervalMs ?? 2000;
|
|
92
|
+
this.maxLogBytes = opts.maxLogBytes ?? 1e6;
|
|
93
|
+
this.maxQueueSize = opts.maxQueueSize ?? 1000;
|
|
94
|
+
this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 1000;
|
|
95
|
+
this.retryMaxDelayMs = opts.retryMaxDelayMs ?? 30000;
|
|
96
|
+
this.maxRetries = opts.maxRetries ?? 5;
|
|
97
|
+
this.retryDelayMs = this.retryBaseDelayMs;
|
|
98
|
+
this.onError = opts.onError;
|
|
99
|
+
if (this.flushIntervalMs > 0) {
|
|
100
|
+
this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
|
|
101
|
+
if (typeof this.timer === "object" && "unref" in this.timer) {
|
|
102
|
+
this.timer.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
report(log) {
|
|
107
|
+
if (this.project) {
|
|
108
|
+
log.project = log.project || this.project;
|
|
109
|
+
}
|
|
110
|
+
log.environment = log.environment ?? this.environment;
|
|
111
|
+
if (this.defaultMetadata) {
|
|
112
|
+
log.metadata = { ...this.defaultMetadata, ...log.metadata ?? {} };
|
|
113
|
+
}
|
|
114
|
+
const raw = JSON.stringify(log);
|
|
115
|
+
const truncated = truncateUtf8(raw, this.maxLogBytes);
|
|
116
|
+
if (!truncated.truncated) {
|
|
117
|
+
this.queue.push(log);
|
|
118
|
+
} else {
|
|
119
|
+
this.queue.push({
|
|
120
|
+
...log,
|
|
121
|
+
request: {
|
|
122
|
+
...log.request,
|
|
123
|
+
body_text: "[truncated - log exceeded maxLogBytes]",
|
|
124
|
+
body_truncated: true
|
|
125
|
+
},
|
|
126
|
+
error: log.error ?? { message: "Log truncated to maxLogBytes" }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
this.enforceQueueLimit();
|
|
130
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
131
|
+
this.flush();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
enforceQueueLimit() {
|
|
135
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
136
|
+
const dropped = this.queue.length - this.maxQueueSize;
|
|
137
|
+
this.queue.splice(0, dropped);
|
|
138
|
+
this.onError?.(new Error(`Queue limit exceeded, dropped ${dropped} logs`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async flush() {
|
|
142
|
+
if (this.flushing)
|
|
143
|
+
return;
|
|
144
|
+
if (this.queue.length === 0)
|
|
145
|
+
return;
|
|
146
|
+
if (this.circuitOpen) {
|
|
147
|
+
if (Date.now() < this.circuitOpenUntil) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.circuitOpen = false;
|
|
151
|
+
}
|
|
152
|
+
this.flushing = true;
|
|
153
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
154
|
+
try {
|
|
155
|
+
const payload = { logs: batch };
|
|
156
|
+
const ctrl = new AbortController;
|
|
157
|
+
const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
158
|
+
const res = await fetch(`${this.baseUrl}/v1/logs/batch`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"content-type": "application/json",
|
|
162
|
+
authorization: `Bearer ${this.apiKey}`
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify(payload),
|
|
165
|
+
signal: ctrl.signal
|
|
166
|
+
});
|
|
167
|
+
clearTimeout(t);
|
|
168
|
+
if (res.status === 207) {
|
|
169
|
+
const body = await res.json().catch(() => null);
|
|
170
|
+
const failedIds = new Set;
|
|
171
|
+
if (body && typeof body === "object") {
|
|
172
|
+
const failed = body.failed;
|
|
173
|
+
if (Array.isArray(failed)) {
|
|
174
|
+
for (const item of failed) {
|
|
175
|
+
if (item && typeof item === "object") {
|
|
176
|
+
const id = item.id;
|
|
177
|
+
if (typeof id === "string") {
|
|
178
|
+
failedIds.add(id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (failedIds.size > 0) {
|
|
185
|
+
const retry = batch.filter((log) => failedIds.has(log.id));
|
|
186
|
+
if (retry.length > 0) {
|
|
187
|
+
this.queue.unshift(...retry);
|
|
188
|
+
this.enforceQueueLimit();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const partialMessage = body && typeof body === "object" ? body.partial_success?.error_message : undefined;
|
|
192
|
+
const message = typeof partialMessage === "string" ? partialMessage : "Telemetry partial success";
|
|
193
|
+
this.onError?.(new Error(message));
|
|
194
|
+
this.handleSuccess();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const errorText = await res.text().catch(() => "");
|
|
199
|
+
throw new Error(`Telemetry ingest failed: ${res.status} ${res.statusText} - ${errorText}`);
|
|
200
|
+
}
|
|
201
|
+
this.handleSuccess();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this.handleFailure(batch, err);
|
|
204
|
+
} finally {
|
|
205
|
+
this.flushing = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
handleSuccess() {
|
|
209
|
+
this.consecutiveFailures = 0;
|
|
210
|
+
this.retryCount = 0;
|
|
211
|
+
this.retryDelayMs = this.retryBaseDelayMs;
|
|
212
|
+
this.circuitOpen = false;
|
|
213
|
+
}
|
|
214
|
+
handleFailure(batch, err) {
|
|
215
|
+
this.consecutiveFailures++;
|
|
216
|
+
this.retryCount++;
|
|
217
|
+
if (this.consecutiveFailures >= this.circuitBreakerThreshold) {
|
|
218
|
+
this.circuitOpen = true;
|
|
219
|
+
this.circuitOpenUntil = Date.now() + this.circuitBreakerDurationMs;
|
|
220
|
+
this.onError?.(new Error(`Circuit breaker opened after ${this.consecutiveFailures} failures`));
|
|
221
|
+
}
|
|
222
|
+
if (this.retryCount > this.maxRetries) {
|
|
223
|
+
this.onError?.(new Error(`Dropping batch after ${this.maxRetries} retries: ${err}`));
|
|
224
|
+
this.retryCount = 0;
|
|
225
|
+
this.retryDelayMs = this.retryBaseDelayMs;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.queue.unshift(...batch);
|
|
229
|
+
this.enforceQueueLimit();
|
|
230
|
+
this.onError?.(err);
|
|
231
|
+
this.retryDelayMs = Math.min(this.retryDelayMs * 2 + Math.random() * 100, this.retryMaxDelayMs);
|
|
232
|
+
setTimeout(() => void this.flush(), this.retryDelayMs);
|
|
233
|
+
}
|
|
234
|
+
async shutdown() {
|
|
235
|
+
if (this.timer) {
|
|
236
|
+
clearInterval(this.timer);
|
|
237
|
+
this.timer = null;
|
|
238
|
+
}
|
|
239
|
+
this.circuitOpen = false;
|
|
240
|
+
await this.flush();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// src/sse.ts
|
|
244
|
+
function parseSSE(chunk) {
|
|
245
|
+
const events = [];
|
|
246
|
+
const normalized = chunk.replace(/\r\n/g, "\n");
|
|
247
|
+
const blocks = normalized.split("\n\n");
|
|
248
|
+
for (let i = 0;i < blocks.length - 1; i += 1) {
|
|
249
|
+
const block = blocks[i];
|
|
250
|
+
if (!block.trim())
|
|
251
|
+
continue;
|
|
252
|
+
const lines = block.split("\n");
|
|
253
|
+
const data = [];
|
|
254
|
+
let event;
|
|
255
|
+
let id;
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
if (!line || line.startsWith(":"))
|
|
258
|
+
continue;
|
|
259
|
+
const idx = line.indexOf(":");
|
|
260
|
+
if (idx === -1)
|
|
261
|
+
continue;
|
|
262
|
+
const field = line.slice(0, idx).trim();
|
|
263
|
+
const raw = line.slice(idx + 1);
|
|
264
|
+
const value = raw.startsWith(" ") ? raw.slice(1) : raw;
|
|
265
|
+
if (field === "data") {
|
|
266
|
+
data.push(value);
|
|
267
|
+
} else if (field === "event") {
|
|
268
|
+
event = value;
|
|
269
|
+
} else if (field === "id") {
|
|
270
|
+
id = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (data.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
events.push({ data: data.join("\n"), event, id });
|
|
276
|
+
}
|
|
277
|
+
return events;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/fetch.ts
|
|
281
|
+
function shouldIntercept(url) {
|
|
282
|
+
return url.includes("/chat/completions") || url.includes("/completions");
|
|
283
|
+
}
|
|
284
|
+
function safeJsonParse2(text) {
|
|
285
|
+
try {
|
|
286
|
+
return { json: JSON.parse(text) };
|
|
287
|
+
} catch {
|
|
288
|
+
return { text };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function createInstrumentedFetch(client, options) {
|
|
292
|
+
const baseFetch = options?.baseFetch ?? globalThis.fetch;
|
|
293
|
+
const defaultMetadata = options?.defaultMetadata;
|
|
294
|
+
const source = options?.source ?? "openrouter";
|
|
295
|
+
const instrumentedFetch = async (input, init) => {
|
|
296
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
297
|
+
if (!shouldIntercept(url)) {
|
|
298
|
+
return baseFetch(input, init);
|
|
299
|
+
}
|
|
300
|
+
const startTime = new Date;
|
|
301
|
+
const logId = createId();
|
|
302
|
+
const method = init?.method ?? "GET";
|
|
303
|
+
const path = new URL(url).pathname;
|
|
304
|
+
let requestBody = {};
|
|
305
|
+
if (init?.body) {
|
|
306
|
+
const bodyStr = typeof init.body === "string" ? init.body : String(init.body);
|
|
307
|
+
requestBody = safeJsonParse2(bodyStr);
|
|
308
|
+
}
|
|
309
|
+
const model = requestBody.json?.model;
|
|
310
|
+
const streamed = requestBody.json?.stream === true;
|
|
311
|
+
const requestMetadata = requestBody.json?.metadata;
|
|
312
|
+
const sanitizedHeaders = sanitizeHeaders(init?.headers);
|
|
313
|
+
try {
|
|
314
|
+
const response = await baseFetch(input, init);
|
|
315
|
+
if (!streamed) {
|
|
316
|
+
const endTime = new Date;
|
|
317
|
+
const cloned = response.clone();
|
|
318
|
+
let responseBody = {};
|
|
319
|
+
let outputText;
|
|
320
|
+
let finishReason2;
|
|
321
|
+
let usage2;
|
|
322
|
+
try {
|
|
323
|
+
const text = await cloned.text();
|
|
324
|
+
responseBody = safeJsonParse2(text);
|
|
325
|
+
if (responseBody.json) {
|
|
326
|
+
const json = responseBody.json;
|
|
327
|
+
const choices = json.choices;
|
|
328
|
+
if (choices?.[0]) {
|
|
329
|
+
const message = choices[0].message;
|
|
330
|
+
outputText = message?.content;
|
|
331
|
+
finishReason2 = choices[0].finish_reason;
|
|
332
|
+
}
|
|
333
|
+
usage2 = json.usage;
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const log = {
|
|
339
|
+
id: logId,
|
|
340
|
+
timestamp: toIso(startTime),
|
|
341
|
+
project: client.project ?? "",
|
|
342
|
+
environment: client.environment,
|
|
343
|
+
source,
|
|
344
|
+
method,
|
|
345
|
+
url,
|
|
346
|
+
path,
|
|
347
|
+
model,
|
|
348
|
+
streamed: false,
|
|
349
|
+
timing: {
|
|
350
|
+
started_at: toIso(startTime),
|
|
351
|
+
ended_at: toIso(endTime),
|
|
352
|
+
duration_ms: endTime.getTime() - startTime.getTime()
|
|
353
|
+
},
|
|
354
|
+
metadata: requestMetadata ? { ...defaultMetadata, ...requestMetadata } : defaultMetadata,
|
|
355
|
+
request: {
|
|
356
|
+
headers: sanitizedHeaders,
|
|
357
|
+
body_json: requestBody.json,
|
|
358
|
+
body_text: requestBody.text
|
|
359
|
+
},
|
|
360
|
+
response: {
|
|
361
|
+
status: response.status,
|
|
362
|
+
headers: sanitizeHeaders(response.headers),
|
|
363
|
+
body_json: responseBody.json,
|
|
364
|
+
body_text: responseBody.text
|
|
365
|
+
},
|
|
366
|
+
stream: outputText ? {
|
|
367
|
+
output_text: outputText,
|
|
368
|
+
finish_reason: finishReason2 ?? null,
|
|
369
|
+
usage: usage2
|
|
370
|
+
} : undefined,
|
|
371
|
+
usage: usage2
|
|
372
|
+
};
|
|
373
|
+
client.report(log);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
return response;
|
|
377
|
+
}
|
|
378
|
+
if (!response.body) {
|
|
379
|
+
return response;
|
|
380
|
+
}
|
|
381
|
+
const sseEvents = [];
|
|
382
|
+
let sseTruncated = false;
|
|
383
|
+
const textChunks = [];
|
|
384
|
+
const toolCalls = new Map;
|
|
385
|
+
let finishReason = null;
|
|
386
|
+
let usage;
|
|
387
|
+
const transform = new TransformStream({
|
|
388
|
+
transform(chunk, controller) {
|
|
389
|
+
controller.enqueue(chunk);
|
|
390
|
+
try {
|
|
391
|
+
const text = new TextDecoder().decode(chunk);
|
|
392
|
+
const events = parseSSE(text);
|
|
393
|
+
for (const event of events) {
|
|
394
|
+
if (event.data === "[DONE]")
|
|
395
|
+
continue;
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(event.data);
|
|
398
|
+
if (sseEvents.length < 100) {
|
|
399
|
+
sseEvents.push(parsed);
|
|
400
|
+
} else if (!sseTruncated) {
|
|
401
|
+
sseTruncated = true;
|
|
402
|
+
}
|
|
403
|
+
const choices = parsed.choices;
|
|
404
|
+
const delta = choices?.[0]?.delta;
|
|
405
|
+
if (delta?.content) {
|
|
406
|
+
textChunks.push(delta.content);
|
|
407
|
+
}
|
|
408
|
+
const deltaToolCalls = delta?.tool_calls;
|
|
409
|
+
if (deltaToolCalls) {
|
|
410
|
+
for (const tc of deltaToolCalls) {
|
|
411
|
+
const idx = tc.index ?? 0;
|
|
412
|
+
if (!toolCalls.has(idx)) {
|
|
413
|
+
toolCalls.set(idx, {
|
|
414
|
+
id: tc.id ?? "",
|
|
415
|
+
type: "function",
|
|
416
|
+
function: { name: "", arguments: "" }
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const existing = toolCalls.get(idx);
|
|
420
|
+
if (tc.id)
|
|
421
|
+
existing.id = tc.id;
|
|
422
|
+
const fn = tc.function;
|
|
423
|
+
if (fn?.name)
|
|
424
|
+
existing.function.name = fn.name;
|
|
425
|
+
if (fn?.arguments)
|
|
426
|
+
existing.function.arguments += fn.arguments;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (choices?.[0]?.finish_reason) {
|
|
430
|
+
finishReason = choices[0].finish_reason;
|
|
431
|
+
}
|
|
432
|
+
if (parsed.usage) {
|
|
433
|
+
usage = parsed.usage;
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
flush() {
|
|
442
|
+
const endTime = new Date;
|
|
443
|
+
const toolCallsArray = Array.from(toolCalls.values());
|
|
444
|
+
const outputText = textChunks.join("");
|
|
445
|
+
let finalEvents = sseEvents;
|
|
446
|
+
if (sseTruncated && sseEvents.length >= 100) {
|
|
447
|
+
finalEvents = [
|
|
448
|
+
...sseEvents.slice(0, 50),
|
|
449
|
+
{ truncated: true, total: sseEvents.length },
|
|
450
|
+
...sseEvents.slice(-50)
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const log = {
|
|
455
|
+
id: logId,
|
|
456
|
+
timestamp: toIso(startTime),
|
|
457
|
+
project: client.project ?? "",
|
|
458
|
+
environment: client.environment,
|
|
459
|
+
source,
|
|
460
|
+
method,
|
|
461
|
+
url,
|
|
462
|
+
path,
|
|
463
|
+
model,
|
|
464
|
+
streamed: true,
|
|
465
|
+
timing: {
|
|
466
|
+
started_at: toIso(startTime),
|
|
467
|
+
ended_at: toIso(endTime),
|
|
468
|
+
duration_ms: endTime.getTime() - startTime.getTime()
|
|
469
|
+
},
|
|
470
|
+
metadata: requestMetadata ? { ...defaultMetadata, ...requestMetadata } : defaultMetadata,
|
|
471
|
+
request: {
|
|
472
|
+
headers: sanitizedHeaders,
|
|
473
|
+
body_json: requestBody.json,
|
|
474
|
+
body_text: requestBody.text
|
|
475
|
+
},
|
|
476
|
+
response: {
|
|
477
|
+
status: response.status,
|
|
478
|
+
headers: sanitizeHeaders(response.headers)
|
|
479
|
+
},
|
|
480
|
+
stream: {
|
|
481
|
+
sse_events: finalEvents,
|
|
482
|
+
sse_events_truncated: sseTruncated,
|
|
483
|
+
output_text: outputText,
|
|
484
|
+
tool_calls: toolCallsArray.length > 0 ? toolCallsArray : undefined,
|
|
485
|
+
finish_reason: finishReason,
|
|
486
|
+
usage
|
|
487
|
+
},
|
|
488
|
+
usage
|
|
489
|
+
};
|
|
490
|
+
client.report(log);
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
496
|
+
status: response.status,
|
|
497
|
+
statusText: response.statusText,
|
|
498
|
+
headers: response.headers
|
|
499
|
+
});
|
|
500
|
+
} catch (err) {
|
|
501
|
+
const endTime = new Date;
|
|
502
|
+
try {
|
|
503
|
+
const log = {
|
|
504
|
+
id: logId,
|
|
505
|
+
timestamp: toIso(startTime),
|
|
506
|
+
project: client.project ?? "",
|
|
507
|
+
environment: client.environment,
|
|
508
|
+
source,
|
|
509
|
+
method,
|
|
510
|
+
url,
|
|
511
|
+
path,
|
|
512
|
+
model,
|
|
513
|
+
streamed,
|
|
514
|
+
timing: {
|
|
515
|
+
started_at: toIso(startTime),
|
|
516
|
+
ended_at: toIso(endTime),
|
|
517
|
+
duration_ms: endTime.getTime() - startTime.getTime()
|
|
518
|
+
},
|
|
519
|
+
metadata: requestMetadata ? { ...defaultMetadata, ...requestMetadata } : defaultMetadata,
|
|
520
|
+
request: {
|
|
521
|
+
headers: sanitizedHeaders,
|
|
522
|
+
body_json: requestBody.json,
|
|
523
|
+
body_text: requestBody.text
|
|
524
|
+
},
|
|
525
|
+
error: {
|
|
526
|
+
message: err instanceof Error ? err.message : String(err),
|
|
527
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
client.report(log);
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
return instrumentedFetch;
|
|
537
|
+
}
|
|
538
|
+
export {
|
|
539
|
+
truncateUtf8,
|
|
540
|
+
toIso,
|
|
541
|
+
sanitizeHeaders,
|
|
542
|
+
safeJsonParse,
|
|
543
|
+
parseSSE,
|
|
544
|
+
createInstrumentedFetch,
|
|
545
|
+
createId,
|
|
546
|
+
TelemetryClient
|
|
547
|
+
};
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export declare function toIso(d: Date): string;
|
|
6
|
+
export declare function createId(): string;
|
|
7
|
+
export declare function safeJsonParse(text: string): {
|
|
8
|
+
ok: true;
|
|
9
|
+
value: JsonValue;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function sanitizeHeaders(headers: HeadersInit | undefined): Record<string, string>;
|
|
15
|
+
export declare function truncateUtf8(input: string, maxBytes: number): {
|
|
16
|
+
text: string;
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnF,wBAAgB,KAAK,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAErC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAIjC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOzG;AAUD,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAQlG"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAoClD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JsonValue } from "../json";
|
|
2
|
+
export type TelemetryTrace = {
|
|
3
|
+
trace_id?: string;
|
|
4
|
+
span_id?: string;
|
|
5
|
+
parent_span_id?: string;
|
|
6
|
+
};
|
|
7
|
+
export type TelemetryLog = {
|
|
8
|
+
id: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
project: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
source: string;
|
|
13
|
+
method: string;
|
|
14
|
+
url: string;
|
|
15
|
+
path: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
generation_id?: string;
|
|
18
|
+
streamed: boolean;
|
|
19
|
+
timing: {
|
|
20
|
+
started_at: string;
|
|
21
|
+
ended_at?: string;
|
|
22
|
+
duration_ms?: number;
|
|
23
|
+
};
|
|
24
|
+
metadata?: Record<string, JsonValue>;
|
|
25
|
+
trace?: TelemetryTrace;
|
|
26
|
+
request: {
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
body_json?: JsonValue;
|
|
29
|
+
body_text?: string;
|
|
30
|
+
body_truncated?: boolean;
|
|
31
|
+
};
|
|
32
|
+
response?: {
|
|
33
|
+
status: number;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
body_json?: JsonValue;
|
|
36
|
+
body_text?: string;
|
|
37
|
+
body_truncated?: boolean;
|
|
38
|
+
};
|
|
39
|
+
stream?: {
|
|
40
|
+
sse_events?: JsonValue[];
|
|
41
|
+
sse_events_truncated?: boolean;
|
|
42
|
+
output_text?: string;
|
|
43
|
+
tool_calls?: JsonValue;
|
|
44
|
+
finish_reason?: string | null;
|
|
45
|
+
usage?: JsonValue;
|
|
46
|
+
};
|
|
47
|
+
usage?: JsonValue;
|
|
48
|
+
error?: {
|
|
49
|
+
message: string;
|
|
50
|
+
stack?: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export type TelemetryBatchPayload = {
|
|
54
|
+
logs: TelemetryLog[];
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/types/telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAElB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAElB,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IAEF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB,OAAO,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;IAEF,QAAQ,CAAC,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;IAEF,MAAM,CAAC,EAAE;QACP,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;QACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,SAAS,CAAC;QACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,KAAK,CAAC,EAAE,SAAS,CAAC;KACnB,CAAC;IAEF,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syntheticminds/wiretap-llm",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Telemetry client for sending LLM logs to Wiretap",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && tsc --emitDeclarationOnly",
|
|
21
|
+
"dev": "bun build ./src/index.ts --outdir ./dist --target node --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test:e2e": "bun test tests/e2e --preload ./tests/e2e/setup.ts",
|
|
24
|
+
"test:e2e:setup": "bun run ./tests/e2e/setup.ts",
|
|
25
|
+
"test:e2e:teardown": "bun run ./tests/e2e/teardown.ts",
|
|
26
|
+
"prepublishOnly": "bun run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"llm",
|
|
30
|
+
"telemetry",
|
|
31
|
+
"openrouter",
|
|
32
|
+
"openai",
|
|
33
|
+
"observability",
|
|
34
|
+
"monitoring",
|
|
35
|
+
"ai",
|
|
36
|
+
"wiretap"
|
|
37
|
+
],
|
|
38
|
+
"author": "Saint",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/kingbootoshi/wiretap.git",
|
|
43
|
+
"directory": "packages/sdk"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/kingbootoshi/wiretap/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/kingbootoshi/wiretap/tree/main/packages/sdk#readme",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@supabase/supabase-js": "^2.90.1",
|
|
57
|
+
"openai": "latest",
|
|
58
|
+
"typescript": "^5.7.2"
|
|
59
|
+
}
|
|
60
|
+
}
|