@scrawn/core 0.0.3 → 0.0.7
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/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/core/auth/apiKeyAuth.d.ts +58 -0
- package/dist/core/auth/apiKeyAuth.d.ts.map +1 -0
- package/dist/core/auth/apiKeyAuth.js +66 -0
- package/dist/core/auth/apiKeyAuth.js.map +1 -0
- package/dist/core/auth/baseAuth.d.ts +70 -0
- package/dist/core/auth/baseAuth.d.ts.map +1 -0
- package/dist/core/auth/baseAuth.js +22 -0
- package/dist/core/auth/baseAuth.js.map +1 -0
- package/dist/core/errors/index.d.ts +192 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +280 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/grpc/callContext.d.ts +18 -0
- package/dist/core/grpc/callContext.d.ts.map +1 -0
- package/dist/core/grpc/callContext.js +35 -0
- package/dist/core/grpc/callContext.js.map +1 -0
- package/dist/core/grpc/client.d.ts +16 -0
- package/dist/core/grpc/client.d.ts.map +1 -0
- package/dist/core/grpc/client.js +30 -0
- package/dist/core/grpc/client.js.map +1 -0
- package/dist/core/grpc/index.d.ts +14 -0
- package/dist/core/grpc/index.d.ts.map +1 -0
- package/dist/core/grpc/index.js +13 -0
- package/dist/core/grpc/index.js.map +1 -0
- package/dist/core/grpc/requestBuilder.d.ts +15 -0
- package/dist/core/grpc/requestBuilder.d.ts.map +1 -0
- package/dist/core/grpc/requestBuilder.js +56 -0
- package/dist/core/grpc/requestBuilder.js.map +1 -0
- package/dist/core/grpc/streamRequestBuilder.d.ts +13 -0
- package/dist/core/grpc/streamRequestBuilder.d.ts.map +1 -0
- package/dist/core/grpc/streamRequestBuilder.js +60 -0
- package/dist/core/grpc/streamRequestBuilder.js.map +1 -0
- package/dist/core/grpc/types.d.ts +7 -0
- package/dist/core/grpc/types.d.ts.map +1 -0
- package/dist/core/grpc/types.js +2 -0
- package/dist/core/grpc/types.js.map +1 -0
- package/dist/core/pricing/builders.d.ts +157 -0
- package/dist/core/pricing/builders.d.ts.map +1 -0
- package/dist/core/pricing/builders.js +218 -0
- package/dist/core/pricing/builders.js.map +1 -0
- package/dist/core/pricing/index.d.ts +30 -0
- package/dist/core/pricing/index.d.ts.map +1 -0
- package/dist/core/pricing/index.js +32 -0
- package/dist/core/pricing/index.js.map +1 -0
- package/dist/core/pricing/resolve.d.ts +39 -0
- package/dist/core/pricing/resolve.d.ts.map +1 -0
- package/dist/core/pricing/resolve.js +50 -0
- package/dist/core/pricing/resolve.js.map +1 -0
- package/dist/core/pricing/serialize.d.ts +55 -0
- package/dist/core/pricing/serialize.d.ts.map +1 -0
- package/dist/core/pricing/serialize.js +127 -0
- package/dist/core/pricing/serialize.js.map +1 -0
- package/dist/core/pricing/types.d.ts +122 -0
- package/dist/core/pricing/types.d.ts.map +1 -0
- package/dist/core/pricing/types.js +17 -0
- package/dist/core/pricing/types.js.map +1 -0
- package/dist/core/pricing/validate.d.ts +56 -0
- package/dist/core/pricing/validate.d.ts.map +1 -0
- package/dist/core/pricing/validate.js +162 -0
- package/dist/core/pricing/validate.js.map +1 -0
- package/dist/core/scrawn.d.ts +414 -0
- package/dist/core/scrawn.d.ts.map +1 -0
- package/dist/core/scrawn.js +822 -0
- package/dist/core/scrawn.js.map +1 -0
- package/dist/core/types/auth.d.ts +31 -0
- package/dist/core/types/auth.d.ts.map +1 -0
- package/dist/core/types/auth.js +2 -0
- package/dist/core/types/auth.js.map +1 -0
- package/dist/core/types/event.d.ts +320 -0
- package/dist/core/types/event.d.ts.map +1 -0
- package/dist/core/types/event.js +155 -0
- package/dist/core/types/event.js.map +1 -0
- package/dist/gen/auth/v1/auth_grpc_pb.d.ts +3 -0
- package/dist/gen/auth/v1/auth_pb.d.ts +65 -0
- package/dist/gen/data/v1/data_grpc_pb.d.ts +5 -0
- package/dist/gen/data/v1/data_pb.d.ts +254 -0
- package/dist/gen/event/v1/event_grpc_pb.d.ts +3 -0
- package/dist/gen/event/v1/event_pb.d.ts +342 -0
- package/dist/gen/event/v1/event_pb.js +573 -117
- package/dist/gen/payment/v1/payment_grpc_pb.d.ts +3 -0
- package/dist/gen/payment/v1/payment_pb.d.ts +45 -0
- package/dist/gen/query/v1/query_grpc_pb.d.ts +5 -0
- package/dist/gen/query/v1/query_pb.d.ts +381 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/forkAsyncIterable.d.ts +13 -0
- package/dist/utils/forkAsyncIterable.d.ts.map +1 -0
- package/dist/utils/forkAsyncIterable.js +78 -0
- package/dist/utils/forkAsyncIterable.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +62 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/pathMatcher.d.ts +25 -0
- package/dist/utils/pathMatcher.d.ts.map +1 -0
- package/dist/utils/pathMatcher.js +46 -0
- package/dist/utils/pathMatcher.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { ApiKeyAuth } from "./auth/apiKeyAuth.js";
|
|
2
|
+
import { ScrawnLogger } from "../utils/logger.js";
|
|
3
|
+
import { matchPath } from "../utils/pathMatcher.js";
|
|
4
|
+
import { forkAsyncIterable } from "../utils/forkAsyncIterable.js";
|
|
5
|
+
import { EventPayloadSchema, AITokenUsagePayloadSchema, } from "./types/event.js";
|
|
6
|
+
import { GrpcClient } from "./grpc/index.js";
|
|
7
|
+
import { EventServiceClient } from "../gen/event/v1/event_grpc_pb.js";
|
|
8
|
+
import { RegisterEventRequest, StreamEventRequest, EventType, BasicUsageType, BasicUsage, AITokenUsage, } from "../gen/event/v1/event_pb.js";
|
|
9
|
+
import { PaymentServiceClient } from "../gen/payment/v1/payment_grpc_pb.js";
|
|
10
|
+
import { CreateCheckoutLinkRequest, } from "../gen/payment/v1/payment_pb.js";
|
|
11
|
+
import { ScrawnConfigError, ScrawnValidationError, convertGrpcError, isScrawnError, isRetryableError, } from "./errors/index.js";
|
|
12
|
+
import { serializeExpr, resolveTokens, prettyPrintExpr, tag as _tag } from "./pricing/index.js";
|
|
13
|
+
import { ScrawnConfig } from "../config.js";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
const log = new ScrawnLogger("Scrawn");
|
|
16
|
+
/**
|
|
17
|
+
* Main SDK class for Scrawn billing infrastructure.
|
|
18
|
+
*
|
|
19
|
+
* Manages authentication, event tracking, and credential caching.
|
|
20
|
+
* All event consumption methods are available directly on the SDK instance.
|
|
21
|
+
*
|
|
22
|
+
* @typeParam TTags - Union of valid tag names for compile-time type checking
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { createScrawn } from '@scrawn/core';
|
|
27
|
+
*
|
|
28
|
+
* const biller = createScrawn({
|
|
29
|
+
* apiKey: process.env.SCRAWN_KEY,
|
|
30
|
+
* baseURL: 'http://localhost:8069',
|
|
31
|
+
* tags: ["PREMIUM_CALL", "EXTRA_FEE"] as const,
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Tags are compile-time checked
|
|
35
|
+
* biller.basicUsageEventConsumer({ userId: 'u123', debitTag: 'PREMIUM_FEATURE' });
|
|
36
|
+
* // biller.basicUsageEventConsumer({ userId: 'u123', debitTag: 'UNKNOWN' }); // Type error!
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class Scrawn {
|
|
40
|
+
/** Public access to the gRPC client for use by other packages (e.g. @scrawn/analytics) */
|
|
41
|
+
get grpc() {
|
|
42
|
+
return this.grpcClient;
|
|
43
|
+
}
|
|
44
|
+
/** API key used for authorizing gRPC calls */
|
|
45
|
+
get apikey() {
|
|
46
|
+
return this.apiKey;
|
|
47
|
+
}
|
|
48
|
+
sleep(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
backoffMs(attempt) {
|
|
52
|
+
return Math.min(1000 * Math.pow(2, attempt), 8000);
|
|
53
|
+
}
|
|
54
|
+
notifyEventConsumerError(error, onError) {
|
|
55
|
+
const converted = isScrawnError(error) ? error : convertGrpcError(error);
|
|
56
|
+
onError?.(converted);
|
|
57
|
+
return converted;
|
|
58
|
+
}
|
|
59
|
+
notifyValidationError(error, onError) {
|
|
60
|
+
onError?.(error);
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new Scrawn SDK instance.
|
|
65
|
+
*
|
|
66
|
+
* @param config - Configuration object
|
|
67
|
+
* @param config.apiKey - Your Scrawn API key for authentication
|
|
68
|
+
* @param config.baseURL - Base URL for the Scrawn API (e.g., 'https://api.scrawn.dev')
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const scrawn = new Scrawn({
|
|
73
|
+
* apiKey: 'sk_test_...',
|
|
74
|
+
* baseURL: 'https://api.scrawn.dev'
|
|
75
|
+
* });
|
|
76
|
+
* await scrawn.init();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
constructor(config) {
|
|
80
|
+
/** Map of authentication method names to their implementations */
|
|
81
|
+
this.authMethods = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Cache of credentials keyed by auth method name for performance.
|
|
84
|
+
* Keys are restricted to registered auth method names only.
|
|
85
|
+
*/
|
|
86
|
+
this.credCache = new Map();
|
|
87
|
+
try {
|
|
88
|
+
// Validate configuration
|
|
89
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
90
|
+
throw new ScrawnConfigError("API key is required and must be a string", {
|
|
91
|
+
details: { provided: typeof config.apiKey },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!config.baseURL || typeof config.baseURL !== "string") {
|
|
95
|
+
throw new ScrawnConfigError("baseURL is required and must be a string", {
|
|
96
|
+
details: { provided: typeof config.baseURL },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.apiKey = config.apiKey;
|
|
100
|
+
this.retryCount = config.retryCount ?? 2;
|
|
101
|
+
this.grpcClient = new GrpcClient(this.parseURLToTarget(config.baseURL), { secure: config.secure ?? true, credentials: config.credentials });
|
|
102
|
+
this.registerAuthMethod("api", new ApiKeyAuth(this.apiKey));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
log.error("Failed to initialize Scrawn SDK");
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
parseURLToTarget(baseURL) {
|
|
110
|
+
if (baseURL.includes("://")) {
|
|
111
|
+
const url = new URL(baseURL);
|
|
112
|
+
return `${url.hostname}:${url.port || ScrawnConfig.grpc.defaultPort}`;
|
|
113
|
+
}
|
|
114
|
+
return baseURL.includes(":")
|
|
115
|
+
? baseURL
|
|
116
|
+
: `${baseURL}:${ScrawnConfig.grpc.defaultPort}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create a type-safe tag reference.
|
|
120
|
+
*
|
|
121
|
+
* Only tag names known to this biller instance are accepted at compile time.
|
|
122
|
+
* Tag values are resolved to cent amounts by the backend at runtime.
|
|
123
|
+
*
|
|
124
|
+
* @param name - The tag name (must be one of the known tags for this instance)
|
|
125
|
+
* @returns A TagExpr referencing the named tag
|
|
126
|
+
* @throws PricingExpressionError at runtime if name format is invalid
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const expr = mul(biller.tag("PREMIUM_CALL"), 3);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
tag(name) {
|
|
134
|
+
return _tag(name);
|
|
135
|
+
}
|
|
136
|
+
expr(value) {
|
|
137
|
+
return {
|
|
138
|
+
_expr: typeof value === "string"
|
|
139
|
+
? { kind: "exprRef", name: value }
|
|
140
|
+
: value,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Register an authentication method with the SDK.
|
|
145
|
+
*
|
|
146
|
+
* Auth methods handle credential management and can be shared across multiple event types.
|
|
147
|
+
* Only auth method names defined in AuthRegistry are allowed.
|
|
148
|
+
*
|
|
149
|
+
* @param name - Unique identifier for this auth method (must be in AuthRegistry)
|
|
150
|
+
* @param auth - Instance of an AuthBase implementation
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* scrawn.registerAuthMethod('api', new ApiKeyAuth('sk_test_...'));
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
registerAuthMethod(name, auth) {
|
|
158
|
+
this.authMethods.set(name, auth);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get credentials for a specific authentication method.
|
|
162
|
+
*
|
|
163
|
+
* Credentials are cached after the first fetch for performance.
|
|
164
|
+
* Subsequent calls return the cached value without re-fetching.
|
|
165
|
+
* Only auth method names defined in AuthRegistry are allowed.
|
|
166
|
+
*
|
|
167
|
+
* @param authMethodName - Name of the auth method to get credentials for (must be in AuthRegistry)
|
|
168
|
+
* @returns A promise that resolves to the credentials object
|
|
169
|
+
* @throws Error if the auth method is not registered
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const creds = await scrawn.getCredsFor('api');
|
|
174
|
+
* // { apiKey: 'sk_test_...' }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
async getCredsFor(authMethodName) {
|
|
178
|
+
// Check cache first
|
|
179
|
+
if (this.credCache.has(authMethodName)) {
|
|
180
|
+
return this.credCache.get(authMethodName);
|
|
181
|
+
}
|
|
182
|
+
// Get fresh creds from auth method
|
|
183
|
+
const auth = this.authMethods.get(authMethodName);
|
|
184
|
+
if (!auth) {
|
|
185
|
+
throw new ScrawnConfigError(`No auth method registered: ${authMethodName}`, {
|
|
186
|
+
details: { requestedMethod: authMethodName },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const creds = await auth.getCreds();
|
|
190
|
+
this.credCache.set(authMethodName, creds);
|
|
191
|
+
return creds;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Track a basic usage event.
|
|
195
|
+
*
|
|
196
|
+
* Records basic usage to the Scrawn backend for billing tracking.
|
|
197
|
+
* The event is authenticated using the API key provided during SDK initialization.
|
|
198
|
+
*
|
|
199
|
+
* @param payload - The usage data to track
|
|
200
|
+
* @param payload.userId - Unique identifier of the user making the call
|
|
201
|
+
* @param payload.debitAmount - (Optional) Direct amount in cents to debit from the user's account
|
|
202
|
+
* @param payload.debitTag - (Optional) Named price tag for backend-managed pricing
|
|
203
|
+
* @param payload.debitExpr - (Optional) Pricing expression for complex calculations
|
|
204
|
+
* @param payload.metadata - (Optional) Arbitrary metadata to associate with the event
|
|
205
|
+
* @param options - Optional configuration
|
|
206
|
+
* @param options.eventId - (Optional) Override the auto-generated event ID
|
|
207
|
+
* @param options.onError - Optional callback for handling validation or gRPC errors
|
|
208
|
+
* @returns A promise that resolves when the event is tracked or returns early on error
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* import { add, mul, tag } from '@scrawn/core';
|
|
213
|
+
*
|
|
214
|
+
* // Using direct amount (500 cents = $5.00)
|
|
215
|
+
* await scrawn.basicUsageEventConsumer({
|
|
216
|
+
* userId: 'user_abc123',
|
|
217
|
+
* debitAmount: 500
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* // Using price tag
|
|
221
|
+
* await scrawn.basicUsageEventConsumer({
|
|
222
|
+
* userId: 'user_abc123',
|
|
223
|
+
* debitTag: 'PREMIUM_FEATURE'
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* // Using pricing expression: (PREMIUM_CALL * 3) + EXTRA_FEE + 250 cents
|
|
227
|
+
* await scrawn.basicUsageEventConsumer({
|
|
228
|
+
* userId: 'user_abc123',
|
|
229
|
+
* debitExpr: add(mul(tag('PREMIUM_CALL'), 3), tag('EXTRA_FEE'), 250)
|
|
230
|
+
* });
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
async basicUsageEventConsumer(payload, options) {
|
|
234
|
+
const rawPayload = {
|
|
235
|
+
userId: payload.userId,
|
|
236
|
+
debitAmount: payload.debitAmount,
|
|
237
|
+
debitTag: payload.debitTag,
|
|
238
|
+
debitExpr: payload.debitExpr?._expr,
|
|
239
|
+
metadata: payload.metadata,
|
|
240
|
+
};
|
|
241
|
+
const validationResult = EventPayloadSchema.safeParse(rawPayload);
|
|
242
|
+
if (!validationResult.success) {
|
|
243
|
+
const errors = validationResult.error.issues
|
|
244
|
+
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
245
|
+
.join(", ");
|
|
246
|
+
log.error(`Invalid payload for basicUsageEventConsumer: ${errors}`);
|
|
247
|
+
const error = new ScrawnValidationError("Payload validation failed", {
|
|
248
|
+
details: {
|
|
249
|
+
errors: validationResult.error.issues.map((e) => ({
|
|
250
|
+
field: e.path.join("."),
|
|
251
|
+
message: e.message,
|
|
252
|
+
})),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
this.notifyValidationError(error, options?.onError);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Fixed identity for this event — survives retries
|
|
259
|
+
const eventId = options?.eventId ?? randomUUID();
|
|
260
|
+
const idempotencyKey = randomUUID();
|
|
261
|
+
const attempt = () => this.consumeEvent(validationResult.data, "api", "RAW", eventId, idempotencyKey);
|
|
262
|
+
try {
|
|
263
|
+
await attempt();
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
log.error(`Failed to track basicUsageEventConsumer event: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
267
|
+
if (options?.onError) {
|
|
268
|
+
const converted = isScrawnError(error)
|
|
269
|
+
? error
|
|
270
|
+
: convertGrpcError(error);
|
|
271
|
+
const retryContext = {
|
|
272
|
+
retry: async () => {
|
|
273
|
+
try {
|
|
274
|
+
await attempt();
|
|
275
|
+
}
|
|
276
|
+
catch (retryError) {
|
|
277
|
+
const convertedRetry = isScrawnError(retryError)
|
|
278
|
+
? retryError
|
|
279
|
+
: convertGrpcError(retryError);
|
|
280
|
+
options.onError(convertedRetry, retryContext);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
options.onError(converted, retryContext);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Create an Express-compatible middleware for tracking API endpoint usage.
|
|
291
|
+
*
|
|
292
|
+
* This middleware automatically tracks requests to your API endpoints for billing purposes.
|
|
293
|
+
* You provide an extractor function that determines the userId and debit info (amount or tag) from each request.
|
|
294
|
+
* Optionally, you can provide a whitelist array to only track specific endpoints,
|
|
295
|
+
* or a blacklist array to exclude specific endpoints from tracking.
|
|
296
|
+
*
|
|
297
|
+
* The middleware is framework-agnostic and works with Express, Fastify, and similar frameworks.
|
|
298
|
+
*
|
|
299
|
+
* @param config - Configuration object for the middleware
|
|
300
|
+
* @param config.extractor - Function that extracts userId and debitAmount from the request. Return null to skip tracking.
|
|
301
|
+
* @param config.whitelist - Optional array of endpoint patterns to track. Supports wildcards:
|
|
302
|
+
* - Exact match: /api/users
|
|
303
|
+
* - Single segment (*): /api/* matches /api/users but not /api/users/123
|
|
304
|
+
* - Multi-segment (**): /api/** matches any path starting with /api/
|
|
305
|
+
* - Mixed: /api/star/profile, **.php
|
|
306
|
+
* Takes precedence over blacklist. If omitted, all requests will be tracked.
|
|
307
|
+
* @param config.blacklist - Optional array of endpoint patterns to exclude. Same wildcard support as whitelist.
|
|
308
|
+
* Only applies to endpoints not in the whitelist.
|
|
309
|
+
* @param config.onError - Optional callback for handling validation or gRPC errors
|
|
310
|
+
*
|
|
311
|
+
* @returns Express-compatible middleware function
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* // Track all endpoints
|
|
316
|
+
* app.use(scrawn.middlewareEventConsumer({
|
|
317
|
+
* extractor: (req) => ({
|
|
318
|
+
* userId: req.user.id,
|
|
319
|
+
* debitAmount: 1
|
|
320
|
+
* })
|
|
321
|
+
* }));
|
|
322
|
+
*
|
|
323
|
+
* // Track only specific endpoints with wildcards
|
|
324
|
+
* app.use(scrawn.middlewareEventConsumer({
|
|
325
|
+
* extractor: (req) => ({
|
|
326
|
+
* userId: req.headers['x-user-id'] as string,
|
|
327
|
+
* debitAmount: req.body.tokens || 1
|
|
328
|
+
* }),
|
|
329
|
+
* whitelist: ['/api/generate', '/api/analyze', '/api/v1/*']
|
|
330
|
+
* }));
|
|
331
|
+
*
|
|
332
|
+
* // Exclude specific endpoints from tracking
|
|
333
|
+
* app.use(scrawn.middlewareEventConsumer({
|
|
334
|
+
* extractor: (req) => ({
|
|
335
|
+
* userId: req.user.id,
|
|
336
|
+
* debitAmount: 1
|
|
337
|
+
* }),
|
|
338
|
+
* blacklist: ['/health', '/api/collect-payment', '/internal/**', '**.tmp']
|
|
339
|
+
* }));
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
middlewareEventConsumer(config) {
|
|
343
|
+
return async (req, res, next) => {
|
|
344
|
+
try {
|
|
345
|
+
const requestPath = req.path || req.url || "";
|
|
346
|
+
// Check whitelist first (takes precedence)
|
|
347
|
+
if (config.whitelist && config.whitelist.length > 0) {
|
|
348
|
+
const isWhitelisted = config.whitelist.some((pattern) => matchPath(requestPath, pattern));
|
|
349
|
+
if (!isWhitelisted) {
|
|
350
|
+
return next();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Then check blacklist
|
|
354
|
+
if (config.blacklist && config.blacklist.length > 0) {
|
|
355
|
+
const isBlacklisted = config.blacklist.some((pattern) => matchPath(requestPath, pattern));
|
|
356
|
+
if (isBlacklisted) {
|
|
357
|
+
return next();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const extractedPayload = await config.extractor(req);
|
|
361
|
+
// If extractor returns null, skip tracking
|
|
362
|
+
if (extractedPayload === null) {
|
|
363
|
+
log.warn(`Extractor returned null for path: ${requestPath}. Skipping event tracking.`);
|
|
364
|
+
return next();
|
|
365
|
+
}
|
|
366
|
+
const rawPayload = {
|
|
367
|
+
userId: extractedPayload.userId,
|
|
368
|
+
debitAmount: extractedPayload.debitAmount,
|
|
369
|
+
debitTag: extractedPayload.debitTag,
|
|
370
|
+
debitExpr: extractedPayload.debitExpr?._expr,
|
|
371
|
+
metadata: extractedPayload.metadata,
|
|
372
|
+
};
|
|
373
|
+
const validationResult = EventPayloadSchema.safeParse(rawPayload);
|
|
374
|
+
if (!validationResult.success) {
|
|
375
|
+
const errors = validationResult.error.issues
|
|
376
|
+
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
377
|
+
.join(", ");
|
|
378
|
+
log.error(`Invalid payload extracted in middlewareEventConsumer: ${errors}`);
|
|
379
|
+
const error = new ScrawnValidationError("Payload validation failed", {
|
|
380
|
+
details: {
|
|
381
|
+
errors: validationResult.error.issues.map((e) => ({
|
|
382
|
+
field: e.path.join("."),
|
|
383
|
+
message: e.message,
|
|
384
|
+
})),
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
this.notifyValidationError(error, config.onError);
|
|
388
|
+
return next();
|
|
389
|
+
}
|
|
390
|
+
const eventId = randomUUID();
|
|
391
|
+
const idempotencyKey = randomUUID();
|
|
392
|
+
this.consumeEvent(validationResult.data, "api", "MIDDLEWARE_CALL", eventId, idempotencyKey).catch((error) => {
|
|
393
|
+
log.error(`Failed to track middleware event: ${error.message}`);
|
|
394
|
+
this.notifyEventConsumerError(error, config.onError);
|
|
395
|
+
});
|
|
396
|
+
next();
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
log.error(`Error in middlewareEventConsumer: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
400
|
+
this.notifyEventConsumerError(error, config.onError);
|
|
401
|
+
next();
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Collect payment by creating a checkout link for a user.
|
|
407
|
+
*
|
|
408
|
+
* Generates a payment checkout link for the specified user via the Scrawn payment service.
|
|
409
|
+
* The checkout link can be used to direct users to complete their payment.
|
|
410
|
+
*
|
|
411
|
+
* @param userId - Unique identifier of the user to collect payment from
|
|
412
|
+
* @returns A promise that resolves to the checkout link URL
|
|
413
|
+
* @throws Error if the gRPC call fails or if authentication is invalid
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* const checkoutLink = await scrawn.collectPayment('user_abc123');
|
|
418
|
+
* // Returns: 'https://checkout.scrawn.dev/...'
|
|
419
|
+
* // Redirect user to this URL to complete payment
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
async collectPayment(userId) {
|
|
423
|
+
// Validate input
|
|
424
|
+
if (!userId || typeof userId !== "string" || userId.trim().length === 0) {
|
|
425
|
+
log.error("Invalid userId provided to collectPayment");
|
|
426
|
+
throw new ScrawnValidationError("userId must be a non-empty string", {
|
|
427
|
+
details: { provided: typeof userId },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
// Get credentials for authentication
|
|
431
|
+
const creds = await this.getCredsFor("api");
|
|
432
|
+
try {
|
|
433
|
+
log.info(`Creating checkout link for user: ${userId}`);
|
|
434
|
+
const request = new CreateCheckoutLinkRequest();
|
|
435
|
+
request.setUserid(userId);
|
|
436
|
+
const response = await this.grpcClient
|
|
437
|
+
.newCall(PaymentServiceClient, "createCheckoutLink")
|
|
438
|
+
.addMetadata("authorization", `Bearer ${creds.apiKey}`)
|
|
439
|
+
.addPayload(request)
|
|
440
|
+
.request();
|
|
441
|
+
log.info(`Checkout link created successfully: ${response.getCheckoutlink()}`);
|
|
442
|
+
return response.getCheckoutlink();
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
log.error(`Failed to create checkout link: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
446
|
+
throw convertGrpcError(error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Internal method to consume and process an event.
|
|
451
|
+
*
|
|
452
|
+
* This method:
|
|
453
|
+
* 1. Validates authentication
|
|
454
|
+
* 2. Fetches/caches credentials
|
|
455
|
+
* 3. Executes any pre-run hooks
|
|
456
|
+
* 4. Processes the event via gRPC call to RegisterEvent
|
|
457
|
+
*
|
|
458
|
+
* @param payload - Event payload data
|
|
459
|
+
* @param authMethodName - Name of the auth method to use (must be in AuthRegistry)
|
|
460
|
+
* @param eventType - Type of event for categorization (RAW or MIDDLEWARE_CALL)
|
|
461
|
+
* @param eventId - Stable event ID (generated by caller, reused across retries)
|
|
462
|
+
* @param idempotencyKey - Stable idempotency key (generated by caller, reused across retries)
|
|
463
|
+
* @returns A promise that resolves when the event is processed
|
|
464
|
+
* @throws Error if auth method is not registered or gRPC call fails
|
|
465
|
+
*
|
|
466
|
+
* @internal
|
|
467
|
+
*/
|
|
468
|
+
async consumeEvent(payload, authMethodName, eventType, eventId, idempotencyKey) {
|
|
469
|
+
const auth = this.authMethods.get(authMethodName);
|
|
470
|
+
if (!auth) {
|
|
471
|
+
throw new ScrawnConfigError(`No auth registered for type ${authMethodName}`, {
|
|
472
|
+
details: { requestedAuth: authMethodName },
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
// Run pre-hook if exists
|
|
476
|
+
if (auth.preRun)
|
|
477
|
+
await auth.preRun();
|
|
478
|
+
// Get creds (from cache or fresh)
|
|
479
|
+
const creds = await this.getCredsFor(authMethodName);
|
|
480
|
+
// Map event type to BasicUsageType
|
|
481
|
+
const basicUsageType = eventType === "RAW" ? BasicUsageType.RAW : BasicUsageType.MIDDLEWARE_CALL;
|
|
482
|
+
// Build debit field based on which debit option is provided
|
|
483
|
+
let debitField;
|
|
484
|
+
if (payload.debitAmount !== undefined) {
|
|
485
|
+
debitField = { case: "amount", value: payload.debitAmount };
|
|
486
|
+
}
|
|
487
|
+
else if (payload.debitTag !== undefined) {
|
|
488
|
+
debitField = { case: "tag", value: payload.debitTag };
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
const serialized = serializeExpr(payload.debitExpr);
|
|
492
|
+
log.debug(`Serialized pricing expression: ${serialized}\n${prettyPrintExpr(payload.debitExpr)}`);
|
|
493
|
+
debitField = {
|
|
494
|
+
case: "expr",
|
|
495
|
+
value: serialized,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// Retry loop for retryable failures
|
|
499
|
+
for (let attempt = 0;; attempt++) {
|
|
500
|
+
try {
|
|
501
|
+
log.info(`Ingesting event (type: ${eventType}) — attempt ${attempt + 1}`);
|
|
502
|
+
const basicUsage = new BasicUsage();
|
|
503
|
+
basicUsage.setBasicusagetype(basicUsageType);
|
|
504
|
+
if (debitField.case === "amount") {
|
|
505
|
+
basicUsage.setAmount(debitField.value);
|
|
506
|
+
}
|
|
507
|
+
else if (debitField.case === "tag") {
|
|
508
|
+
basicUsage.setTag(debitField.value);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
basicUsage.setExpr(debitField.value);
|
|
512
|
+
}
|
|
513
|
+
if (payload.metadata) {
|
|
514
|
+
basicUsage.setMetadata(JSON.stringify(payload.metadata));
|
|
515
|
+
}
|
|
516
|
+
const request = new RegisterEventRequest();
|
|
517
|
+
request.setType(EventType.BASIC_USAGE);
|
|
518
|
+
request.setUserid(payload.userId);
|
|
519
|
+
request.setEventid(eventId);
|
|
520
|
+
request.setIdempotencykey(idempotencyKey);
|
|
521
|
+
request.setBasicusage(basicUsage);
|
|
522
|
+
const response = await this.grpcClient
|
|
523
|
+
.newCall(EventServiceClient, "registerEvent")
|
|
524
|
+
.addMetadata("authorization", `Bearer ${creds.apiKey}`)
|
|
525
|
+
.addPayload(request)
|
|
526
|
+
.request();
|
|
527
|
+
log.info(`Event registered successfully: ${JSON.stringify(response)}`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
const converted = convertGrpcError(error);
|
|
532
|
+
if (attempt < this.retryCount && isRetryableError(converted)) {
|
|
533
|
+
const delay = this.backoffMs(attempt);
|
|
534
|
+
log.warn(`Retryable error on attempt ${attempt + 1}, retrying in ${delay}ms: ${converted.message}`);
|
|
535
|
+
await this.sleep(delay);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
log.error(`Failed to register event: ${converted.message}`);
|
|
539
|
+
throw converted;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (auth.postRun)
|
|
543
|
+
await auth.postRun();
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Stream AI token usage events to the Scrawn backend.
|
|
547
|
+
*
|
|
548
|
+
* Consumes an async iterable of AI token usage payloads and streams them
|
|
549
|
+
* to the backend for billing tracking. This is designed for real-time
|
|
550
|
+
* AI token tracking where usage is reported as tokens are consumed.
|
|
551
|
+
*
|
|
552
|
+
* The streaming is non-blocking: the iterable is consumed in the background
|
|
553
|
+
* and streamed to the server without blocking the caller's code path.
|
|
554
|
+
*
|
|
555
|
+
* When `return: true`, the stream is forked internally - one fork goes to
|
|
556
|
+
* billing (non-blocking), and another is returned to the caller for streaming
|
|
557
|
+
* to the user.
|
|
558
|
+
*
|
|
559
|
+
* @param stream - An async iterable of AI token usage payloads
|
|
560
|
+
* @param config - Optional configuration object
|
|
561
|
+
* @param config.return - If true, returns a forked stream alongside the response promise
|
|
562
|
+
* @param config.onError - Optional callback for handling validation or gRPC errors
|
|
563
|
+
* @returns Depends on config.return:
|
|
564
|
+
* - false/undefined: Promise<StreamEventResponse | undefined>
|
|
565
|
+
* - true: { response: Promise<StreamEventResponse | undefined>, stream: AsyncIterable<AITokenUsagePayload> }
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```typescript
|
|
569
|
+
* // Fire-and-forget mode (default)
|
|
570
|
+
* async function* tokenUsageStream() {
|
|
571
|
+
* yield {
|
|
572
|
+
* userId: 'user_abc123',
|
|
573
|
+
* model: 'gpt-4',
|
|
574
|
+
* inputTokens: 100,
|
|
575
|
+
* outputTokens: 50,
|
|
576
|
+
* inputDebit: { amount: 1 },
|
|
577
|
+
* outputDebit: { amount: 1 }
|
|
578
|
+
* };
|
|
579
|
+
* }
|
|
580
|
+
*
|
|
581
|
+
* const response = await scrawn.aiTokenStreamConsumer(tokenUsageStream());
|
|
582
|
+
* if (response) {
|
|
583
|
+
* console.log(`Processed ${response.getEventsprocessed()} events`);
|
|
584
|
+
* }
|
|
585
|
+
*
|
|
586
|
+
* // Return mode - stream to user while billing
|
|
587
|
+
* const { response, stream } = await scrawn.aiTokenStreamConsumer(
|
|
588
|
+
* tokenUsageStream(),
|
|
589
|
+
* { return: true }
|
|
590
|
+
* );
|
|
591
|
+
*
|
|
592
|
+
* for await (const token of stream) {
|
|
593
|
+
* // Stream to user
|
|
594
|
+
* }
|
|
595
|
+
*
|
|
596
|
+
* const result = await response;
|
|
597
|
+
* if (!result) return;
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
// fallow-ignore-next-line unused-class-member
|
|
601
|
+
async aiTokenStreamConsumer(stream, config) {
|
|
602
|
+
const onError = config?.onError;
|
|
603
|
+
// Get credentials for authentication
|
|
604
|
+
const creds = await this.getCredsFor("api");
|
|
605
|
+
// If return mode, fork the stream
|
|
606
|
+
if (config?.return === true) {
|
|
607
|
+
const [billingStream, userStream] = forkAsyncIterable(stream);
|
|
608
|
+
// Transform billing stream and send to backend (non-blocking)
|
|
609
|
+
const transformedStream = this.transformAITokenStream(billingStream, onError);
|
|
610
|
+
const responsePromise = (async () => {
|
|
611
|
+
try {
|
|
612
|
+
log.info("Starting AI token usage stream (return mode)");
|
|
613
|
+
const response = await this.grpcClient
|
|
614
|
+
.newStreamCall(EventServiceClient, "streamEvents")
|
|
615
|
+
.addMetadata("authorization", `Bearer ${creds.apiKey}`)
|
|
616
|
+
.stream(transformedStream);
|
|
617
|
+
log.info(`AI token stream completed: ${response.getEventsprocessed()} events processed`);
|
|
618
|
+
return response;
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
log.error(`Failed to stream AI token usage: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
622
|
+
this.notifyEventConsumerError(error, onError);
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
})();
|
|
626
|
+
return { response: responsePromise, stream: userStream };
|
|
627
|
+
}
|
|
628
|
+
// Default: fire-and-forget mode
|
|
629
|
+
const transformedStream = this.transformAITokenStream(stream, onError);
|
|
630
|
+
try {
|
|
631
|
+
log.info("Starting AI token usage stream");
|
|
632
|
+
const response = await this.grpcClient
|
|
633
|
+
.newStreamCall(EventServiceClient, "streamEvents")
|
|
634
|
+
.addMetadata("authorization", `Bearer ${creds.apiKey}`)
|
|
635
|
+
.stream(transformedStream);
|
|
636
|
+
log.info(`AI token stream completed: ${response.getEventsprocessed()} events processed`);
|
|
637
|
+
return response;
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
log.error(`Failed to stream AI token usage: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
641
|
+
this.notifyEventConsumerError(error, onError);
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Transform user-provided AI token usage payloads into StreamEventRequest format.
|
|
647
|
+
*
|
|
648
|
+
* Validates each payload and maps it to the gRPC request format.
|
|
649
|
+
* Invalid payloads are logged and skipped.
|
|
650
|
+
*
|
|
651
|
+
* @param stream - The user's async iterable of AITokenUsagePayload
|
|
652
|
+
* @returns An async iterable of StreamEventRequest payloads
|
|
653
|
+
* @internal
|
|
654
|
+
*/
|
|
655
|
+
async *transformAITokenStream(stream, onError) {
|
|
656
|
+
for await (const payload of stream) {
|
|
657
|
+
// Unwrap ScrawnExpr before Zod validation
|
|
658
|
+
const rawPayload = {
|
|
659
|
+
userId: payload.userId,
|
|
660
|
+
model: payload.model,
|
|
661
|
+
inputTokens: payload.inputTokens,
|
|
662
|
+
outputTokens: payload.outputTokens,
|
|
663
|
+
inputDebit: {
|
|
664
|
+
amount: payload.inputDebit.amount,
|
|
665
|
+
tag: payload.inputDebit.tag,
|
|
666
|
+
expr: payload.inputDebit.expr?._expr,
|
|
667
|
+
},
|
|
668
|
+
outputDebit: {
|
|
669
|
+
amount: payload.outputDebit.amount,
|
|
670
|
+
tag: payload.outputDebit.tag,
|
|
671
|
+
expr: payload.outputDebit.expr?._expr,
|
|
672
|
+
},
|
|
673
|
+
metadata: payload.metadata,
|
|
674
|
+
provider: payload.provider,
|
|
675
|
+
inputCacheTokens: payload.inputCacheTokens,
|
|
676
|
+
inputCacheDebit: payload.inputCacheDebit
|
|
677
|
+
? {
|
|
678
|
+
amount: payload.inputCacheDebit.amount,
|
|
679
|
+
tag: payload.inputCacheDebit.tag,
|
|
680
|
+
expr: payload.inputCacheDebit.expr?._expr,
|
|
681
|
+
}
|
|
682
|
+
: undefined,
|
|
683
|
+
};
|
|
684
|
+
// Validate each payload
|
|
685
|
+
const validationResult = AITokenUsagePayloadSchema.safeParse(rawPayload);
|
|
686
|
+
if (!validationResult.success) {
|
|
687
|
+
const errors = validationResult.error.issues
|
|
688
|
+
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
689
|
+
.join(", ");
|
|
690
|
+
log.error(`Invalid AI token usage payload, skipping: ${errors}`);
|
|
691
|
+
const error = new ScrawnValidationError("AI token usage payload validation failed", {
|
|
692
|
+
details: {
|
|
693
|
+
errors: validationResult.error.issues.map((e) => ({
|
|
694
|
+
field: e.path.join("."),
|
|
695
|
+
message: e.message,
|
|
696
|
+
})),
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
this.notifyValidationError(error, onError);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const validated = validationResult.data;
|
|
703
|
+
// Token context for resolving inputTokens()/outputTokens() placeholders
|
|
704
|
+
const tokenContext = {
|
|
705
|
+
inputTokens: validated.inputTokens,
|
|
706
|
+
outputTokens: validated.outputTokens,
|
|
707
|
+
};
|
|
708
|
+
// Build input debit field (amount, tag, or expr)
|
|
709
|
+
let inputDebit;
|
|
710
|
+
if (validated.inputDebit.amount !== undefined) {
|
|
711
|
+
inputDebit = {
|
|
712
|
+
case: "inputAmount",
|
|
713
|
+
value: validated.inputDebit.amount,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
else if (validated.inputDebit.tag !== undefined) {
|
|
717
|
+
inputDebit = {
|
|
718
|
+
case: "inputTag",
|
|
719
|
+
value: validated.inputDebit.tag,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const resolved = resolveTokens(validated.inputDebit.expr, tokenContext);
|
|
724
|
+
const serialized = serializeExpr(resolved);
|
|
725
|
+
log.debug(`Resolved input debit expression (inputTokens=${validated.inputTokens}): ${serialized}\n${prettyPrintExpr(resolved)}`);
|
|
726
|
+
inputDebit = {
|
|
727
|
+
case: "inputExpr",
|
|
728
|
+
value: serialized,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
// Build output debit field (amount, tag, or expr)
|
|
732
|
+
let outputDebit;
|
|
733
|
+
if (validated.outputDebit.amount !== undefined) {
|
|
734
|
+
outputDebit = {
|
|
735
|
+
case: "outputAmount",
|
|
736
|
+
value: validated.outputDebit.amount,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
else if (validated.outputDebit.tag !== undefined) {
|
|
740
|
+
outputDebit = {
|
|
741
|
+
case: "outputTag",
|
|
742
|
+
value: validated.outputDebit.tag,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const resolved = resolveTokens(validated.outputDebit.expr, tokenContext);
|
|
747
|
+
const serialized = serializeExpr(resolved);
|
|
748
|
+
log.debug(`Resolved output debit expression (outputTokens=${validated.outputTokens}): ${serialized}\n${prettyPrintExpr(resolved)}`);
|
|
749
|
+
outputDebit = {
|
|
750
|
+
case: "outputExpr",
|
|
751
|
+
value: serialized,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const aiTokenUsage = new AITokenUsage();
|
|
755
|
+
aiTokenUsage.setModel(validated.model);
|
|
756
|
+
aiTokenUsage.setInputtokens(validated.inputTokens);
|
|
757
|
+
aiTokenUsage.setOutputtokens(validated.outputTokens);
|
|
758
|
+
if (inputDebit.case === "inputAmount") {
|
|
759
|
+
aiTokenUsage.setInputamount(inputDebit.value);
|
|
760
|
+
}
|
|
761
|
+
else if (inputDebit.case === "inputTag") {
|
|
762
|
+
aiTokenUsage.setInputtag(inputDebit.value);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
aiTokenUsage.setInputexpr(inputDebit.value);
|
|
766
|
+
}
|
|
767
|
+
if (outputDebit.case === "outputAmount") {
|
|
768
|
+
aiTokenUsage.setOutputamount(outputDebit.value);
|
|
769
|
+
}
|
|
770
|
+
else if (outputDebit.case === "outputTag") {
|
|
771
|
+
aiTokenUsage.setOutputtag(outputDebit.value);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
aiTokenUsage.setOutputexpr(outputDebit.value);
|
|
775
|
+
}
|
|
776
|
+
// Set metadata on AITokenUsage if provided
|
|
777
|
+
if (validated.metadata) {
|
|
778
|
+
aiTokenUsage.setMetadata(JSON.stringify(validated.metadata));
|
|
779
|
+
}
|
|
780
|
+
// Set provider if provided
|
|
781
|
+
if (validated.provider) {
|
|
782
|
+
aiTokenUsage.setProvider(validated.provider);
|
|
783
|
+
}
|
|
784
|
+
// Set input cache tokens if provided
|
|
785
|
+
if (validated.inputCacheTokens !== undefined) {
|
|
786
|
+
aiTokenUsage.setInputcachetokens(validated.inputCacheTokens);
|
|
787
|
+
}
|
|
788
|
+
// Set input cache debit if provided
|
|
789
|
+
if (validated.inputCacheDebit) {
|
|
790
|
+
if (validated.inputCacheDebit.amount !== undefined) {
|
|
791
|
+
aiTokenUsage.setInputcacheamount(validated.inputCacheDebit.amount);
|
|
792
|
+
}
|
|
793
|
+
else if (validated.inputCacheDebit.tag !== undefined) {
|
|
794
|
+
aiTokenUsage.setInputcachetag(validated.inputCacheDebit.tag);
|
|
795
|
+
}
|
|
796
|
+
else if (validated.inputCacheDebit.expr) {
|
|
797
|
+
const resolved = resolveTokens(validated.inputCacheDebit.expr, tokenContext);
|
|
798
|
+
aiTokenUsage.setInputcacheexpr(serializeExpr(resolved));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const eventId = randomUUID();
|
|
802
|
+
const idempotencyKey = randomUUID();
|
|
803
|
+
const request = new StreamEventRequest();
|
|
804
|
+
request.setType(EventType.AI_TOKEN_USAGE);
|
|
805
|
+
request.setUserid(validated.userId);
|
|
806
|
+
request.setEventid(eventId);
|
|
807
|
+
request.setIdempotencykey(idempotencyKey);
|
|
808
|
+
request.setAitokenusage(aiTokenUsage);
|
|
809
|
+
yield request;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
export function createScrawn(config) {
|
|
814
|
+
return new Scrawn({
|
|
815
|
+
apiKey: config.apiKey,
|
|
816
|
+
baseURL: config.baseURL,
|
|
817
|
+
secure: config.secure,
|
|
818
|
+
credentials: config.credentials,
|
|
819
|
+
retryCount: config.retryCount,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
//# sourceMappingURL=scrawn.js.map
|