@dexto/server 1.6.15 → 1.6.17
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/events/a2a-sse-subscriber.cjs +9 -2
- package/dist/events/a2a-sse-subscriber.d.ts.map +1 -1
- package/dist/events/a2a-sse-subscriber.js +9 -2
- package/dist/events/usage-event-subscriber.cjs +263 -0
- package/dist/events/usage-event-subscriber.d.ts +35 -0
- package/dist/events/usage-event-subscriber.d.ts.map +1 -0
- package/dist/events/usage-event-subscriber.js +244 -0
- package/dist/events/usage-event-types.cjs +16 -0
- package/dist/events/usage-event-types.d.ts +33 -0
- package/dist/events/usage-event-types.d.ts.map +1 -0
- package/dist/events/usage-event-types.js +0 -0
- package/dist/hono/index.d.ts +157 -32
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/routes/a2a-tasks.d.ts +9 -9
- package/dist/hono/routes/approvals.cjs +94 -6
- package/dist/hono/routes/approvals.d.ts +22 -6
- package/dist/hono/routes/approvals.d.ts.map +1 -1
- package/dist/hono/routes/approvals.js +94 -6
- package/dist/hono/routes/messages.cjs +16 -5
- package/dist/hono/routes/messages.d.ts +6 -0
- package/dist/hono/routes/messages.d.ts.map +1 -1
- package/dist/hono/routes/messages.js +17 -6
- package/dist/hono/routes/search.d.ts +10 -0
- package/dist/hono/routes/search.d.ts.map +1 -1
- package/dist/hono/routes/sessions.cjs +54 -1
- package/dist/hono/routes/sessions.d.ts +111 -18
- package/dist/hono/routes/sessions.d.ts.map +1 -1
- package/dist/hono/routes/sessions.js +59 -2
- package/dist/hono/schemas/responses.cjs +48 -8
- package/dist/hono/schemas/responses.d.ts +489 -22
- package/dist/hono/schemas/responses.d.ts.map +1 -1
- package/dist/hono/schemas/responses.js +49 -9
- package/dist/index.cjs +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +9 -9
|
@@ -98,11 +98,18 @@ class A2ASseEventSubscriber {
|
|
|
98
98
|
message: {
|
|
99
99
|
role: "agent",
|
|
100
100
|
content: [{ type: "text", text: payload.content }],
|
|
101
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
101
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
...payload.messageId && { messageId: payload.messageId }
|
|
102
103
|
},
|
|
103
104
|
tokenUsage: payload.tokenUsage,
|
|
104
105
|
provider: payload.provider,
|
|
105
|
-
model: payload.model
|
|
106
|
+
model: payload.model,
|
|
107
|
+
...payload.messageId && { messageId: payload.messageId },
|
|
108
|
+
...payload.usageScopeId && { usageScopeId: payload.usageScopeId },
|
|
109
|
+
...payload.estimatedCost !== void 0 && {
|
|
110
|
+
estimatedCost: payload.estimatedCost
|
|
111
|
+
},
|
|
112
|
+
...payload.pricingStatus && { pricingStatus: payload.pricingStatus }
|
|
106
113
|
});
|
|
107
114
|
},
|
|
108
115
|
{ signal }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"a2a-sse-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/a2a-sse-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,qBAAqB,CAAC,CAAkB;IAEhD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"a2a-sse-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/a2a-sse-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,qBAAqB,CAAC,CAAkB;IAEhD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAyHxC;;;;;;;OAOG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC;IAiDxD;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAKtB;;OAEG;IACH,OAAO,IAAI,IAAI;IAgBf;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;OAEG;IACH,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CASjD"}
|
|
@@ -75,11 +75,18 @@ class A2ASseEventSubscriber {
|
|
|
75
75
|
message: {
|
|
76
76
|
role: "agent",
|
|
77
77
|
content: [{ type: "text", text: payload.content }],
|
|
78
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
78
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79
|
+
...payload.messageId && { messageId: payload.messageId }
|
|
79
80
|
},
|
|
80
81
|
tokenUsage: payload.tokenUsage,
|
|
81
82
|
provider: payload.provider,
|
|
82
|
-
model: payload.model
|
|
83
|
+
model: payload.model,
|
|
84
|
+
...payload.messageId && { messageId: payload.messageId },
|
|
85
|
+
...payload.usageScopeId && { usageScopeId: payload.usageScopeId },
|
|
86
|
+
...payload.estimatedCost !== void 0 && {
|
|
87
|
+
estimatedCost: payload.estimatedCost
|
|
88
|
+
},
|
|
89
|
+
...payload.pricingStatus && { pricingStatus: payload.pricingStatus }
|
|
83
90
|
});
|
|
84
91
|
},
|
|
85
92
|
{ signal }
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var usage_event_subscriber_exports = {};
|
|
20
|
+
__export(usage_event_subscriber_exports, {
|
|
21
|
+
UsageEventSubscriber: () => UsageEventSubscriber
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(usage_event_subscriber_exports);
|
|
24
|
+
var import_events = require("events");
|
|
25
|
+
var import_core = require("@dexto/core");
|
|
26
|
+
const OUTBOX_KEY_PREFIX = "usage-outbox:";
|
|
27
|
+
const DEFAULT_DELIVERY_OPTIONS = {
|
|
28
|
+
fetchFn: fetch,
|
|
29
|
+
flushIntervalMs: 5e3,
|
|
30
|
+
batchSize: 50,
|
|
31
|
+
requestTimeoutMs: 1e4
|
|
32
|
+
};
|
|
33
|
+
function requirePositiveIntegerOption(name, value) {
|
|
34
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
35
|
+
throw new RangeError(
|
|
36
|
+
`UsageEventSubscriber ${name} must be a positive integer. Received: ${value}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function createUsageEventId(usageScopeId, messageId) {
|
|
42
|
+
return `usage:${usageScopeId}:${messageId}`;
|
|
43
|
+
}
|
|
44
|
+
function createOutboxKey(eventId) {
|
|
45
|
+
return `${OUTBOX_KEY_PREFIX}${eventId}`;
|
|
46
|
+
}
|
|
47
|
+
class UsageEventSubscriber {
|
|
48
|
+
database;
|
|
49
|
+
targetUrl;
|
|
50
|
+
authToken;
|
|
51
|
+
runtimeId;
|
|
52
|
+
runId;
|
|
53
|
+
deliveryOptions;
|
|
54
|
+
abortController;
|
|
55
|
+
inFlightDeliveryAbortController;
|
|
56
|
+
flushInterval;
|
|
57
|
+
flushPromise = null;
|
|
58
|
+
flushRequestedWhileRunning = false;
|
|
59
|
+
isCleaningUp = false;
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.database = config.database;
|
|
62
|
+
this.targetUrl = config.targetUrl;
|
|
63
|
+
this.authToken = config.authToken;
|
|
64
|
+
this.runtimeId = config.runtimeId;
|
|
65
|
+
this.runId = config.runId;
|
|
66
|
+
const flushIntervalMs = requirePositiveIntegerOption(
|
|
67
|
+
"flushIntervalMs",
|
|
68
|
+
config.flushIntervalMs ?? DEFAULT_DELIVERY_OPTIONS.flushIntervalMs
|
|
69
|
+
);
|
|
70
|
+
const batchSize = requirePositiveIntegerOption(
|
|
71
|
+
"batchSize",
|
|
72
|
+
config.batchSize ?? DEFAULT_DELIVERY_OPTIONS.batchSize
|
|
73
|
+
);
|
|
74
|
+
const requestTimeoutMs = requirePositiveIntegerOption(
|
|
75
|
+
"requestTimeoutMs",
|
|
76
|
+
config.requestTimeoutMs ?? DEFAULT_DELIVERY_OPTIONS.requestTimeoutMs
|
|
77
|
+
);
|
|
78
|
+
this.deliveryOptions = {
|
|
79
|
+
fetchFn: config.fetchFn ?? DEFAULT_DELIVERY_OPTIONS.fetchFn,
|
|
80
|
+
flushIntervalMs,
|
|
81
|
+
batchSize,
|
|
82
|
+
requestTimeoutMs
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
subscribe(eventBus) {
|
|
86
|
+
this.isCleaningUp = false;
|
|
87
|
+
this.abortController?.abort();
|
|
88
|
+
if (this.flushInterval) {
|
|
89
|
+
clearInterval(this.flushInterval);
|
|
90
|
+
}
|
|
91
|
+
this.abortController = new AbortController();
|
|
92
|
+
const { signal } = this.abortController;
|
|
93
|
+
(0, import_events.setMaxListeners)(10, signal);
|
|
94
|
+
eventBus.on(
|
|
95
|
+
"llm:response",
|
|
96
|
+
(payload) => {
|
|
97
|
+
void this.enqueueUsageEvent(payload);
|
|
98
|
+
},
|
|
99
|
+
{ signal }
|
|
100
|
+
);
|
|
101
|
+
this.flushInterval = setInterval(() => {
|
|
102
|
+
void this.flushPending();
|
|
103
|
+
}, this.deliveryOptions.flushIntervalMs);
|
|
104
|
+
void this.flushPending();
|
|
105
|
+
}
|
|
106
|
+
cleanup() {
|
|
107
|
+
this.isCleaningUp = true;
|
|
108
|
+
if (this.abortController) {
|
|
109
|
+
this.abortController.abort();
|
|
110
|
+
delete this.abortController;
|
|
111
|
+
}
|
|
112
|
+
if (this.flushInterval) {
|
|
113
|
+
clearInterval(this.flushInterval);
|
|
114
|
+
delete this.flushInterval;
|
|
115
|
+
}
|
|
116
|
+
if (this.inFlightDeliveryAbortController) {
|
|
117
|
+
this.inFlightDeliveryAbortController.abort();
|
|
118
|
+
delete this.inFlightDeliveryAbortController;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async flush() {
|
|
122
|
+
await this.flushPending();
|
|
123
|
+
}
|
|
124
|
+
buildUsageEvent(payload) {
|
|
125
|
+
if (!payload.messageId || !payload.usageScopeId || !payload.tokenUsage) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!(0, import_core.hasMeaningfulTokenUsage)(payload.tokenUsage)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const resolvedCostBreakdown = payload.provider && payload.model ? (() => {
|
|
132
|
+
const pricing = (0, import_core.getModelPricing)(payload.provider, payload.model);
|
|
133
|
+
if (!pricing) {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
return (0, import_core.calculateCostBreakdown)(payload.tokenUsage, pricing);
|
|
137
|
+
})() : void 0;
|
|
138
|
+
const resolvedEstimatedCost = payload.estimatedCost ?? resolvedCostBreakdown?.totalUsd;
|
|
139
|
+
return {
|
|
140
|
+
eventId: createUsageEventId(payload.usageScopeId, payload.messageId),
|
|
141
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
142
|
+
sessionId: payload.sessionId,
|
|
143
|
+
messageId: payload.messageId,
|
|
144
|
+
usageScopeId: payload.usageScopeId,
|
|
145
|
+
...payload.provider && { provider: payload.provider },
|
|
146
|
+
...payload.model && { model: payload.model },
|
|
147
|
+
tokenUsage: payload.tokenUsage,
|
|
148
|
+
...resolvedEstimatedCost !== void 0 && { estimatedCostUsd: resolvedEstimatedCost },
|
|
149
|
+
...resolvedCostBreakdown && { costBreakdownUsd: resolvedCostBreakdown },
|
|
150
|
+
...this.runtimeId && { runtimeId: this.runtimeId },
|
|
151
|
+
...this.runId && { runId: this.runId }
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async enqueueUsageEvent(payload) {
|
|
155
|
+
const usageEvent = this.buildUsageEvent(payload);
|
|
156
|
+
if (!usageEvent) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await this.database.set(createOutboxKey(usageEvent.eventId), {
|
|
160
|
+
event: usageEvent
|
|
161
|
+
});
|
|
162
|
+
void this.flushPending();
|
|
163
|
+
}
|
|
164
|
+
async listPendingRecords() {
|
|
165
|
+
const keys = await this.database.list(OUTBOX_KEY_PREFIX);
|
|
166
|
+
if (keys.length === 0) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const records = await Promise.all(
|
|
170
|
+
keys.map(async (key) => {
|
|
171
|
+
const record = await this.database.get(key);
|
|
172
|
+
return record ? { key, record } : null;
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
return records.filter(
|
|
176
|
+
(value) => value !== null
|
|
177
|
+
).sort(
|
|
178
|
+
(left, right) => left.record.event.occurredAt.localeCompare(right.record.event.occurredAt)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async flushPending() {
|
|
182
|
+
if (this.isCleaningUp) {
|
|
183
|
+
return this.flushPromise ?? Promise.resolve();
|
|
184
|
+
}
|
|
185
|
+
if (this.flushPromise) {
|
|
186
|
+
this.flushRequestedWhileRunning = true;
|
|
187
|
+
return this.flushPromise;
|
|
188
|
+
}
|
|
189
|
+
this.flushPromise = this.doFlushPending().finally(() => {
|
|
190
|
+
this.flushPromise = null;
|
|
191
|
+
const shouldFlushAgain = this.flushRequestedWhileRunning;
|
|
192
|
+
this.flushRequestedWhileRunning = false;
|
|
193
|
+
if (shouldFlushAgain) {
|
|
194
|
+
void this.flushPending();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return this.flushPromise;
|
|
198
|
+
}
|
|
199
|
+
async doFlushPending() {
|
|
200
|
+
if (this.isCleaningUp) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const pendingRecords = await this.listPendingRecords();
|
|
204
|
+
if (pendingRecords.length === 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (let index = 0; index < pendingRecords.length; index += this.deliveryOptions.batchSize) {
|
|
208
|
+
if (this.isCleaningUp) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const batchRecords = pendingRecords.slice(
|
|
212
|
+
index,
|
|
213
|
+
index + this.deliveryOptions.batchSize
|
|
214
|
+
);
|
|
215
|
+
const payload = {
|
|
216
|
+
events: batchRecords.map((record) => record.record.event)
|
|
217
|
+
};
|
|
218
|
+
const abortController = new AbortController();
|
|
219
|
+
this.inFlightDeliveryAbortController = abortController;
|
|
220
|
+
const timeout = setTimeout(() => {
|
|
221
|
+
abortController.abort();
|
|
222
|
+
}, this.deliveryOptions.requestTimeoutMs);
|
|
223
|
+
try {
|
|
224
|
+
const response = await this.deliveryOptions.fetchFn(this.targetUrl, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: {
|
|
227
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
228
|
+
"Content-Type": "application/json"
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify(payload),
|
|
231
|
+
signal: abortController.signal
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
import_core.logger.warn(
|
|
235
|
+
`Usage event delivery failed (${response.status}) for ${batchRecords.length} events`
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (this.isCleaningUp) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await Promise.all(batchRecords.map((record) => this.database.delete(record.key)));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (this.isCleaningUp && abortController.signal.aborted) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
import_core.logger.warn(
|
|
248
|
+
`Usage event delivery failed: ${error instanceof Error ? error.message : String(error)}`
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
} finally {
|
|
252
|
+
clearTimeout(timeout);
|
|
253
|
+
if (this.inFlightDeliveryAbortController === abortController) {
|
|
254
|
+
delete this.inFlightDeliveryAbortController;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
261
|
+
0 && (module.exports = {
|
|
262
|
+
UsageEventSubscriber
|
|
263
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AgentEventBus, type Database } from '@dexto/core';
|
|
2
|
+
import type { EventSubscriber } from './types.js';
|
|
3
|
+
import type { UsageEventDeliveryOptions } from './usage-event-types.js';
|
|
4
|
+
interface UsageEventSubscriberConfig extends UsageEventDeliveryOptions {
|
|
5
|
+
database: Database;
|
|
6
|
+
targetUrl: string;
|
|
7
|
+
authToken: string;
|
|
8
|
+
runtimeId?: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class UsageEventSubscriber implements EventSubscriber {
|
|
12
|
+
private readonly database;
|
|
13
|
+
private readonly targetUrl;
|
|
14
|
+
private readonly authToken;
|
|
15
|
+
private readonly runtimeId;
|
|
16
|
+
private readonly runId;
|
|
17
|
+
private readonly deliveryOptions;
|
|
18
|
+
private abortController?;
|
|
19
|
+
private inFlightDeliveryAbortController?;
|
|
20
|
+
private flushInterval?;
|
|
21
|
+
private flushPromise;
|
|
22
|
+
private flushRequestedWhileRunning;
|
|
23
|
+
private isCleaningUp;
|
|
24
|
+
constructor(config: UsageEventSubscriberConfig);
|
|
25
|
+
subscribe(eventBus: AgentEventBus): void;
|
|
26
|
+
cleanup(): void;
|
|
27
|
+
flush(): Promise<void>;
|
|
28
|
+
private buildUsageEvent;
|
|
29
|
+
private enqueueUsageEvent;
|
|
30
|
+
private listPendingRecords;
|
|
31
|
+
private flushPending;
|
|
32
|
+
private doFlushPending;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=usage-event-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-event-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/usage-event-subscriber.ts"],"names":[],"mappings":"AACA,OAAO,EACH,aAAa,EAMb,KAAK,QAAQ,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGR,yBAAyB,EAC5B,MAAM,wBAAwB,CAAC;AAqBhC,UAAU,0BAA2B,SAAQ,yBAAyB;IAClE,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,qBAAa,oBAAqB,YAAW,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IACtE,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,+BAA+B,CAAC,CAAkB;IAC1D,OAAO,CAAC,aAAa,CAAC,CAAiC;IACvD,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,0BAA0B,CAAS;IAC3C,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,0BAA0B;IA0B9C,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IA2BxC,OAAO,IAAI,IAAI;IAmBF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC,OAAO,CAAC,eAAe;YAsCT,iBAAiB;YAajB,kBAAkB;YA2BlB,YAAY;YAsBZ,cAAc;CA2E/B"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { setMaxListeners } from "events";
|
|
2
|
+
import {
|
|
3
|
+
calculateCostBreakdown,
|
|
4
|
+
getModelPricing,
|
|
5
|
+
hasMeaningfulTokenUsage,
|
|
6
|
+
logger
|
|
7
|
+
} from "@dexto/core";
|
|
8
|
+
const OUTBOX_KEY_PREFIX = "usage-outbox:";
|
|
9
|
+
const DEFAULT_DELIVERY_OPTIONS = {
|
|
10
|
+
fetchFn: fetch,
|
|
11
|
+
flushIntervalMs: 5e3,
|
|
12
|
+
batchSize: 50,
|
|
13
|
+
requestTimeoutMs: 1e4
|
|
14
|
+
};
|
|
15
|
+
function requirePositiveIntegerOption(name, value) {
|
|
16
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
17
|
+
throw new RangeError(
|
|
18
|
+
`UsageEventSubscriber ${name} must be a positive integer. Received: ${value}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function createUsageEventId(usageScopeId, messageId) {
|
|
24
|
+
return `usage:${usageScopeId}:${messageId}`;
|
|
25
|
+
}
|
|
26
|
+
function createOutboxKey(eventId) {
|
|
27
|
+
return `${OUTBOX_KEY_PREFIX}${eventId}`;
|
|
28
|
+
}
|
|
29
|
+
class UsageEventSubscriber {
|
|
30
|
+
database;
|
|
31
|
+
targetUrl;
|
|
32
|
+
authToken;
|
|
33
|
+
runtimeId;
|
|
34
|
+
runId;
|
|
35
|
+
deliveryOptions;
|
|
36
|
+
abortController;
|
|
37
|
+
inFlightDeliveryAbortController;
|
|
38
|
+
flushInterval;
|
|
39
|
+
flushPromise = null;
|
|
40
|
+
flushRequestedWhileRunning = false;
|
|
41
|
+
isCleaningUp = false;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.database = config.database;
|
|
44
|
+
this.targetUrl = config.targetUrl;
|
|
45
|
+
this.authToken = config.authToken;
|
|
46
|
+
this.runtimeId = config.runtimeId;
|
|
47
|
+
this.runId = config.runId;
|
|
48
|
+
const flushIntervalMs = requirePositiveIntegerOption(
|
|
49
|
+
"flushIntervalMs",
|
|
50
|
+
config.flushIntervalMs ?? DEFAULT_DELIVERY_OPTIONS.flushIntervalMs
|
|
51
|
+
);
|
|
52
|
+
const batchSize = requirePositiveIntegerOption(
|
|
53
|
+
"batchSize",
|
|
54
|
+
config.batchSize ?? DEFAULT_DELIVERY_OPTIONS.batchSize
|
|
55
|
+
);
|
|
56
|
+
const requestTimeoutMs = requirePositiveIntegerOption(
|
|
57
|
+
"requestTimeoutMs",
|
|
58
|
+
config.requestTimeoutMs ?? DEFAULT_DELIVERY_OPTIONS.requestTimeoutMs
|
|
59
|
+
);
|
|
60
|
+
this.deliveryOptions = {
|
|
61
|
+
fetchFn: config.fetchFn ?? DEFAULT_DELIVERY_OPTIONS.fetchFn,
|
|
62
|
+
flushIntervalMs,
|
|
63
|
+
batchSize,
|
|
64
|
+
requestTimeoutMs
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
subscribe(eventBus) {
|
|
68
|
+
this.isCleaningUp = false;
|
|
69
|
+
this.abortController?.abort();
|
|
70
|
+
if (this.flushInterval) {
|
|
71
|
+
clearInterval(this.flushInterval);
|
|
72
|
+
}
|
|
73
|
+
this.abortController = new AbortController();
|
|
74
|
+
const { signal } = this.abortController;
|
|
75
|
+
setMaxListeners(10, signal);
|
|
76
|
+
eventBus.on(
|
|
77
|
+
"llm:response",
|
|
78
|
+
(payload) => {
|
|
79
|
+
void this.enqueueUsageEvent(payload);
|
|
80
|
+
},
|
|
81
|
+
{ signal }
|
|
82
|
+
);
|
|
83
|
+
this.flushInterval = setInterval(() => {
|
|
84
|
+
void this.flushPending();
|
|
85
|
+
}, this.deliveryOptions.flushIntervalMs);
|
|
86
|
+
void this.flushPending();
|
|
87
|
+
}
|
|
88
|
+
cleanup() {
|
|
89
|
+
this.isCleaningUp = true;
|
|
90
|
+
if (this.abortController) {
|
|
91
|
+
this.abortController.abort();
|
|
92
|
+
delete this.abortController;
|
|
93
|
+
}
|
|
94
|
+
if (this.flushInterval) {
|
|
95
|
+
clearInterval(this.flushInterval);
|
|
96
|
+
delete this.flushInterval;
|
|
97
|
+
}
|
|
98
|
+
if (this.inFlightDeliveryAbortController) {
|
|
99
|
+
this.inFlightDeliveryAbortController.abort();
|
|
100
|
+
delete this.inFlightDeliveryAbortController;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async flush() {
|
|
104
|
+
await this.flushPending();
|
|
105
|
+
}
|
|
106
|
+
buildUsageEvent(payload) {
|
|
107
|
+
if (!payload.messageId || !payload.usageScopeId || !payload.tokenUsage) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (!hasMeaningfulTokenUsage(payload.tokenUsage)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const resolvedCostBreakdown = payload.provider && payload.model ? (() => {
|
|
114
|
+
const pricing = getModelPricing(payload.provider, payload.model);
|
|
115
|
+
if (!pricing) {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
return calculateCostBreakdown(payload.tokenUsage, pricing);
|
|
119
|
+
})() : void 0;
|
|
120
|
+
const resolvedEstimatedCost = payload.estimatedCost ?? resolvedCostBreakdown?.totalUsd;
|
|
121
|
+
return {
|
|
122
|
+
eventId: createUsageEventId(payload.usageScopeId, payload.messageId),
|
|
123
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
+
sessionId: payload.sessionId,
|
|
125
|
+
messageId: payload.messageId,
|
|
126
|
+
usageScopeId: payload.usageScopeId,
|
|
127
|
+
...payload.provider && { provider: payload.provider },
|
|
128
|
+
...payload.model && { model: payload.model },
|
|
129
|
+
tokenUsage: payload.tokenUsage,
|
|
130
|
+
...resolvedEstimatedCost !== void 0 && { estimatedCostUsd: resolvedEstimatedCost },
|
|
131
|
+
...resolvedCostBreakdown && { costBreakdownUsd: resolvedCostBreakdown },
|
|
132
|
+
...this.runtimeId && { runtimeId: this.runtimeId },
|
|
133
|
+
...this.runId && { runId: this.runId }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async enqueueUsageEvent(payload) {
|
|
137
|
+
const usageEvent = this.buildUsageEvent(payload);
|
|
138
|
+
if (!usageEvent) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await this.database.set(createOutboxKey(usageEvent.eventId), {
|
|
142
|
+
event: usageEvent
|
|
143
|
+
});
|
|
144
|
+
void this.flushPending();
|
|
145
|
+
}
|
|
146
|
+
async listPendingRecords() {
|
|
147
|
+
const keys = await this.database.list(OUTBOX_KEY_PREFIX);
|
|
148
|
+
if (keys.length === 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const records = await Promise.all(
|
|
152
|
+
keys.map(async (key) => {
|
|
153
|
+
const record = await this.database.get(key);
|
|
154
|
+
return record ? { key, record } : null;
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
return records.filter(
|
|
158
|
+
(value) => value !== null
|
|
159
|
+
).sort(
|
|
160
|
+
(left, right) => left.record.event.occurredAt.localeCompare(right.record.event.occurredAt)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
async flushPending() {
|
|
164
|
+
if (this.isCleaningUp) {
|
|
165
|
+
return this.flushPromise ?? Promise.resolve();
|
|
166
|
+
}
|
|
167
|
+
if (this.flushPromise) {
|
|
168
|
+
this.flushRequestedWhileRunning = true;
|
|
169
|
+
return this.flushPromise;
|
|
170
|
+
}
|
|
171
|
+
this.flushPromise = this.doFlushPending().finally(() => {
|
|
172
|
+
this.flushPromise = null;
|
|
173
|
+
const shouldFlushAgain = this.flushRequestedWhileRunning;
|
|
174
|
+
this.flushRequestedWhileRunning = false;
|
|
175
|
+
if (shouldFlushAgain) {
|
|
176
|
+
void this.flushPending();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return this.flushPromise;
|
|
180
|
+
}
|
|
181
|
+
async doFlushPending() {
|
|
182
|
+
if (this.isCleaningUp) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const pendingRecords = await this.listPendingRecords();
|
|
186
|
+
if (pendingRecords.length === 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (let index = 0; index < pendingRecords.length; index += this.deliveryOptions.batchSize) {
|
|
190
|
+
if (this.isCleaningUp) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const batchRecords = pendingRecords.slice(
|
|
194
|
+
index,
|
|
195
|
+
index + this.deliveryOptions.batchSize
|
|
196
|
+
);
|
|
197
|
+
const payload = {
|
|
198
|
+
events: batchRecords.map((record) => record.record.event)
|
|
199
|
+
};
|
|
200
|
+
const abortController = new AbortController();
|
|
201
|
+
this.inFlightDeliveryAbortController = abortController;
|
|
202
|
+
const timeout = setTimeout(() => {
|
|
203
|
+
abortController.abort();
|
|
204
|
+
}, this.deliveryOptions.requestTimeoutMs);
|
|
205
|
+
try {
|
|
206
|
+
const response = await this.deliveryOptions.fetchFn(this.targetUrl, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify(payload),
|
|
213
|
+
signal: abortController.signal
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
logger.warn(
|
|
217
|
+
`Usage event delivery failed (${response.status}) for ${batchRecords.length} events`
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (this.isCleaningUp) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await Promise.all(batchRecords.map((record) => this.database.delete(record.key)));
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (this.isCleaningUp && abortController.signal.aborted) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
logger.warn(
|
|
230
|
+
`Usage event delivery failed: ${error instanceof Error ? error.message : String(error)}`
|
|
231
|
+
);
|
|
232
|
+
return;
|
|
233
|
+
} finally {
|
|
234
|
+
clearTimeout(timeout);
|
|
235
|
+
if (this.inFlightDeliveryAbortController === abortController) {
|
|
236
|
+
delete this.inFlightDeliveryAbortController;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
UsageEventSubscriber
|
|
244
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
var usage_event_types_exports = {};
|
|
16
|
+
module.exports = __toCommonJS(usage_event_types_exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LLMProvider, TokenUsage } from '@dexto/core';
|
|
2
|
+
export interface UsageEventCostBreakdown {
|
|
3
|
+
inputUsd: number;
|
|
4
|
+
outputUsd: number;
|
|
5
|
+
reasoningUsd: number;
|
|
6
|
+
cacheReadUsd: number;
|
|
7
|
+
cacheWriteUsd: number;
|
|
8
|
+
totalUsd: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UsageEvent {
|
|
11
|
+
eventId: string;
|
|
12
|
+
occurredAt: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
messageId: string;
|
|
15
|
+
usageScopeId: string;
|
|
16
|
+
provider?: LLMProvider;
|
|
17
|
+
model?: string;
|
|
18
|
+
tokenUsage: TokenUsage;
|
|
19
|
+
estimatedCostUsd?: number;
|
|
20
|
+
costBreakdownUsd?: UsageEventCostBreakdown;
|
|
21
|
+
runtimeId?: string;
|
|
22
|
+
runId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface UsageEventBatch {
|
|
25
|
+
events: UsageEvent[];
|
|
26
|
+
}
|
|
27
|
+
export interface UsageEventDeliveryOptions {
|
|
28
|
+
fetchFn?: typeof globalThis.fetch;
|
|
29
|
+
flushIntervalMs?: number;
|
|
30
|
+
batchSize?: number;
|
|
31
|
+
requestTimeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=usage-event-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-event-types.d.ts","sourceRoot":"","sources":["../../src/events/usage-event-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,uBAAuB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACtC,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B"}
|
|
File without changes
|