@databuddy/sdk 2.3.1 → 2.3.21

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.
@@ -1,12 +1,162 @@
1
1
  import { LanguageModelV2 } from '@ai-sdk/provider';
2
- import { db as Databuddy } from '../../node/index.mjs';
3
2
 
4
3
  /**
5
- * Wrap a Vercel language model with Databuddy middleware
6
- * @param model - The Vercel language model to wrap, if you are using the Vercel AI Gateway, please use the LanguageModelV2 type e.g. gateway('xai/grok-3') instead of the string type e.g. 'xai/grok-3'
7
- * @param buddy - The Databuddy instance to use
8
- * @returns The wrapped language model, can be used in e.g. generateText from the ai package
4
+ * Token usage from AI model calls
9
5
  */
10
- declare const wrapVercelLanguageModel: (model: LanguageModelV2, buddy: Databuddy) => LanguageModelV2;
6
+ interface TokenUsage {
7
+ inputTokens: number;
8
+ outputTokens: number;
9
+ totalTokens: number;
10
+ cachedInputTokens?: number;
11
+ }
12
+ /**
13
+ * Cost breakdown from TokenLens
14
+ */
15
+ interface TokenCost {
16
+ inputTokenCostUSD?: number;
17
+ outputTokenCostUSD?: number;
18
+ totalTokenCostUSD?: number;
19
+ }
20
+ /**
21
+ * Tool call information
22
+ */
23
+ interface ToolCallInfo {
24
+ toolCallCount: number;
25
+ toolResultCount: number;
26
+ toolCallNames: string[];
27
+ }
28
+ /**
29
+ * Error information for failed AI calls
30
+ */
31
+ interface AIError {
32
+ name: string;
33
+ message: string;
34
+ stack?: string;
35
+ }
36
+ /**
37
+ * Complete AI call log entry
38
+ */
39
+ interface AICall {
40
+ timestamp: Date;
41
+ type: "generate" | "stream";
42
+ model: string;
43
+ provider: string;
44
+ finishReason?: string;
45
+ usage: TokenUsage;
46
+ cost: TokenCost;
47
+ tools: ToolCallInfo;
48
+ error?: AIError;
49
+ durationMs: number;
50
+ }
51
+ /**
52
+ * Transport function for sending log entries
53
+ */
54
+ type Transport = (call: AICall) => Promise<void> | void;
55
+ /**
56
+ * Configuration options for Databuddy LLM tracking
57
+ */
58
+ interface DatabuddyLLMOptions {
59
+ /**
60
+ * API endpoint for sending logs (should include /llm path)
61
+ * @default process.env.DATABUDDY_API_URL or 'https://basket.databuddy.cc/llm'
62
+ */
63
+ apiUrl?: string;
64
+ /**
65
+ * API key for authentication
66
+ * @default process.env.DATABUDDY_API_KEY
67
+ */
68
+ apiKey?: string;
69
+ /**
70
+ * Client/Website ID for tracking
71
+ * @default process.env.DATABUDDY_CLIENT_ID
72
+ */
73
+ clientId?: string;
74
+ /**
75
+ * Custom transport function to send log entries
76
+ * If provided, overrides default HTTP transport
77
+ */
78
+ transport?: Transport;
79
+ /**
80
+ * Whether to compute costs using TokenLens
81
+ * @default true
82
+ */
83
+ computeCosts?: boolean;
84
+ /**
85
+ * Called on successful AI calls
86
+ */
87
+ onSuccess?: (call: AICall) => void;
88
+ /**
89
+ * Called on failed AI calls
90
+ */
91
+ onError?: (call: AICall) => void;
92
+ }
93
+ /**
94
+ * Configuration options for tracking individual models
95
+ */
96
+ interface TrackOptions {
97
+ /**
98
+ * Transport function to send log entries
99
+ * If not provided, uses the transport from DatabuddyLLM instance
100
+ */
101
+ transport?: Transport;
102
+ /**
103
+ * Whether to compute costs using TokenLens
104
+ * @default true
105
+ */
106
+ computeCosts?: boolean;
107
+ /**
108
+ * Called on successful AI calls
109
+ */
110
+ onSuccess?: (call: AICall) => void;
111
+ /**
112
+ * Called on failed AI calls
113
+ */
114
+ onError?: (call: AICall) => void;
115
+ }
116
+
117
+ /**
118
+ * Create a Databuddy LLM tracking instance
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { databuddyLLM } from "@databuddy/sdk/ai/vercel";
123
+ *
124
+ * // Use default endpoint (basket.databuddy.cc/llm)
125
+ * const { track } = databuddyLLM({
126
+ * apiKey: "your-api-key",
127
+ * });
128
+ *
129
+ * // Or override with custom endpoint
130
+ * const { track } = databuddyLLM({
131
+ * apiUrl: "https://custom.example.com/llm",
132
+ * apiKey: "your-api-key",
133
+ * });
134
+ *
135
+ * // Track a model
136
+ * const model = track(openai("gpt-4"));
137
+ *
138
+ * // Or with custom transport
139
+ * const { track } = databuddyLLM({
140
+ * transport: async (call) => console.log(call),
141
+ * });
142
+ * ```
143
+ */
144
+ declare const databuddyLLM: (options?: DatabuddyLLMOptions) => {
145
+ track: (model: LanguageModelV2, trackOptions?: TrackOptions) => LanguageModelV2;
146
+ };
147
+ /**
148
+ * Create an HTTP transport that sends logs to an API endpoint
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * import { databuddyLLM, httpTransport } from "@databuddy/sdk/ai/vercel";
153
+ *
154
+ * const { track } = databuddyLLM({
155
+ * transport: httpTransport("https://api.example.com/ai-logs", "client-id", "api-key"),
156
+ * });
157
+ * ```
158
+ */
159
+ declare const httpTransport: (url: string, clientId?: string, apiKey?: string) => Transport;
11
160
 
