assistant-cloud 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/AssistantCloud.d.ts +15 -17
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +7 -2
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +69 -66
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +23 -25
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthTokens.d.ts +6 -7
- package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +32 -25
- package/dist/AssistantCloudEvents.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.js +65 -16
- package/dist/AssistantCloudEvents.js.map +1 -1
- package/dist/AssistantCloudFiles.d.ts +25 -26
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreadMessages.d.ts +10 -11
- package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreads.d.ts +11 -12
- package/dist/AssistantCloudProjectThreads.d.ts.map +1 -1
- package/dist/AssistantCloudProjects.d.ts +3 -5
- package/dist/AssistantCloudProjects.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +65 -53
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +9 -4
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/AssistantCloudScores.d.ts +17 -19
- package/dist/AssistantCloudScores.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.d.ts +33 -31
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +9 -2
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +37 -38
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +53 -54
- package/dist/CloudEngagementReporter.d.ts.map +1 -1
- package/dist/CloudEngagementReporter.js +4 -1
- package/dist/CloudEngagementReporter.js.map +1 -1
- package/dist/CloudMessagePersistence.d.ts +55 -56
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +3 -0
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/CloudRunReporter.d.ts +6 -8
- package/dist/CloudRunReporter.d.ts.map +1 -1
- package/dist/FormattedCloudPersistence.d.ts +34 -36
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/ai-sdk/index.d.ts +10 -12
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/cloudResponse.d.ts +2 -4
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/generateThreadTitle.d.ts +8 -10
- package/dist/generateThreadTitle.d.ts.map +1 -1
- package/dist/index.d.ts +18 -17
- package/dist/index.d.ts.map +1 -0
- package/dist/instrumentMcpSampling.d.ts +30 -32
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/runTelemetry.d.ts +86 -78
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js +57 -15
- package/dist/runTelemetry.js.map +1 -1
- package/dist/telemetry/index.d.ts +13 -15
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/AssistantCloud.ts +17 -2
- package/src/AssistantCloudAPI.ts +4 -0
- package/src/AssistantCloudEvents.test.ts +116 -0
- package/src/AssistantCloudEvents.ts +81 -20
- package/src/AssistantCloudRuns.ts +35 -5
- package/src/AssistantCloudThreadMessages.ts +21 -1
- package/src/BackendTranscriptCopy.test.ts +79 -0
- package/src/CloudEngagementReporter.ts +9 -3
- package/src/CloudMessagePersistence.ts +4 -0
- package/src/ai-sdk/index.test.ts +2 -2
- package/src/runTelemetry.test.ts +226 -2
- package/src/runTelemetry.ts +121 -17
- package/src/tests/AssistantCloud.test.ts +22 -0
- package/src/tests/AssistantCloudRuns.test.ts +28 -0
- package/src/tests/CloudEngagementReporter.test.ts +45 -0
- package/src/tests/CloudRunReporter.test.ts +14 -2
- package/dist/tests/setup.d.ts +0 -1
- package/dist/tests/setup.js +0 -7
- package/dist/tests/setup.js.map +0 -1
package/src/runTelemetry.test.ts
CHANGED
|
@@ -187,7 +187,7 @@ describe("createRunReport", () => {
|
|
|
187
187
|
threadId: "ai-sdk-thread",
|
|
188
188
|
status: "completed",
|
|
189
189
|
traceId: "00112233445566778899aabbccddeeff",
|
|
190
|
-
modelId: "gpt-
|
|
190
|
+
modelId: "gpt-6-sol",
|
|
191
191
|
provider: "gateway",
|
|
192
192
|
usage: {
|
|
193
193
|
inputTokens: 100,
|
|
@@ -215,7 +215,7 @@ describe("createRunReport", () => {
|
|
|
215
215
|
thread_id: "ai-sdk-thread",
|
|
216
216
|
status: "completed",
|
|
217
217
|
trace_id: "00112233445566778899aabbccddeeff",
|
|
218
|
-
model_id: "gpt-
|
|
218
|
+
model_id: "gpt-6-sol",
|
|
219
219
|
provider: "gateway",
|
|
220
220
|
provider_type: "gateway",
|
|
221
221
|
input_tokens: 100,
|
|
@@ -289,6 +289,90 @@ describe("createRunReport", () => {
|
|
|
289
289
|
}),
|
|
290
290
|
).toEqual({ thread_id: "thread", status: "completed" });
|
|
291
291
|
});
|
|
292
|
+
|
|
293
|
+
it("normalizes caller-supplied run cost, attributes, and root span", () => {
|
|
294
|
+
expect(
|
|
295
|
+
createRunReport({
|
|
296
|
+
threadId: "thread",
|
|
297
|
+
status: "completed",
|
|
298
|
+
rootSpanId: "AABBCCDDEEFF0011",
|
|
299
|
+
costUsd: 0.012,
|
|
300
|
+
costDetails: {
|
|
301
|
+
input: 0.002,
|
|
302
|
+
inputCachedTokens: 0.001,
|
|
303
|
+
output: 0.009,
|
|
304
|
+
total: 0.012,
|
|
305
|
+
},
|
|
306
|
+
attributes: { tenant: "acme" },
|
|
307
|
+
}),
|
|
308
|
+
).toEqual({
|
|
309
|
+
thread_id: "thread",
|
|
310
|
+
status: "completed",
|
|
311
|
+
root_span_id: "aabbccddeeff0011",
|
|
312
|
+
cost_usd: 0.012,
|
|
313
|
+
cost_details: {
|
|
314
|
+
input: 0.002,
|
|
315
|
+
input_cached_tokens: 0.001,
|
|
316
|
+
output: 0.009,
|
|
317
|
+
total: 0.012,
|
|
318
|
+
},
|
|
319
|
+
attributes: { tenant: "acme" },
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("keeps valid cost members beside invalid ones", () => {
|
|
324
|
+
expect(
|
|
325
|
+
createRunReport({
|
|
326
|
+
threadId: "thread",
|
|
327
|
+
status: "completed",
|
|
328
|
+
costUsd: 0,
|
|
329
|
+
costDetails: {
|
|
330
|
+
input: 0.002,
|
|
331
|
+
inputCachedTokens: Number.NaN,
|
|
332
|
+
output: -1,
|
|
333
|
+
total: 0,
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
).toEqual({
|
|
337
|
+
thread_id: "thread",
|
|
338
|
+
status: "completed",
|
|
339
|
+
cost_usd: 0,
|
|
340
|
+
cost_details: { input: 0.002, total: 0 },
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("omits invalid run cost and root span values", () => {
|
|
345
|
+
expect(
|
|
346
|
+
createRunReport({
|
|
347
|
+
threadId: "thread",
|
|
348
|
+
status: "completed",
|
|
349
|
+
rootSpanId: "not-a-span-id",
|
|
350
|
+
costUsd: Number.POSITIVE_INFINITY,
|
|
351
|
+
costDetails: {
|
|
352
|
+
input: -1,
|
|
353
|
+
inputCachedTokens: Number.NaN,
|
|
354
|
+
output: Number.NEGATIVE_INFINITY,
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
).toEqual({ thread_id: "thread", status: "completed" });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("keeps a server outcome type and truncates the step input", () => {
|
|
361
|
+
expect(
|
|
362
|
+
createRunReport({
|
|
363
|
+
threadId: "thread",
|
|
364
|
+
status: "error",
|
|
365
|
+
outcome: "timeout",
|
|
366
|
+
steps: [{ input: "a".repeat(MAX + 1) }],
|
|
367
|
+
}),
|
|
368
|
+
).toEqual({
|
|
369
|
+
thread_id: "thread",
|
|
370
|
+
status: "error",
|
|
371
|
+
outcome_type: "timeout",
|
|
372
|
+
steps: [{ input: "a".repeat(MAX) }],
|
|
373
|
+
total_steps: 1,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
292
376
|
});
|
|
293
377
|
|
|
294
378
|
describe("truncateRunTelemetryText", () => {
|
|
@@ -371,6 +455,146 @@ describe("createRunTelemetryToolCall", () => {
|
|
|
371
455
|
expect(call.tool_result).not.toContain("A".repeat(200));
|
|
372
456
|
});
|
|
373
457
|
|
|
458
|
+
it("summarizes a short base64 payload the same as a long one", () => {
|
|
459
|
+
// `aGk=` is how this repo's own MCP fixtures spell a whole image.
|
|
460
|
+
const call = createRunTelemetryToolCall({
|
|
461
|
+
toolName: "t",
|
|
462
|
+
toolCallId: "call-1",
|
|
463
|
+
toolSource: "mcp",
|
|
464
|
+
result: {
|
|
465
|
+
content: [
|
|
466
|
+
{ type: "image", data: "aGk=", mimeType: "image/png" },
|
|
467
|
+
{ type: "audio", data: "aGk=", mimeType: "audio/wav" },
|
|
468
|
+
{
|
|
469
|
+
type: "resource",
|
|
470
|
+
resource: { uri: "file:///a.pdf", blob: "aGk=" },
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
expect(call.tool_result).not.toContain("aGk=");
|
|
476
|
+
expect(call.tool_result).toContain("[image:");
|
|
477
|
+
expect(call.tool_result).toContain("[audio:");
|
|
478
|
+
expect(call.tool_result).toContain("[resource:");
|
|
479
|
+
// a field the grammar does not define as base64 is left alone
|
|
480
|
+
expect(call.tool_result).toContain("image/png");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("leaves plain text in an mcp result untouched", () => {
|
|
484
|
+
const call = createRunTelemetryToolCall({
|
|
485
|
+
toolName: "t",
|
|
486
|
+
toolCallId: "call-1",
|
|
487
|
+
toolSource: "mcp",
|
|
488
|
+
result: { content: [{ type: "text", text: "test" }] },
|
|
489
|
+
});
|
|
490
|
+
expect(call.tool_result).toContain("test");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("summarizes base64 blocks inside an mcp CallToolResult envelope", () => {
|
|
494
|
+
// what @modelcontextprotocol/sdk callTool actually returns
|
|
495
|
+
const call = createRunTelemetryToolCall({
|
|
496
|
+
toolName: "t",
|
|
497
|
+
toolCallId: "call-1",
|
|
498
|
+
toolSource: "mcp",
|
|
499
|
+
result: {
|
|
500
|
+
content: [
|
|
501
|
+
{ type: "text", text: "keep me" },
|
|
502
|
+
{ type: "image", data: "A".repeat(4096) },
|
|
503
|
+
],
|
|
504
|
+
isError: false,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(call.tool_result).toContain("keep me");
|
|
509
|
+
expect(call.tool_result).toContain("[image: 3.0KB]");
|
|
510
|
+
expect(call.tool_result).not.toContain("A".repeat(200));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("keeps the envelope's sibling fields when summarizing its content", () => {
|
|
514
|
+
const call = createRunTelemetryToolCall({
|
|
515
|
+
toolName: "t",
|
|
516
|
+
toolCallId: "call-1",
|
|
517
|
+
toolSource: "mcp",
|
|
518
|
+
result: {
|
|
519
|
+
content: [{ type: "image", data: "A".repeat(4096) }],
|
|
520
|
+
isError: true,
|
|
521
|
+
structuredContent: { ok: false },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(call.tool_result).toContain("[image: 3.0KB]");
|
|
526
|
+
expect(call.tool_result).toContain('"isError":true');
|
|
527
|
+
expect(call.tool_result).toContain('"structuredContent":{"ok":false}');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("summarizes a CallToolResult that arrived as a JSON string", () => {
|
|
531
|
+
const call = createRunTelemetryToolCall({
|
|
532
|
+
toolName: "t",
|
|
533
|
+
toolCallId: "call-1",
|
|
534
|
+
toolSource: "mcp",
|
|
535
|
+
result: JSON.stringify({
|
|
536
|
+
content: [{ type: "audio", data: "A".repeat(4096) }],
|
|
537
|
+
}),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
expect(call.tool_result).toContain("[audio: 3.0KB]");
|
|
541
|
+
expect(call.tool_result).not.toContain("A".repeat(200));
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("leaves an object without content blocks alone", () => {
|
|
545
|
+
const call = createRunTelemetryToolCall({
|
|
546
|
+
toolName: "t",
|
|
547
|
+
toolCallId: "call-1",
|
|
548
|
+
toolSource: "mcp",
|
|
549
|
+
result: { content: "not blocks", other: 1 },
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(call.tool_result).toContain('"content":"not blocks"');
|
|
553
|
+
expect(call.tool_result).toContain('"other":1');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("summarizes the base64 blob of an embedded resource", () => {
|
|
557
|
+
const call = createRunTelemetryToolCall({
|
|
558
|
+
toolName: "t",
|
|
559
|
+
toolCallId: "call-1",
|
|
560
|
+
toolSource: "mcp",
|
|
561
|
+
result: {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: "resource",
|
|
565
|
+
resource: {
|
|
566
|
+
uri: "file:///report.pdf",
|
|
567
|
+
mimeType: "application/pdf",
|
|
568
|
+
blob: "A".repeat(4096),
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
expect(call.tool_result).toContain("[resource: 3.0KB]");
|
|
576
|
+
expect(call.tool_result).toContain("file:///report.pdf");
|
|
577
|
+
expect(call.tool_result).not.toContain("A".repeat(200));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("leaves a text-bearing embedded resource alone", () => {
|
|
581
|
+
const call = createRunTelemetryToolCall({
|
|
582
|
+
toolName: "t",
|
|
583
|
+
toolCallId: "call-1",
|
|
584
|
+
toolSource: "mcp",
|
|
585
|
+
result: {
|
|
586
|
+
content: [
|
|
587
|
+
{
|
|
588
|
+
type: "resource",
|
|
589
|
+
resource: { uri: "file:///a.txt", text: "plain text" },
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(call.tool_result).toContain("plain text");
|
|
596
|
+
});
|
|
597
|
+
|
|
374
598
|
it("leaves a non-mcp result unsummarized", () => {
|
|
375
599
|
const result = [{ type: "image", data: "A".repeat(4096) }];
|
|
376
600
|
const call = createRunTelemetryToolCall({
|
package/src/runTelemetry.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AssistantCloudRunReport } from "./AssistantCloudRuns";
|
|
|
3
3
|
|
|
4
4
|
const MAX_TELEMETRY_TEXT_LENGTH = 50_000;
|
|
5
5
|
|
|
6
|
-
const BASE64_PATTERN = /^[A-Za-z0-9+/]{
|
|
6
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
7
7
|
|
|
8
8
|
export type AssistantCloudRunReportToolCall = {
|
|
9
9
|
tool_name: string;
|
|
@@ -114,28 +114,81 @@ function safeStringify(value: unknown): string | undefined {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
const base64SizeKB = (value: string) =>
|
|
118
|
+
((value.length * 3) / 4 / 1024).toFixed(1);
|
|
119
|
+
|
|
120
|
+
// Both call sites read a field the MCP content grammar already defines as
|
|
121
|
+
// base64, so the test only has to recognise the encoding's own shape: its
|
|
122
|
+
// alphabet, and a length that is a multiple of four. A size floor would leave
|
|
123
|
+
// a short payload — `aGk=` is a whole image in this repo's own fixtures —
|
|
124
|
+
// serialized raw.
|
|
125
|
+
const isInlineBase64 = (value: unknown): value is string => {
|
|
126
|
+
if (typeof value !== "string") return false;
|
|
127
|
+
const head = value.slice(0, 200);
|
|
128
|
+
return head.length > 0 && head.length % 4 === 0 && BASE64_PATTERN.test(head);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function summarizeMcpContentBlock(item: unknown): unknown {
|
|
132
|
+
if (!item || typeof item !== "object") return item;
|
|
133
|
+
const block = item as {
|
|
134
|
+
type?: unknown;
|
|
135
|
+
data?: unknown;
|
|
136
|
+
resource?: unknown;
|
|
137
|
+
};
|
|
138
|
+
if (
|
|
139
|
+
(block.type === "image" || block.type === "audio") &&
|
|
140
|
+
isInlineBase64(block.data)
|
|
141
|
+
) {
|
|
142
|
+
return {
|
|
143
|
+
...block,
|
|
144
|
+
data: `[${String(block.type)}: ${base64SizeKB(block.data)}KB]`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// An EmbeddedResource is the third inline carrier: a binary resource arrives
|
|
148
|
+
// as base64 under `resource.blob` rather than as a top-level `data` field.
|
|
149
|
+
if (
|
|
150
|
+
block.type === "resource" &&
|
|
151
|
+
block.resource &&
|
|
152
|
+
typeof block.resource === "object"
|
|
153
|
+
) {
|
|
154
|
+
const resource = block.resource as { blob?: unknown };
|
|
155
|
+
if (isInlineBase64(resource.blob)) {
|
|
156
|
+
return {
|
|
157
|
+
...block,
|
|
158
|
+
resource: {
|
|
159
|
+
...resource,
|
|
160
|
+
blob: `[resource: ${base64SizeKB(resource.blob)}KB]`,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return item;
|
|
166
|
+
}
|
|
167
|
+
|
|
117
168
|
function summarizeMcpResult(value: unknown): string | undefined {
|
|
118
169
|
if (value == null) return undefined;
|
|
119
170
|
try {
|
|
120
171
|
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
121
172
|
if (Array.isArray(parsed)) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
173
|
+
return truncateRunTelemetryText(
|
|
174
|
+
JSON.stringify(parsed.map(summarizeMcpContentBlock)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
// `callTool` resolves to a CallToolResult, so the blocks carrying base64
|
|
178
|
+
// arrive under `content` rather than as the result itself.
|
|
179
|
+
if (typeof parsed === "object") {
|
|
180
|
+
const content = (parsed as { content?: unknown }).content;
|
|
181
|
+
if (Array.isArray(content)) {
|
|
182
|
+
return truncateRunTelemetryText(
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
...(parsed as Record<string, unknown>),
|
|
185
|
+
content: content.map(summarizeMcpContentBlock),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
136
189
|
}
|
|
137
190
|
} catch {
|
|
138
|
-
// not
|
|
191
|
+
// not a shape with summarizable content, fall through
|
|
139
192
|
}
|
|
140
193
|
return safeStringify(value);
|
|
141
194
|
}
|
|
@@ -223,6 +276,7 @@ export type RunReportStepInit = {
|
|
|
223
276
|
startMs?: number | undefined;
|
|
224
277
|
endMs?: number | undefined;
|
|
225
278
|
finishReason?: string | undefined;
|
|
279
|
+
input?: string | undefined;
|
|
226
280
|
};
|
|
227
281
|
|
|
228
282
|
/**
|
|
@@ -245,11 +299,12 @@ export type RunMessageTelemetry = {
|
|
|
245
299
|
export type RunReportInit = {
|
|
246
300
|
threadId: string;
|
|
247
301
|
status: AssistantCloudRunReport["status"];
|
|
248
|
-
outcome?:
|
|
302
|
+
outcome?: AssistantCloudRunReport["outcome_type"] | undefined;
|
|
249
303
|
errorCode?: string | undefined;
|
|
250
304
|
error?: string | undefined;
|
|
251
305
|
messageId?: string | undefined;
|
|
252
306
|
traceId?: string | undefined;
|
|
307
|
+
rootSpanId?: string | undefined;
|
|
253
308
|
modelId?: string | undefined;
|
|
254
309
|
provider?: string | undefined;
|
|
255
310
|
usage?: RunTelemetryUsageInit | undefined;
|
|
@@ -258,7 +313,17 @@ export type RunReportInit = {
|
|
|
258
313
|
toolCalls?: AssistantCloudRunReportToolCall[] | undefined;
|
|
259
314
|
durationMs?: number | undefined;
|
|
260
315
|
firstTokenMs?: number | undefined;
|
|
316
|
+
costUsd?: number | undefined;
|
|
317
|
+
costDetails?:
|
|
318
|
+
| {
|
|
319
|
+
input?: number | undefined;
|
|
320
|
+
inputCachedTokens?: number | undefined;
|
|
321
|
+
output?: number | undefined;
|
|
322
|
+
total?: number | undefined;
|
|
323
|
+
}
|
|
324
|
+
| undefined;
|
|
261
325
|
outputText?: string | undefined;
|
|
326
|
+
attributes?: Record<string, unknown> | undefined;
|
|
262
327
|
metadata?: Record<string, unknown> | undefined;
|
|
263
328
|
telemetry?: {
|
|
264
329
|
environment?: string | undefined;
|
|
@@ -308,6 +373,9 @@ function createRunReportStep(
|
|
|
308
373
|
if (init.finishReason !== undefined) {
|
|
309
374
|
step.finish_reason = init.finishReason.slice(0, 32);
|
|
310
375
|
}
|
|
376
|
+
if (init.input !== undefined) {
|
|
377
|
+
step.input = truncateRunTelemetryText(init.input);
|
|
378
|
+
}
|
|
311
379
|
return step;
|
|
312
380
|
}
|
|
313
381
|
|
|
@@ -334,6 +402,34 @@ function normalizeRunReportMilliseconds(
|
|
|
334
402
|
return Math.max(0, Math.round(value));
|
|
335
403
|
}
|
|
336
404
|
|
|
405
|
+
function normalizeRunReportCost(value: number | undefined): number | undefined {
|
|
406
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function assignCostDetails(
|
|
413
|
+
report: AssistantCloudRunReport,
|
|
414
|
+
costDetails: RunReportInit["costDetails"],
|
|
415
|
+
): void {
|
|
416
|
+
if (!costDetails) return;
|
|
417
|
+
const normalized: NonNullable<AssistantCloudRunReport["cost_details"]> = {};
|
|
418
|
+
const input = normalizeRunReportCost(costDetails.input);
|
|
419
|
+
if (input !== undefined) normalized.input = input;
|
|
420
|
+
const inputCachedTokens = normalizeRunReportCost(
|
|
421
|
+
costDetails.inputCachedTokens,
|
|
422
|
+
);
|
|
423
|
+
if (inputCachedTokens !== undefined) {
|
|
424
|
+
normalized.input_cached_tokens = inputCachedTokens;
|
|
425
|
+
}
|
|
426
|
+
const output = normalizeRunReportCost(costDetails.output);
|
|
427
|
+
if (output !== undefined) normalized.output = output;
|
|
428
|
+
const total = normalizeRunReportCost(costDetails.total);
|
|
429
|
+
if (total !== undefined) normalized.total = total;
|
|
430
|
+
if (Object.keys(normalized).length > 0) report.cost_details = normalized;
|
|
431
|
+
}
|
|
432
|
+
|
|
337
433
|
export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
|
|
338
434
|
const report: AssistantCloudRunReport = {
|
|
339
435
|
thread_id: init.threadId,
|
|
@@ -341,6 +437,10 @@ export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
|
|
|
341
437
|
};
|
|
342
438
|
const traceId = init.traceId?.toLowerCase();
|
|
343
439
|
if (traceId && /^[0-9a-f]{32}$/.test(traceId)) report.trace_id = traceId;
|
|
440
|
+
const rootSpanId = init.rootSpanId?.toLowerCase();
|
|
441
|
+
if (rootSpanId && /^[0-9a-f]{16}$/.test(rootSpanId)) {
|
|
442
|
+
report.root_span_id = rootSpanId;
|
|
443
|
+
}
|
|
344
444
|
if (init.outcome !== undefined) report.outcome_type = init.outcome;
|
|
345
445
|
if (init.errorCode !== undefined) report.error_code = init.errorCode;
|
|
346
446
|
if (init.error !== undefined) report.error = init.error;
|
|
@@ -362,9 +462,13 @@ export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
|
|
|
362
462
|
if (durationMs !== undefined) report.duration_ms = durationMs;
|
|
363
463
|
const firstTokenMs = normalizeRunReportMilliseconds(init.firstTokenMs);
|
|
364
464
|
if (firstTokenMs !== undefined) report.first_token_ms = firstTokenMs;
|
|
465
|
+
const costUsd = normalizeRunReportCost(init.costUsd);
|
|
466
|
+
if (costUsd !== undefined) report.cost_usd = costUsd;
|
|
467
|
+
assignCostDetails(report, init.costDetails);
|
|
365
468
|
if (init.outputText !== undefined) {
|
|
366
469
|
report.output_text = truncateRunTelemetryText(init.outputText);
|
|
367
470
|
}
|
|
471
|
+
if (init.attributes !== undefined) report.attributes = init.attributes;
|
|
368
472
|
if (init.metadata !== undefined) report.metadata = init.metadata;
|
|
369
473
|
if (init.telemetry?.environment !== undefined) {
|
|
370
474
|
report.environment = init.telemetry.environment;
|
|
@@ -14,6 +14,7 @@ const createCloud = (
|
|
|
14
14
|
|
|
15
15
|
describe("AssistantCloud telemetry config", () => {
|
|
16
16
|
afterEach(() => {
|
|
17
|
+
vi.useRealTimers();
|
|
17
18
|
vi.unstubAllGlobals();
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -34,6 +35,27 @@ describe("AssistantCloud telemetry config", () => {
|
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
|
|
38
|
+
it.each(["enabled", "events"] as const)(
|
|
39
|
+
"does not resume pending events after telemetry %s is disabled",
|
|
40
|
+
async (property) => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
const fetchMock = vi.fn().mockRejectedValueOnce(new Error("offline"));
|
|
43
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
44
|
+
const cloud = createCloud();
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < 20; index++) {
|
|
47
|
+
cloud.events.track({ kind: "message_sent" });
|
|
48
|
+
}
|
|
49
|
+
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce());
|
|
50
|
+
|
|
51
|
+
cloud.telemetry[property] = false;
|
|
52
|
+
cloud.telemetry[property] = true;
|
|
53
|
+
await vi.runAllTimersAsync();
|
|
54
|
+
|
|
55
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
37
59
|
it("stays enabled when the config object carries an undefined enabled", () => {
|
|
38
60
|
const beforeReport: NonNullable<
|
|
39
61
|
AssistantCloudTelemetryConfig["beforeReport"]
|
|
@@ -90,6 +90,34 @@ describe("AssistantCloudRuns", () => {
|
|
|
90
90
|
);
|
|
91
91
|
expect(cancel).toHaveBeenCalledOnce();
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
it("pins the UI message stream protocol in assistant options", () => {
|
|
95
|
+
const { protocol } =
|
|
96
|
+
createCloud().runs.__internal_getAssistantOptions("assistant-id");
|
|
97
|
+
|
|
98
|
+
expect(protocol).toBe("ui-message-stream");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses the requested thread ID in assistant options", async () => {
|
|
102
|
+
const { body } =
|
|
103
|
+
createCloud().runs.__internal_getAssistantOptions("assistant-id");
|
|
104
|
+
|
|
105
|
+
await expect(body({ threadId: "remote-thread" })).resolves.toEqual({
|
|
106
|
+
assistant_id: "assistant-id",
|
|
107
|
+
response_format: "vercel-ai-data-stream/v1",
|
|
108
|
+
thread_id: "remote-thread",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects assistant options without a thread ID", async () => {
|
|
113
|
+
const { body } =
|
|
114
|
+
createCloud().runs.__internal_getAssistantOptions("assistant-id");
|
|
115
|
+
|
|
116
|
+
await expect(body({})).rejects.toThrow(
|
|
117
|
+
"Assistant Cloud runs need a thread",
|
|
118
|
+
);
|
|
119
|
+
await expect(body()).rejects.toThrow("Assistant Cloud runs need a thread");
|
|
120
|
+
});
|
|
93
121
|
});
|
|
94
122
|
|
|
95
123
|
const createCloudRuns = () => {
|
|
@@ -47,6 +47,24 @@ describe("CloudEngagementReporter", () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
it("drops an event its resolver declines", async () => {
|
|
51
|
+
const { cloud, track } = createCloud();
|
|
52
|
+
const reporter = new CloudEngagementReporter(cloud, (threadId) =>
|
|
53
|
+
threadId === "known" ? { thread_id: "remote-known" } : undefined,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
reporter.messageSent("unknown", { chars: 3, attachments: 0 });
|
|
57
|
+
reporter.messageSent("known", { chars: 5, attachments: 0 });
|
|
58
|
+
await flush();
|
|
59
|
+
|
|
60
|
+
expect(track).toHaveBeenCalledOnce();
|
|
61
|
+
expect(track).toHaveBeenCalledWith({
|
|
62
|
+
kind: "message_sent",
|
|
63
|
+
thread_id: "remote-known",
|
|
64
|
+
props: { chars: 5, attachments: 0 },
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
50
68
|
it("reports tool approval decisions with their resolved message IDs", async () => {
|
|
51
69
|
const { cloud, track } = createCloud();
|
|
52
70
|
const reporter = new CloudEngagementReporter(
|
|
@@ -113,6 +131,33 @@ describe("CloudEngagementReporter", () => {
|
|
|
113
131
|
);
|
|
114
132
|
});
|
|
115
133
|
|
|
134
|
+
it("measures the next send from a stopped run, not the last completed one", async () => {
|
|
135
|
+
const { cloud, track } = createCloud();
|
|
136
|
+
const reporter = new CloudEngagementReporter(cloud);
|
|
137
|
+
|
|
138
|
+
vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
|
|
139
|
+
reporter.runStarted("t1");
|
|
140
|
+
vi.setSystemTime(new Date("2023-01-01T00:01:40.000Z"));
|
|
141
|
+
reporter.runEnded("t1");
|
|
142
|
+
|
|
143
|
+
// a second run the user stops rather than lets finish
|
|
144
|
+
vi.setSystemTime(new Date("2023-01-01T00:03:20.000Z"));
|
|
145
|
+
reporter.runStarted("t1");
|
|
146
|
+
vi.setSystemTime(new Date("2023-01-01T00:05:00.000Z"));
|
|
147
|
+
reporter.runStopped("t1");
|
|
148
|
+
|
|
149
|
+
vi.setSystemTime(new Date("2023-01-01T00:05:10.000Z"));
|
|
150
|
+
reporter.messageSent("t1", { chars: 3, attachments: 0 });
|
|
151
|
+
await flush();
|
|
152
|
+
|
|
153
|
+
// a stopped run is still a run that ended, so the idle gap is the 10s
|
|
154
|
+
// since the stop, not the 210s since the last completed run
|
|
155
|
+
const sent = track.mock.calls
|
|
156
|
+
.map(([event]) => event as { kind: string; value?: number })
|
|
157
|
+
.find((event) => event.kind === "message_sent");
|
|
158
|
+
expect(sent?.value).toBe(10_000);
|
|
159
|
+
});
|
|
160
|
+
|
|
116
161
|
it("shows one error per run and one suggestion list per thread", async () => {
|
|
117
162
|
const { cloud, track } = createCloud();
|
|
118
163
|
const reporter = new CloudEngagementReporter(cloud);
|
|
@@ -21,7 +21,13 @@ describe("CloudRunReporter", () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("stamps the cloud's environment, release and tags and applies beforeReport last", async () => {
|
|
24
|
-
const beforeReport = vi.fn((report) => ({
|
|
24
|
+
const beforeReport = vi.fn((report) => ({
|
|
25
|
+
...report,
|
|
26
|
+
model_id: "gpt",
|
|
27
|
+
cost_usd: 0.012,
|
|
28
|
+
attributes: { tenant: "acme" },
|
|
29
|
+
root_span_id: "0011223344556677",
|
|
30
|
+
}));
|
|
25
31
|
const { cloud, report } = createCloud({
|
|
26
32
|
enabled: true,
|
|
27
33
|
environment: "production",
|
|
@@ -43,7 +49,13 @@ describe("CloudRunReporter", () => {
|
|
|
43
49
|
}),
|
|
44
50
|
);
|
|
45
51
|
expect(report).toHaveBeenCalledWith(
|
|
46
|
-
expect.objectContaining({
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
thread_id: "thread_1",
|
|
54
|
+
model_id: "gpt",
|
|
55
|
+
cost_usd: 0.012,
|
|
56
|
+
attributes: { tenant: "acme" },
|
|
57
|
+
root_span_id: "0011223344556677",
|
|
58
|
+
}),
|
|
47
59
|
);
|
|
48
60
|
});
|
|
49
61
|
|
package/dist/tests/setup.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/dist/tests/setup.js
DELETED
package/dist/tests/setup.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","names":[],"sources":["../../src/tests/setup.ts"],"sourcesContent":["import { vi } from \"vitest\";\n\nvi.useFakeTimers({ toFake: [\"Date\"] });\nvi.setSystemTime(new Date(\"2023-01-01T00:00:00.000Z\"));\n"],"mappings":";;AAEA,GAAG,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,GAAG,8BAAc,IAAI,KAAK,0BAA0B,CAAC"}
|