@tozil/sdk 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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/express.d.ts +4 -0
- package/dist/express.js +17 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +321 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tozil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# tozil
|
|
2
|
+
|
|
3
|
+
Know what every AI user costs you. Track AI costs per user, per model, per endpoint — with 2 lines of code.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install tozil
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import tozil from "tozil";
|
|
15
|
+
|
|
16
|
+
tozil.init(); // reads TOZIL_API_KEY from env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That's it. Tozil automatically patches the Anthropic and OpenAI SDKs to track every API call — tokens, latency, model, and cost.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
1. Sign up at [tozil.dev](https://tozil.dev) and grab your API key
|
|
24
|
+
2. Set your environment variable:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
TOZIL_API_KEY=tz_your_key_here
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Call `tozil.init()` before creating any Anthropic/OpenAI clients:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import tozil from "tozil";
|
|
34
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
35
|
+
|
|
36
|
+
tozil.init();
|
|
37
|
+
|
|
38
|
+
const client = new Anthropic();
|
|
39
|
+
const response = await client.messages.create({
|
|
40
|
+
model: "claude-sonnet-4-20250514",
|
|
41
|
+
max_tokens: 1024,
|
|
42
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
43
|
+
});
|
|
44
|
+
// ^ This call is automatically tracked
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Track Users & Endpoints
|
|
48
|
+
|
|
49
|
+
Use the Express middleware to automatically tag requests with user and endpoint info:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import tozil from "tozil";
|
|
53
|
+
import { tozilMiddleware } from "tozil/express";
|
|
54
|
+
import express from "express";
|
|
55
|
+
|
|
56
|
+
tozil.init();
|
|
57
|
+
|
|
58
|
+
const app = express();
|
|
59
|
+
app.use(tozilMiddleware({
|
|
60
|
+
getUserId: (req) => req.headers["x-user-id"] as string,
|
|
61
|
+
}));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or set context manually anywhere in your code:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { withContext } from "tozil";
|
|
68
|
+
|
|
69
|
+
await withContext({ userId: "user_123", endpoint: "/chat" }, async () => {
|
|
70
|
+
// All AI calls inside here are tagged with this user & endpoint
|
|
71
|
+
await client.messages.create({ ... });
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
tozil.init({
|
|
79
|
+
apiKey: "tz_...", // default: process.env.TOZIL_API_KEY
|
|
80
|
+
baseUrl: "https://...", // default: https://app.tozil.dev/api/v1
|
|
81
|
+
flushInterval: 5000, // ms between flushes (default: 5000)
|
|
82
|
+
maxBatchSize: 100, // events per batch (default: 100)
|
|
83
|
+
debug: false, // log tracking events to console
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## How It Works
|
|
88
|
+
|
|
89
|
+
- Monkey-patches Anthropic and OpenAI SDK prototypes at init time
|
|
90
|
+
- Uses Node.js AsyncLocalStorage for zero-config context propagation
|
|
91
|
+
- Buffers events in memory, flushes every 5s (or at batch size)
|
|
92
|
+
- Silent failure — never throws, never blocks your critical path
|
|
93
|
+
- Zero runtime dependencies
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- Node.js >= 18
|
|
98
|
+
- `@anthropic-ai/sdk` and/or `openai` as peer dependencies (install whichever you use)
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runWithContext, setUser, setEndpoint } from "./index.js";
|
|
2
|
+
export function tozilMiddleware(options) {
|
|
3
|
+
return (req, _res, next) => {
|
|
4
|
+
runWithContext(() => {
|
|
5
|
+
// Set endpoint from request
|
|
6
|
+
setEndpoint(req.path || req.url);
|
|
7
|
+
// Set user ID if extractor provided, or from common patterns
|
|
8
|
+
const userId = options?.getUserId
|
|
9
|
+
? options.getUserId(req)
|
|
10
|
+
: req.user?.id || req.user?.email || req.body?.user_id || null;
|
|
11
|
+
if (userId)
|
|
12
|
+
setUser(userId);
|
|
13
|
+
next();
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export default tozilMiddleware;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface TozilEvent {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
user_id: string | null;
|
|
4
|
+
model: string;
|
|
5
|
+
provider: "anthropic" | "openai" | "unknown";
|
|
6
|
+
endpoint: string | null;
|
|
7
|
+
input_tokens: number;
|
|
8
|
+
output_tokens: number;
|
|
9
|
+
latency_ms: number;
|
|
10
|
+
metadata: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function init(options?: {
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
flushInterval?: number;
|
|
16
|
+
maxBatchSize?: number;
|
|
17
|
+
debug?: boolean;
|
|
18
|
+
clients?: {
|
|
19
|
+
Anthropic?: any;
|
|
20
|
+
OpenAI?: any;
|
|
21
|
+
};
|
|
22
|
+
}): void;
|
|
23
|
+
export declare function setUser(userId: string): void;
|
|
24
|
+
export declare function setEndpoint(endpoint: string): void;
|
|
25
|
+
export declare function setMetadata(key: string, value: string): void;
|
|
26
|
+
export declare function withContext<T>(context: {
|
|
27
|
+
userId?: string;
|
|
28
|
+
endpoint?: string;
|
|
29
|
+
metadata?: Record<string, string>;
|
|
30
|
+
}, fn: () => T): T;
|
|
31
|
+
export declare function runWithContext<T>(fn: () => T): T;
|
|
32
|
+
export declare function shutdown(): void;
|
|
33
|
+
declare const _default: {
|
|
34
|
+
init: typeof init;
|
|
35
|
+
setUser: typeof setUser;
|
|
36
|
+
setEndpoint: typeof setEndpoint;
|
|
37
|
+
setMetadata: typeof setMetadata;
|
|
38
|
+
withContext: typeof withContext;
|
|
39
|
+
runWithContext: typeof runWithContext;
|
|
40
|
+
shutdown: typeof shutdown;
|
|
41
|
+
};
|
|
42
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
// --- State ---
|
|
4
|
+
const als = new AsyncLocalStorage();
|
|
5
|
+
let config = null;
|
|
6
|
+
let eventBuffer = [];
|
|
7
|
+
let flushTimer = null;
|
|
8
|
+
let initialized = false;
|
|
9
|
+
// --- Auto-discovery ---
|
|
10
|
+
// Resolve an SDK module from the host app's node_modules (not tozil's own).
|
|
11
|
+
// Uses createRequire anchored at cwd so it finds the host app's dependencies.
|
|
12
|
+
// CJS require returns the CJS entry which shares the same prototype chain as
|
|
13
|
+
// the ESM import when the package uses conditional exports pointing both to
|
|
14
|
+
// the same underlying class. For packages where CJS and ESM are separate
|
|
15
|
+
// builds, we fall back to async import() from the resolved file path.
|
|
16
|
+
function autoDiscover(modName, patchFn) {
|
|
17
|
+
try {
|
|
18
|
+
const hostRequire = createRequire(process.cwd() + "/package.json");
|
|
19
|
+
// First try: resolve the path, then use import() to get the ESM version
|
|
20
|
+
// This ensures we patch the same prototype the host app uses
|
|
21
|
+
const resolved = hostRequire.resolve(modName);
|
|
22
|
+
import(/* @vite-ignore */ "file://" + resolved)
|
|
23
|
+
.then((m) => patchFn(m.default || m))
|
|
24
|
+
.catch(() => {
|
|
25
|
+
// Fallback: use the CJS module directly
|
|
26
|
+
try {
|
|
27
|
+
const mod = hostRequire(modName);
|
|
28
|
+
patchFn(mod.default || mod);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
if (config?.debug)
|
|
35
|
+
console.log(`[tozil] ${modName} not installed, skipping`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// --- Public API ---
|
|
39
|
+
export function init(options) {
|
|
40
|
+
if (initialized)
|
|
41
|
+
return;
|
|
42
|
+
const apiKey = options?.apiKey || process.env.TOZIL_API_KEY;
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
if (options?.debug)
|
|
45
|
+
console.warn("[tozil] No API key found. Set TOZIL_API_KEY or pass apiKey option.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
config = {
|
|
49
|
+
apiKey,
|
|
50
|
+
baseUrl: options?.baseUrl || process.env.TOZIL_BASE_URL || "https://app.tozil.dev/api/v1",
|
|
51
|
+
flushInterval: options?.flushInterval || 5000,
|
|
52
|
+
maxBatchSize: options?.maxBatchSize || 100,
|
|
53
|
+
debug: options?.debug || false,
|
|
54
|
+
};
|
|
55
|
+
// Patch explicitly passed clients
|
|
56
|
+
if (options?.clients?.Anthropic)
|
|
57
|
+
patchAnthropic(options.clients.Anthropic);
|
|
58
|
+
if (options?.clients?.OpenAI)
|
|
59
|
+
patchOpenAI(options.clients.OpenAI);
|
|
60
|
+
// Auto-discover SDKs if not explicitly passed
|
|
61
|
+
if (!options?.clients?.Anthropic)
|
|
62
|
+
autoDiscover("@anthropic-ai/sdk", patchAnthropic);
|
|
63
|
+
if (!options?.clients?.OpenAI)
|
|
64
|
+
autoDiscover("openai", patchOpenAI);
|
|
65
|
+
flushTimer = setInterval(() => flush(), config.flushInterval);
|
|
66
|
+
process.on("beforeExit", () => flush());
|
|
67
|
+
initialized = true;
|
|
68
|
+
if (config.debug)
|
|
69
|
+
console.log("[tozil] Initialized");
|
|
70
|
+
}
|
|
71
|
+
export function setUser(userId) {
|
|
72
|
+
const store = als.getStore();
|
|
73
|
+
if (store)
|
|
74
|
+
store.userId = userId;
|
|
75
|
+
}
|
|
76
|
+
export function setEndpoint(endpoint) {
|
|
77
|
+
const store = als.getStore();
|
|
78
|
+
if (store)
|
|
79
|
+
store.endpoint = endpoint;
|
|
80
|
+
}
|
|
81
|
+
export function setMetadata(key, value) {
|
|
82
|
+
const store = als.getStore();
|
|
83
|
+
if (store)
|
|
84
|
+
store.metadata[key] = value;
|
|
85
|
+
}
|
|
86
|
+
export function withContext(context, fn) {
|
|
87
|
+
return als.run({ userId: context.userId || null, endpoint: context.endpoint || null, metadata: context.metadata || {} }, fn);
|
|
88
|
+
}
|
|
89
|
+
export function runWithContext(fn) {
|
|
90
|
+
return als.run({ userId: null, endpoint: null, metadata: {} }, fn);
|
|
91
|
+
}
|
|
92
|
+
export function shutdown() {
|
|
93
|
+
if (flushTimer)
|
|
94
|
+
clearInterval(flushTimer);
|
|
95
|
+
flush();
|
|
96
|
+
initialized = false;
|
|
97
|
+
config = null;
|
|
98
|
+
}
|
|
99
|
+
// --- Event tracking ---
|
|
100
|
+
function trackEvent(event) {
|
|
101
|
+
if (!config)
|
|
102
|
+
return;
|
|
103
|
+
eventBuffer.push(event);
|
|
104
|
+
if (config.debug) {
|
|
105
|
+
console.log(`[tozil] ${event.provider}/${event.model} | user=${event.user_id} endpoint=${event.endpoint} | in=${event.input_tokens} out=${event.output_tokens} | ${event.latency_ms}ms`);
|
|
106
|
+
}
|
|
107
|
+
if (eventBuffer.length >= config.maxBatchSize)
|
|
108
|
+
flush();
|
|
109
|
+
}
|
|
110
|
+
async function flush() {
|
|
111
|
+
if (!config || eventBuffer.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
const events = eventBuffer.splice(0);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${config.baseUrl}/events`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ events }),
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok && config.debug)
|
|
124
|
+
console.warn(`[tozil] Flush failed: ${res.status}`);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
if (config?.debug)
|
|
128
|
+
console.warn(`[tozil] Flush failed (silent):`, e.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// --- Context helper ---
|
|
132
|
+
function getContext() {
|
|
133
|
+
const store = als.getStore();
|
|
134
|
+
return {
|
|
135
|
+
userId: store?.userId || null,
|
|
136
|
+
endpoint: store?.endpoint || null,
|
|
137
|
+
metadata: store?.metadata || {},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// --- Monkey patching ---
|
|
141
|
+
// Strategy: create a temp instance to find the prototype, patch it there.
|
|
142
|
+
// This way all future instances are patched too.
|
|
143
|
+
function trackFromResult(provider, params, result, latency) {
|
|
144
|
+
try {
|
|
145
|
+
const ctx = getContext();
|
|
146
|
+
const isAnthropic = provider === "anthropic";
|
|
147
|
+
trackEvent({
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
user_id: ctx.userId,
|
|
150
|
+
model: params.model || result?.model || "unknown",
|
|
151
|
+
provider,
|
|
152
|
+
endpoint: ctx.endpoint,
|
|
153
|
+
input_tokens: isAnthropic ? (result?.usage?.input_tokens || 0) : (result?.usage?.prompt_tokens || 0),
|
|
154
|
+
output_tokens: isAnthropic ? (result?.usage?.output_tokens || 0) : (result?.usage?.completion_tokens || 0),
|
|
155
|
+
latency_ms: latency,
|
|
156
|
+
metadata: { ...ctx.metadata },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
function wrapStream(provider, params, stream, start) {
|
|
162
|
+
// Anthropic streams have a finalMessage or final usage event
|
|
163
|
+
// OpenAI streams have a .usage property after iteration
|
|
164
|
+
const origSymbol = Symbol.asyncIterator in stream ? Symbol.asyncIterator : null;
|
|
165
|
+
if (!origSymbol)
|
|
166
|
+
return stream;
|
|
167
|
+
let inputTokens = 0;
|
|
168
|
+
let outputTokens = 0;
|
|
169
|
+
const isAnthropic = provider === "anthropic";
|
|
170
|
+
const origIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
171
|
+
stream[Symbol.asyncIterator] = async function* () {
|
|
172
|
+
for await (const chunk of origIterator()) {
|
|
173
|
+
// Anthropic: message_delta event contains usage
|
|
174
|
+
if (isAnthropic && chunk?.type === "message_start" && chunk?.message?.usage) {
|
|
175
|
+
inputTokens = chunk.message.usage.input_tokens || 0;
|
|
176
|
+
}
|
|
177
|
+
if (isAnthropic && chunk?.type === "message_delta" && chunk?.usage) {
|
|
178
|
+
outputTokens = chunk.usage.output_tokens || 0;
|
|
179
|
+
}
|
|
180
|
+
// OpenAI: last chunk sometimes has usage
|
|
181
|
+
if (!isAnthropic && chunk?.usage) {
|
|
182
|
+
inputTokens = chunk.usage.prompt_tokens || 0;
|
|
183
|
+
outputTokens = chunk.usage.completion_tokens || 0;
|
|
184
|
+
}
|
|
185
|
+
yield chunk;
|
|
186
|
+
}
|
|
187
|
+
// After stream ends, track the event
|
|
188
|
+
const latency = Date.now() - start;
|
|
189
|
+
try {
|
|
190
|
+
const ctx = getContext();
|
|
191
|
+
trackEvent({
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
user_id: ctx.userId,
|
|
194
|
+
model: params.model || "unknown",
|
|
195
|
+
provider,
|
|
196
|
+
endpoint: ctx.endpoint,
|
|
197
|
+
input_tokens: inputTokens,
|
|
198
|
+
output_tokens: outputTokens,
|
|
199
|
+
latency_ms: latency,
|
|
200
|
+
metadata: { ...ctx.metadata },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch { }
|
|
204
|
+
// Anthropic streams have finalMessage — also check after iteration
|
|
205
|
+
if (isAnthropic && typeof stream.finalMessage === "function") {
|
|
206
|
+
try {
|
|
207
|
+
const msg = await stream.finalMessage();
|
|
208
|
+
if (msg?.usage && inputTokens === 0) {
|
|
209
|
+
trackFromResult(provider, params, msg, Date.now() - start);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return stream;
|
|
216
|
+
}
|
|
217
|
+
function patchAnthropic(Client) {
|
|
218
|
+
try {
|
|
219
|
+
const temp = new Client({ apiKey: "tozil-patch-probe" });
|
|
220
|
+
const messagesProto = Object.getPrototypeOf(temp.messages);
|
|
221
|
+
if (messagesProto && typeof messagesProto.create === "function" && !messagesProto._tozilPatched) {
|
|
222
|
+
const origCreate = messagesProto.create;
|
|
223
|
+
messagesProto.create = async function (...args) {
|
|
224
|
+
const params = args[0] || {};
|
|
225
|
+
// Streaming: when stream is true, the SDK returns a stream object
|
|
226
|
+
if (params.stream) {
|
|
227
|
+
try {
|
|
228
|
+
const start = Date.now();
|
|
229
|
+
const stream = await origCreate.apply(this, args);
|
|
230
|
+
return wrapStream("anthropic", params, stream, start);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
if (e?.status || e?.error)
|
|
234
|
+
throw e;
|
|
235
|
+
return origCreate.apply(this, args);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
const result = await origCreate.apply(this, args);
|
|
241
|
+
const latency = Date.now() - start;
|
|
242
|
+
trackFromResult("anthropic", params, result, latency);
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
if (e?.status || e?.error)
|
|
247
|
+
throw e;
|
|
248
|
+
return origCreate.apply(this, args);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
// Also patch .stream() if it exists (Anthropic SDK helper)
|
|
252
|
+
if (typeof messagesProto.stream === "function" && !messagesProto._tozilStreamPatched) {
|
|
253
|
+
const origStream = messagesProto.stream;
|
|
254
|
+
messagesProto.stream = async function (...args) {
|
|
255
|
+
try {
|
|
256
|
+
const start = Date.now();
|
|
257
|
+
const stream = await origStream.apply(this, args);
|
|
258
|
+
return wrapStream("anthropic", args[0] || {}, stream, start);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
if (e?.status || e?.error)
|
|
262
|
+
throw e;
|
|
263
|
+
return origStream.apply(this, args);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
messagesProto._tozilStreamPatched = true;
|
|
267
|
+
}
|
|
268
|
+
messagesProto._tozilPatched = true;
|
|
269
|
+
if (config?.debug)
|
|
270
|
+
console.log("[tozil] Patched Anthropic SDK");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
if (config?.debug)
|
|
275
|
+
console.log("[tozil] Anthropic SDK not found, skipping");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function patchOpenAI(Client) {
|
|
279
|
+
try {
|
|
280
|
+
const temp = new Client({ apiKey: "tozil-patch-probe" });
|
|
281
|
+
const completionsProto = Object.getPrototypeOf(temp.chat.completions);
|
|
282
|
+
if (completionsProto && typeof completionsProto.create === "function" && !completionsProto._tozilPatched) {
|
|
283
|
+
const origCreate = completionsProto.create;
|
|
284
|
+
completionsProto.create = async function (...args) {
|
|
285
|
+
const params = args[0] || {};
|
|
286
|
+
if (params.stream) {
|
|
287
|
+
try {
|
|
288
|
+
const start = Date.now();
|
|
289
|
+
const stream = await origCreate.apply(this, args);
|
|
290
|
+
return wrapStream("openai", params, stream, start);
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
if (e?.status || e?.error)
|
|
294
|
+
throw e;
|
|
295
|
+
return origCreate.apply(this, args);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const start = Date.now();
|
|
300
|
+
const result = await origCreate.apply(this, args);
|
|
301
|
+
const latency = Date.now() - start;
|
|
302
|
+
trackFromResult("openai", params, result, latency);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
if (e?.status || e?.error)
|
|
307
|
+
throw e;
|
|
308
|
+
return origCreate.apply(this, args);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
completionsProto._tozilPatched = true;
|
|
312
|
+
if (config?.debug)
|
|
313
|
+
console.log("[tozil] Patched OpenAI SDK");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
if (config?.debug)
|
|
318
|
+
console.log("[tozil] OpenAI SDK not found, skipping");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export default { init, setUser, setEndpoint, setMetadata, withContext, runWithContext, shutdown };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tozil/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Know what every AI user costs you — track AI costs per user with 2 lines of code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./express": {
|
|
14
|
+
"import": "./dist/express.js",
|
|
15
|
+
"types": "./dist/express.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"files": ["dist"],
|
|
24
|
+
"keywords": ["ai", "llm", "cost", "tracking", "anthropic", "openai", "claude", "gpt", "observability"],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://tozil.dev",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/tozil-dev/tozil"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.7.0",
|
|
36
|
+
"@types/node": "^22.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|