12
- export { wrapVercelLanguageModel as withBuddy };
161
+ export { databuddyLLM, httpTransport };
162
+ export type { AICall, AIError, DatabuddyLLMOptions, TokenCost, TokenUsage, ToolCallInfo, TrackOptions, Transport };
@@ -1,12 +1,162 @@
1
1
  import { LanguageModelV2 } from '@ai-sdk/provider';
2
- import { db as Databuddy } from '../../node/index.js';
3
2
 
4
3
  /**
5
- * Wrap a Vercel language model with Databuddy middleware
6
- * @param model - The Vercel language model to wrap, if you are using the Vercel AI Gateway, please use the LanguageModelV2 type e.g. gateway('xai/grok-3') instead of the string type e.g. 'xai/grok-3'
7
- * @param buddy - The Databuddy instance to use
8
- * @returns The wrapped language model, can be used in e.g. generateText from the ai package
4
+ * Token usage from AI model calls
9
5
  */
10
- declare const wrapVercelLanguageModel: (model: LanguageModelV2, buddy: Databuddy) => LanguageModelV2;
6
+ interface TokenUsage {
7
+ inputTokens: number;
8
+ outputTokens: number;
9
+ totalTokens: number;
10
+ cachedInputTokens?: number;
11
+ }
12
+ /**
13
+ * Cost breakdown from TokenLens
14
+ */
15
+ interface TokenCost {
16
+ inputTokenCostUSD?: number;
17
+ outputTokenCostUSD?: number;
18
+ totalTokenCostUSD?: number;
19
+ }
20
+ /**
21
+ * Tool call information
22
+ */
23
+ interface ToolCallInfo {
24
+ toolCallCount: number;
25
+ toolResultCount: number;
26
+ toolCallNames: string[];
27
+ }
28
+ /**
29
+ * Error information for failed AI calls
30
+ */
31
+ interface AIError {
32
+ name: string;
33
+ message: string;
34
+ stack?: string;
35
+ }
36
+ /**
37
+ * Complete AI call log entry
38
+ */
39
+ interface AICall {
40
+ timestamp: Date;
41
+ type: "generate" | "stream";
42
+ model: string;
43
+ provider: string;
44
+ finishReason?: string;
45
+ usage: TokenUsage;
46
+ cost: TokenCost;
47
+ tools: ToolCallInfo;
48
+ error?: AIError;
49
+ durationMs: number;
50
+ }
51
+ /**
52
+ * Transport function for sending log entries
53
+ */
54
+ type Transport = (call: AICall) => Promise<void> | void;
55
+ /**
56
+ * Configuration options for Databuddy LLM tracking
57
+ */
58
+ interface DatabuddyLLMOptions {
59
+ /**
60
+ * API endpoint for sending logs (should include /llm path)
61
+ * @default process.env.DATABUDDY_API_URL or 'https://basket.databuddy.cc/llm'
62
+ */
63
+ apiUrl?: string;
64
+ /**
65
+ * API key for authentication
66
+ * @default process.env.DATABUDDY_API_KEY
67
+ */
68
+ apiKey?: string;
69
+ /**
70
+ * Client/Website ID for tracking
71
+ * @default process.env.DATABUDDY_CLIENT_ID
72
+ */
73
+ clientId?: string;
74
+ /**
75
+ * Custom transport function to send log entries
76
+ * If provided, overrides default HTTP transport
77
+ */
78
+ transport?: Transport;
79
+ /**
80
+ * Whether to compute costs using TokenLens
81
+ * @default true
82
+ */
83
+ computeCosts?: boolean;
84
+ /**
85
+ * Called on successful AI calls
86
+ */
87
+ onSuccess?: (call: AICall) => void;
88
+ /**
89
+ * Called on failed AI calls
90
+ */
91
+ onError?: (call: AICall) => void;
92
+ }
93
+ /**
94
+ * Configuration options for tracking individual models
95
+ */
96
+ interface TrackOptions {
97
+ /**
98
+ * Transport function to send log entries
99
+ * If not provided, uses the transport from DatabuddyLLM instance
100
+ */
101
+ transport?: Transport;
102
+ /**
103
+ * Whether to compute costs using TokenLens
104
+ * @default true
105
+ */
106
+ computeCosts?: boolean;
107
+ /**
108
+ * Called on successful AI calls
109
+ */
110
+ onSuccess?: (call: AICall) => void;
111
+ /**
112
+ * Called on failed AI calls
113
+ */
114
+ onError?: (call: AICall) => void;
115
+ }
116
+
117
+ /**
118
+ * Create a Databuddy LLM tracking instance
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { databuddyLLM } from "@databuddy/sdk/ai/vercel";
123
+ *
124
+ * // Use default endpoint (basket.databuddy.cc/llm)
125
+ * const { track } = databuddyLLM({
126
+ * apiKey: "your-api-key",
127
+ * });
128
+ *
129
+ * // Or override with custom endpoint
130
+ * const { track } = databuddyLLM({
131
+ * apiUrl: "https://custom.example.com/llm",
132
+ * apiKey: "your-api-key",
133
+ * });
134
+ *
135
+ * // Track a model
136
+ * const model = track(openai("gpt-4"));
137
+ *
138
+ * // Or with custom transport
139
+ * const { track } = databuddyLLM({
140
+ * transport: async (call) => console.log(call),
141
+ * });
142
+ * ```
143
+ */
144
+ declare const databuddyLLM: (options?: DatabuddyLLMOptions) => {
145
+ track: (model: LanguageModelV2, trackOptions?: TrackOptions) => LanguageModelV2;
146
+ };
147
+ /**
148
+ * Create an HTTP transport that sends logs to an API endpoint
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * import { databuddyLLM, httpTransport } from "@databuddy/sdk/ai/vercel";
153
+ *
154
+ * const { track } = databuddyLLM({
155
+ * transport: httpTransport("https://api.example.com/ai-logs", "client-id", "api-key"),
156
+ * });
157
+ * ```
158
+ */
159
+ declare const httpTransport: (url: string, clientId?: string, apiKey?: string) => Transport;
11
160
 
12
- export { wrapVercelLanguageModel as withBuddy };
161
+ export { databuddyLLM, httpTransport };
162
+ export type { AICall, AIError, DatabuddyLLMOptions, TokenCost, TokenUsage, ToolCallInfo, TrackOptions, Transport };
@@ -1,43 +1,238 @@
1
1
  import { wrapLanguageModel } from 'ai';
2
- import { computeCostUSD } from 'tokenlens';
3
2
 
4
- const buddyWare = (buddy) => {
3
+ const extractToolInfo = (content) => {
4
+ const toolCalls = content.filter((part) => part.type === "tool-call");
5
+ const toolResults = content.filter((part) => part.type === "tool-result");
6
+ const toolCallNames = [
7
+ ...new Set(
8
+ toolCalls.map((c) => c.toolName).filter((name) => Boolean(name))
9
+ )
10
+ ];
11
+ return {
12
+ toolCallCount: toolCalls.length,
13
+ toolResultCount: toolResults.length,
14
+ toolCallNames
15
+ };
16
+ };
17
+ const computeCosts = async (modelId, provider, usage) => {
18
+ try {
19
+ const { computeCostUSD } = await import('tokenlens');
20
+ const result = await computeCostUSD({
21
+ modelId,
22
+ provider,
23
+ usage: {
24
+ input_tokens: usage.inputTokens,
25
+ output_tokens: usage.outputTokens
26
+ }
27
+ });
28
+ return {
29
+ inputTokenCostUSD: result.inputTokenCostUSD,
30
+ outputTokenCostUSD: result.outputTokenCostUSD,
31
+ totalTokenCostUSD: result.totalTokenCostUSD
32
+ };
33
+ } catch {
34
+ return {};
35
+ }
36
+ };
37
+ const createDefaultTransport = (apiUrl, clientId, apiKey) => {
38
+ return async (call) => {
39
+ const headers = {
40
+ "Content-Type": "application/json"
41
+ };
42
+ if (apiKey) {
43
+ headers.Authorization = `Bearer ${apiKey}`;
44
+ }
45
+ if (clientId) {
46
+ headers["databuddy-client-id"] = clientId;
47
+ }
48
+ const response = await fetch(apiUrl, {
49
+ method: "POST",
50
+ headers,
51
+ body: JSON.stringify(call)
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(
55
+ `Failed to send AI log: ${response.status} ${response.statusText}`
56
+ );
57
+ }
58
+ };
59
+ };
60
+ const createMiddleware = (transport, options = {}) => {
61
+ const { computeCosts: shouldComputeCosts = true } = options;
5
62
  return {
6
63
  wrapGenerate: async ({ doGenerate, model }) => {
7
- const result = await doGenerate();
8
- const isToolCall = (part) => part.type === "tool-call";
9
- const isToolResult = (part) => part.type === "tool-result";
10
- const toolCalls = result.content.filter(isToolCall);
11
- const toolResults = result.content.filter(isToolResult);
12
- const toolCallNames = Array.from(
13
- new Set(toolCalls.map((c) => c.toolName))
64
+ const startTime = Date.now();
65
+ try {
66
+ const result = await doGenerate();
67
+ const durationMs = Date.now() - startTime;
68
+ const tools = extractToolInfo(
69
+ result.content
70
+ );
71
+ const inputTokens = result.usage.inputTokens ?? 0;
72
+ const outputTokens = result.usage.outputTokens ?? 0;
73
+ const totalTokens = result.usage.totalTokens ?? inputTokens + outputTokens;
74
+ const cachedInputTokens = result.usage.cachedInputTokens;
75
+ const cost = shouldComputeCosts && (inputTokens > 0 || outputTokens > 0) ? await computeCosts(model.modelId, model.provider, {
76
+ inputTokens,
77
+ outputTokens
78
+ }) : {};
79
+ const call = {
80
+ timestamp: /* @__PURE__ */ new Date(),
81
+ type: "generate",
82
+ model: model.modelId,
83
+ provider: model.provider,
84
+ finishReason: result.finishReason,
85
+ usage: {
86
+ inputTokens,
87
+ outputTokens,
88
+ totalTokens,
89
+ cachedInputTokens
90
+ },
91
+ cost,
92
+ tools,
93
+ durationMs
94
+ };
95
+ const effectiveTransport = options.transport ?? transport;
96
+ const transportResult = effectiveTransport(call);
97
+ if (transportResult instanceof Promise) {
98
+ transportResult.catch(() => {
99
+ });
100
+ }
101
+ options.onSuccess?.(call);
102
+ return result;
103
+ } catch (error) {
104
+ const durationMs = Date.now() - startTime;
105
+ const call = {
106
+ timestamp: /* @__PURE__ */ new Date(),
107
+ type: "generate",
108
+ model: model.modelId,
109
+ provider: model.provider,
110
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
111
+ cost: {},
112
+ tools: { toolCallCount: 0, toolResultCount: 0, toolCallNames: [] },
113
+ durationMs,
114
+ error: {
115
+ name: error instanceof Error ? error.name : "UnknownError",
116
+ message: error instanceof Error ? error.message : String(error),
117
+ stack: error instanceof Error ? error.stack : void 0
118
+ }
119
+ };
120
+ const effectiveTransport = options.transport ?? transport;
121
+ const transportResult = effectiveTransport(call);
122
+ if (transportResult instanceof Promise) {
123
+ transportResult.catch(() => {
124
+ });
125
+ }
126
+ options.onError?.(call);
127
+ throw error;
128
+ }
129
+ },
130
+ wrapStream: async ({ doStream, model }) => {
131
+ const startTime = Date.now();
132
+ try {
133
+ const { stream, ...rest } = await doStream();
134
+ const durationMs = Date.now() - startTime;
135
+ const call = {
136
+ timestamp: /* @__PURE__ */ new Date(),
137
+ type: "stream",
138
+ model: model.modelId,
139
+ provider: model.provider,
140
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
141
+ cost: {},
142
+ tools: { toolCallCount: 0, toolResultCount: 0, toolCallNames: [] },
143
+ durationMs
144
+ };
145
+ const effectiveTransport = options.transport ?? transport;
146
+ const transportResult = effectiveTransport(call);
147
+ if (transportResult instanceof Promise) {
148
+ transportResult.catch(() => {
149
+ });
150
+ }
151
+ options.onSuccess?.(call);
152
+ return { stream, ...rest };
153
+ } catch (error) {
154
+ const durationMs = Date.now() - startTime;
155
+ const call = {
156
+ timestamp: /* @__PURE__ */ new Date(),
157
+ type: "stream",
158
+ model: model.modelId,
159
+ provider: model.provider,
160
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
161
+ cost: {},
162
+ tools: { toolCallCount: 0, toolResultCount: 0, toolCallNames: [] },
163
+ durationMs,
164
+ error: {
165
+ name: error instanceof Error ? error.name : "UnknownError",
166
+ message: error instanceof Error ? error.message : String(error),
167
+ stack: error instanceof Error ? error.stack : void 0
168
+ }
169
+ };
170
+ const effectiveTransport = options.transport ?? transport;
171
+ const transportResult = effectiveTransport(call);
172
+ if (transportResult instanceof Promise) {
173
+ transportResult.catch(() => {
174
+ });
175
+ }
176
+ options.onError?.(call);
177
+ throw error;
178
+ }
179
+ }
180
+ };
181
+ };
182
+ const databuddyLLM = (options = {}) => {
183
+ const {
184
+ apiUrl,
185
+ apiKey,
186
+ clientId,
187
+ transport: customTransport,
188
+ computeCosts: defaultComputeCosts = true,
189
+ onSuccess: defaultOnSuccess,
190
+ onError: defaultOnError
191
+ } = options;
192
+ let transport;
193
+ if (customTransport) {
194
+ transport = customTransport;
195
+ } else {
196
+ const endpoint = apiUrl ?? process.env.DATABUDDY_API_URL ?? "https://basket.databuddy.cc/llm";
197
+ const client = clientId ?? process.env.DATABUDDY_CLIENT_ID;
198
+ const key = apiKey ?? process.env.DATABUDDY_API_KEY;
199
+ transport = createDefaultTransport(endpoint, client, key);
200
+ }
201
+ const track = (model, trackOptions = {}) => {
202
+ return wrapLanguageModel({
203
+ model,
204
+ middleware: createMiddleware(transport, {
205
+ computeCosts: trackOptions.computeCosts ?? defaultComputeCosts,
206
+ onSuccess: trackOptions.onSuccess ?? defaultOnSuccess,
207
+ onError: trackOptions.onError ?? defaultOnError,
208
+ transport: trackOptions.transport
209
+ })
210
+ });
211
+ };
212
+ return { track };
213
+ };
214
+ const httpTransport = (url, clientId, apiKey) => {
215
+ return async (call) => {
216
+ const headers = {
217
+ "Content-Type": "application/json"
218
+ };
219
+ if (apiKey) {
220
+ headers.Authorization = `Bearer ${apiKey}`;
221
+ }
222
+ if (clientId) {
223
+ headers["databuddy-client-id"] = clientId;
224
+ }
225
+ const response = await fetch(url, {
226
+ method: "POST",
227
+ headers,
228
+ body: JSON.stringify(call)
229
+ });
230
+ if (!response.ok) {
231
+ throw new Error(
232
+ `Failed to send AI log: ${response.status} ${response.statusText}`
14
233
  );
15
- const costs = await computeCostUSD({
16
- modelId: model.modelId,
17
- provider: model.provider,
18
- usage: result.usage
19
- });
20
- const payload = {
21
- inputTokens: result.usage.inputTokens,
22
- outputTokens: result.usage.outputTokens,
23
- totalTokens: result.usage.totalTokens,
24
- cachedInputTokens: result.usage.cachedInputTokens,
25
- finishReason: result.finishReason,
26
- toolCallCount: toolCalls.length,
27
- toolResultCount: toolResults.length,
28
- inputTokenCostUSD: costs.inputTokenCostUSD,
29
- outputTokenCostUSD: costs.outputTokenCostUSD,
30
- totalTokenCostUSD: costs.totalTokenCostUSD,
31
- toolCallNames
32
- };
33
- console.log("payload", payload);
34
- return result;
35
234
  }
36
235
  };
37
236
  };
38
- const wrapVercelLanguageModel = (model, buddy) => wrapLanguageModel({
39
- model,
40
- middleware: buddyWare()
41
- });
42
237
 
43
- export { wrapVercelLanguageModel as withBuddy };
238
+ export { databuddyLLM, httpTransport };