assistant-cloud 0.1.43 → 0.2.1
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 +66 -0
- package/dist/AssistantCloud.d.ts +6 -1
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +13 -5
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +28 -3
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +37 -10
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +15 -3
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +32 -0
- package/dist/AssistantCloudEvents.d.ts.map +1 -0
- package/dist/AssistantCloudEvents.js +119 -0
- package/dist/AssistantCloudEvents.js.map +1 -0
- package/dist/AssistantCloudFiles.d.ts +12 -1
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudFiles.js +13 -1
- package/dist/AssistantCloudFiles.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +12 -0
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +2 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/AssistantCloudScores.d.ts +27 -0
- package/dist/AssistantCloudScores.d.ts.map +1 -0
- package/dist/AssistantCloudScores.js +30 -0
- package/dist/AssistantCloudScores.js.map +1 -0
- package/dist/AssistantCloudThreadMessages.d.ts +11 -1
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +12 -1
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +8 -0
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/AssistantCloudThreads.js +9 -1
- package/dist/AssistantCloudThreads.js.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +74 -0
- package/dist/CloudEngagementReporter.d.ts.map +1 -0
- package/dist/CloudEngagementReporter.js +132 -0
- package/dist/CloudEngagementReporter.js.map +1 -0
- package/dist/CloudMessagePersistence.d.ts +12 -9
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +54 -20
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/CloudRunReporter.d.ts +20 -0
- package/dist/CloudRunReporter.d.ts.map +1 -0
- package/dist/CloudRunReporter.js +38 -0
- package/dist/CloudRunReporter.js.map +1 -0
- package/dist/FormattedCloudPersistence.d.ts +3 -8
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/FormattedCloudPersistence.js +3 -8
- package/dist/FormattedCloudPersistence.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +33 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +147 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/cloudResponse.d.ts +4 -1
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/cloudResponse.js +14 -1
- package/dist/cloudResponse.js.map +1 -1
- package/dist/generateThreadTitle.js +1 -0
- package/dist/generateThreadTitle.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/instrumentMcpSampling.js +12 -2
- package/dist/instrumentMcpSampling.js.map +1 -1
- package/dist/runTelemetry.d.ts +86 -7
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js +144 -1
- package/dist/runTelemetry.js.map +1 -1
- package/dist/telemetry/index.d.ts +32 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +62 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +43 -6
- package/src/AssistantCloud.ts +23 -8
- package/src/AssistantCloudAPI.ts +72 -10
- package/src/AssistantCloudAuthStrategy.ts +17 -2
- package/src/AssistantCloudEvents.test.ts +149 -0
- package/src/AssistantCloudEvents.ts +190 -0
- package/src/AssistantCloudFiles.test.ts +59 -0
- package/src/AssistantCloudFiles.ts +39 -0
- package/src/AssistantCloudRuns.ts +13 -1
- package/src/AssistantCloudScores.test.ts +113 -0
- package/src/AssistantCloudScores.ts +61 -0
- package/src/AssistantCloudThreadMessages.test.ts +51 -0
- package/src/AssistantCloudThreadMessages.ts +33 -0
- package/src/AssistantCloudThreads.test.ts +21 -0
- package/src/AssistantCloudThreads.ts +21 -0
- package/src/CloudEngagementReporter.ts +231 -0
- package/src/CloudMessagePersistence.ts +70 -27
- package/src/CloudRunReporter.ts +38 -0
- package/src/FormattedCloudPersistence.ts +3 -8
- package/src/ai-sdk/index.test.ts +258 -0
- package/src/ai-sdk/index.ts +222 -0
- package/src/cloudResponse.test.ts +34 -1
- package/src/cloudResponse.ts +30 -0
- package/src/generateThreadTitle.test.ts +32 -0
- package/src/generateThreadTitle.ts +1 -0
- package/src/index.ts +35 -2
- package/src/instrumentMcpSampling.test.ts +102 -0
- package/src/instrumentMcpSampling.ts +16 -2
- package/src/runTelemetry.test.ts +325 -0
- package/src/runTelemetry.ts +274 -6
- package/src/telemetry/index.test.ts +252 -0
- package/src/telemetry/index.ts +98 -0
- package/src/tests/AssistantCloud.test.ts +55 -1
- package/src/tests/AssistantCloudAPI.test.ts +95 -4
- package/src/tests/AssistantCloudAuthStrategy.test.ts +93 -2
- package/src/tests/AssistantCloudProjects.test.ts +1 -5
- package/src/tests/CloudEngagementReporter.test.ts +184 -0
- package/src/tests/CloudMessagePersistence.test.ts +350 -0
- package/src/tests/CloudRunReporter.test.ts +100 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
wrapSamplingHandler,
|
|
4
|
+
createSamplingCollector,
|
|
5
|
+
type McpSamplingRequest,
|
|
6
|
+
} from "./instrumentMcpSampling";
|
|
7
|
+
|
|
8
|
+
const request: McpSamplingRequest = {
|
|
9
|
+
method: "sampling/createMessage",
|
|
10
|
+
params: { messages: [], maxTokens: 10 },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("wrapSamplingHandler", () => {
|
|
14
|
+
it("returns the response and records sampling metrics", async () => {
|
|
15
|
+
const collector = createSamplingCollector();
|
|
16
|
+
const response = {
|
|
17
|
+
model: "test-model",
|
|
18
|
+
content: { type: "text", text: "hello" },
|
|
19
|
+
usage: { inputTokens: 2, outputTokens: 3 },
|
|
20
|
+
};
|
|
21
|
+
const handler = vi.fn(async () => response);
|
|
22
|
+
await expect(
|
|
23
|
+
wrapSamplingHandler(handler, collector.collect)(request),
|
|
24
|
+
).resolves.toBe(response);
|
|
25
|
+
expect(handler).toHaveBeenCalledWith(request);
|
|
26
|
+
expect(collector.getCalls()).toEqual([
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
model_id: "test-model",
|
|
29
|
+
input_tokens: 2,
|
|
30
|
+
output_tokens: 3,
|
|
31
|
+
duration_ms: expect.any(Number),
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each([false, true])(
|
|
37
|
+
"preserves successful responses when the observer fails asynchronously=%s",
|
|
38
|
+
async (asynchronous) => {
|
|
39
|
+
const error = new Error("metrics unavailable");
|
|
40
|
+
const observer = asynchronous
|
|
41
|
+
? async () => {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
: () => {
|
|
45
|
+
throw error;
|
|
46
|
+
};
|
|
47
|
+
const response = { content: "successful response" };
|
|
48
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
49
|
+
try {
|
|
50
|
+
await expect(
|
|
51
|
+
wrapSamplingHandler(async () => response, observer)(request),
|
|
52
|
+
).resolves.toBe(response);
|
|
53
|
+
expect(log).toHaveBeenCalledWith(
|
|
54
|
+
"[assistant-cloud] onSamplingCall callback threw an error",
|
|
55
|
+
error,
|
|
56
|
+
);
|
|
57
|
+
} finally {
|
|
58
|
+
log.mockRestore();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
it("does not await an asynchronous observer", async () => {
|
|
64
|
+
const response = { content: "successful response" };
|
|
65
|
+
await expect(
|
|
66
|
+
wrapSamplingHandler(
|
|
67
|
+
async () => response,
|
|
68
|
+
() => new Promise<void>(() => {}),
|
|
69
|
+
)(request),
|
|
70
|
+
).resolves.toBe(response);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("propagates model errors without calling the observer", async () => {
|
|
74
|
+
const error = new Error("model failed");
|
|
75
|
+
const observer = vi.fn();
|
|
76
|
+
await expect(
|
|
77
|
+
wrapSamplingHandler(async () => {
|
|
78
|
+
throw error;
|
|
79
|
+
}, observer)(request),
|
|
80
|
+
).rejects.toBe(error);
|
|
81
|
+
expect(observer).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("preserves successful responses even if error reporting throws", async () => {
|
|
85
|
+
const response = { content: "successful response" };
|
|
86
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {
|
|
87
|
+
throw new Error("logging failed");
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
await expect(
|
|
91
|
+
wrapSamplingHandler(
|
|
92
|
+
async () => response,
|
|
93
|
+
() => {
|
|
94
|
+
throw new Error("metrics failed");
|
|
95
|
+
},
|
|
96
|
+
)(request),
|
|
97
|
+
).resolves.toBe(response);
|
|
98
|
+
} finally {
|
|
99
|
+
log.mockRestore();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -43,6 +43,15 @@ export type McpSamplingResponse = {
|
|
|
43
43
|
[key: string]: unknown;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const reportSamplingError = (error: unknown): void => {
|
|
47
|
+
try {
|
|
48
|
+
console.error(
|
|
49
|
+
"[assistant-cloud] onSamplingCall callback threw an error",
|
|
50
|
+
error,
|
|
51
|
+
);
|
|
52
|
+
} catch {}
|
|
53
|
+
};
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* Wraps an MCP sampling handler to intercept and measure sampling calls.
|
|
48
57
|
*
|
|
@@ -80,7 +89,7 @@ export function wrapSamplingHandler(
|
|
|
80
89
|
const reasoningTokens = response.usage?.reasoningTokens;
|
|
81
90
|
const cachedInputTokens = response.usage?.cachedInputTokens;
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
const data: SamplingCallData = {
|
|
84
93
|
...(modelId ? { model_id: modelId } : undefined),
|
|
85
94
|
...(inputTokens != null ? { input_tokens: inputTokens } : undefined),
|
|
86
95
|
...(outputTokens != null ? { output_tokens: outputTokens } : undefined),
|
|
@@ -91,7 +100,12 @@ export function wrapSamplingHandler(
|
|
|
91
100
|
? { cached_input_tokens: cachedInputTokens }
|
|
92
101
|
: undefined),
|
|
93
102
|
duration_ms: durationMs,
|
|
94
|
-
}
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
void Promise.resolve(onSamplingCall(data)).catch(reportSamplingError);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reportSamplingError(error);
|
|
108
|
+
}
|
|
95
109
|
|
|
96
110
|
return response;
|
|
97
111
|
};
|
package/src/runTelemetry.test.ts
CHANGED
|
@@ -1,12 +1,296 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
createRunReport,
|
|
3
4
|
createRunTelemetryToolCall,
|
|
5
|
+
deriveRunOutcome,
|
|
6
|
+
describeRunError,
|
|
7
|
+
extractRunTelemetryModelId,
|
|
4
8
|
normalizeRunTelemetryUsage,
|
|
5
9
|
truncateRunTelemetryText,
|
|
6
10
|
} from "./runTelemetry";
|
|
7
11
|
|
|
8
12
|
const MAX = 50_000;
|
|
9
13
|
|
|
14
|
+
describe("deriveRunOutcome", () => {
|
|
15
|
+
it.each([
|
|
16
|
+
[{ isError: true }, { status: "error" }],
|
|
17
|
+
[{ isAbort: true }, { status: "incomplete", outcome: "aborted" }],
|
|
18
|
+
[{ isDisconnect: true }, { status: "incomplete", outcome: "disconnected" }],
|
|
19
|
+
[{ finishReason: "length" }, { status: "incomplete", outcome: "length" }],
|
|
20
|
+
[
|
|
21
|
+
{ finishReason: "content-filter" },
|
|
22
|
+
{ status: "incomplete", outcome: "content_filter" },
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
{ finishReason: "content_filter" },
|
|
26
|
+
{ status: "incomplete", outcome: "content_filter" },
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{ finishReason: "cancelled" },
|
|
30
|
+
{ status: "incomplete", outcome: "aborted" },
|
|
31
|
+
],
|
|
32
|
+
[{ finishReason: "stop" }, { status: "completed" }],
|
|
33
|
+
[{ finishReason: "tool-calls" }, { status: "completed" }],
|
|
34
|
+
[{}, { status: "completed" }],
|
|
35
|
+
[{ finishReason: "other" }, { status: "completed" }],
|
|
36
|
+
[{ finishReason: "error" }, { status: "error" }],
|
|
37
|
+
])("maps %o", (input, expected) => {
|
|
38
|
+
expect(deriveRunOutcome(input)).toEqual(expected);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("uses the fallback status only when the event carries no finish reason", () => {
|
|
42
|
+
expect(deriveRunOutcome({}, "incomplete")).toEqual({
|
|
43
|
+
status: "incomplete",
|
|
44
|
+
});
|
|
45
|
+
expect(deriveRunOutcome({ finishReason: "stop" }, "incomplete")).toEqual({
|
|
46
|
+
status: "completed",
|
|
47
|
+
});
|
|
48
|
+
expect(deriveRunOutcome({ finishReason: "length" }, "incomplete")).toEqual({
|
|
49
|
+
status: "incomplete",
|
|
50
|
+
outcome: "length",
|
|
51
|
+
});
|
|
52
|
+
expect(deriveRunOutcome({ isError: true }, "incomplete")).toEqual({
|
|
53
|
+
status: "error",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("describeRunError", () => {
|
|
59
|
+
it("reads the message and the class name of an AI SDK error", () => {
|
|
60
|
+
const error = new Error("Rate limited");
|
|
61
|
+
error.name = "AI_APICallError";
|
|
62
|
+
expect(describeRunError(error)).toEqual({
|
|
63
|
+
error: "Rate limited",
|
|
64
|
+
errorCode: "AI_APICallError",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("prefers an explicit code and skips the plain Error name", () => {
|
|
69
|
+
expect(
|
|
70
|
+
describeRunError(Object.assign(new Error("boom"), { code: "ETIMEDOUT" })),
|
|
71
|
+
).toEqual({ error: "boom", errorCode: "ETIMEDOUT" });
|
|
72
|
+
expect(describeRunError(new Error("boom"))).toEqual({ error: "boom" });
|
|
73
|
+
expect(describeRunError("boom")).toEqual({ error: "boom" });
|
|
74
|
+
expect(describeRunError(undefined)).toEqual({});
|
|
75
|
+
expect(describeRunError(42)).toEqual({});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("clamps the message and code to what the runs endpoint accepts", () => {
|
|
79
|
+
const error = new Error("m".repeat(3000));
|
|
80
|
+
error.name = "c".repeat(100);
|
|
81
|
+
expect(describeRunError(error)).toEqual({
|
|
82
|
+
error: "m".repeat(2048),
|
|
83
|
+
errorCode: "c".repeat(64),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("createRunReport", () => {
|
|
89
|
+
const toolCall = {
|
|
90
|
+
tool_name: "weather",
|
|
91
|
+
tool_call_id: "call-1",
|
|
92
|
+
tool_args: '{"city":"Singapore"}',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
it("creates the full aui/v0 report", () => {
|
|
96
|
+
expect(
|
|
97
|
+
createRunReport({
|
|
98
|
+
threadId: "aui-thread",
|
|
99
|
+
status: "incomplete",
|
|
100
|
+
outcome: "length",
|
|
101
|
+
errorCode: "model_limit",
|
|
102
|
+
error: "response cut off",
|
|
103
|
+
messageId: "cloud-message",
|
|
104
|
+
traceId: "AABBCCDDEEFF00112233445566778899",
|
|
105
|
+
modelId: "provider/model",
|
|
106
|
+
provider: "openai",
|
|
107
|
+
usage: {
|
|
108
|
+
promptTokens: 11,
|
|
109
|
+
completionTokens: 7,
|
|
110
|
+
reasoningTokens: 3,
|
|
111
|
+
cachedInputTokens: 2,
|
|
112
|
+
},
|
|
113
|
+
steps: [
|
|
114
|
+
{
|
|
115
|
+
usage: { inputTokens: 5, outputTokens: 4 },
|
|
116
|
+
toolCalls: [toolCall],
|
|
117
|
+
startMs: 10,
|
|
118
|
+
endMs: 20,
|
|
119
|
+
finishReason: "tool-calls",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
usage: { inputTokens: 6, outputTokens: 3, reasoningTokens: 3 },
|
|
123
|
+
startMs: 21,
|
|
124
|
+
endMs: 42,
|
|
125
|
+
finishReason: "length".repeat(8),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
toolCalls: [toolCall],
|
|
129
|
+
durationMs: 42.5,
|
|
130
|
+
firstTokenMs: -4,
|
|
131
|
+
outputText: "hello from aui/v0",
|
|
132
|
+
metadata: { tenant: "acme" },
|
|
133
|
+
telemetry: {
|
|
134
|
+
environment: "production",
|
|
135
|
+
release: "web-2026.09.09",
|
|
136
|
+
tags: [" region:sg ", "region:sg", "tier:paid"],
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
).toEqual({
|
|
140
|
+
thread_id: "aui-thread",
|
|
141
|
+
status: "incomplete",
|
|
142
|
+
outcome_type: "length",
|
|
143
|
+
error_code: "model_limit",
|
|
144
|
+
error: "response cut off",
|
|
145
|
+
message_id: "cloud-message",
|
|
146
|
+
trace_id: "aabbccddeeff00112233445566778899",
|
|
147
|
+
model_id: "provider/model",
|
|
148
|
+
provider: "openai",
|
|
149
|
+
provider_type: "openai",
|
|
150
|
+
input_tokens: 11,
|
|
151
|
+
output_tokens: 7,
|
|
152
|
+
reasoning_tokens: 3,
|
|
153
|
+
cached_input_tokens: 2,
|
|
154
|
+
steps: [
|
|
155
|
+
{
|
|
156
|
+
input_tokens: 5,
|
|
157
|
+
output_tokens: 4,
|
|
158
|
+
tool_calls: [toolCall],
|
|
159
|
+
start_ms: 10,
|
|
160
|
+
end_ms: 20,
|
|
161
|
+
finish_reason: "tool-calls",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
input_tokens: 6,
|
|
165
|
+
output_tokens: 3,
|
|
166
|
+
reasoning_tokens: 3,
|
|
167
|
+
start_ms: 21,
|
|
168
|
+
end_ms: 42,
|
|
169
|
+
finish_reason: "lengthlengthlengthlengthlengthle",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
total_steps: 2,
|
|
173
|
+
tool_calls: [toolCall],
|
|
174
|
+
duration_ms: 43,
|
|
175
|
+
first_token_ms: 0,
|
|
176
|
+
output_text: "hello from aui/v0",
|
|
177
|
+
metadata: { tenant: "acme" },
|
|
178
|
+
environment: "production",
|
|
179
|
+
release: "web-2026.09.09",
|
|
180
|
+
tags: ["region:sg", "tier:paid"],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("creates the full ai-sdk/v6 report", () => {
|
|
185
|
+
expect(
|
|
186
|
+
createRunReport({
|
|
187
|
+
threadId: "ai-sdk-thread",
|
|
188
|
+
status: "completed",
|
|
189
|
+
traceId: "00112233445566778899aabbccddeeff",
|
|
190
|
+
modelId: "gpt-5.6-terra",
|
|
191
|
+
provider: "gateway",
|
|
192
|
+
usage: {
|
|
193
|
+
inputTokens: 100,
|
|
194
|
+
outputTokens: 50,
|
|
195
|
+
inputTokenDetails: { cacheReadTokens: 10 },
|
|
196
|
+
outputTokenDetails: { reasoningTokens: 20 },
|
|
197
|
+
},
|
|
198
|
+
steps: [
|
|
199
|
+
{
|
|
200
|
+
usage: {
|
|
201
|
+
inputTokens: 100,
|
|
202
|
+
outputTokens: 50,
|
|
203
|
+
inputTokenDetails: { cacheReadTokens: 10 },
|
|
204
|
+
outputTokenDetails: { reasoningTokens: 20 },
|
|
205
|
+
},
|
|
206
|
+
finishReason: "stop",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
durationMs: 125.2,
|
|
210
|
+
firstTokenMs: 12.6,
|
|
211
|
+
outputText: "hello from ai-sdk/v6",
|
|
212
|
+
telemetry: { tags: ["sdk:v6"] },
|
|
213
|
+
}),
|
|
214
|
+
).toEqual({
|
|
215
|
+
thread_id: "ai-sdk-thread",
|
|
216
|
+
status: "completed",
|
|
217
|
+
trace_id: "00112233445566778899aabbccddeeff",
|
|
218
|
+
model_id: "gpt-5.6-terra",
|
|
219
|
+
provider: "gateway",
|
|
220
|
+
provider_type: "gateway",
|
|
221
|
+
input_tokens: 100,
|
|
222
|
+
output_tokens: 50,
|
|
223
|
+
reasoning_tokens: 20,
|
|
224
|
+
cached_input_tokens: 10,
|
|
225
|
+
steps: [
|
|
226
|
+
{
|
|
227
|
+
input_tokens: 100,
|
|
228
|
+
output_tokens: 50,
|
|
229
|
+
reasoning_tokens: 20,
|
|
230
|
+
cached_input_tokens: 10,
|
|
231
|
+
finish_reason: "stop",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
total_steps: 1,
|
|
235
|
+
duration_ms: 125,
|
|
236
|
+
first_token_ms: 13,
|
|
237
|
+
output_text: "hello from ai-sdk/v6",
|
|
238
|
+
tags: ["sdk:v6"],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("normalizes tags and limits them to twenty 64-character values", () => {
|
|
243
|
+
const longTag = "a".repeat(70);
|
|
244
|
+
const tags = [" one ", "", "one", longTag];
|
|
245
|
+
for (let index = 0; index < 24; index++) tags.push(`tag:${index}`);
|
|
246
|
+
|
|
247
|
+
const report = createRunReport({
|
|
248
|
+
threadId: "thread",
|
|
249
|
+
status: "completed",
|
|
250
|
+
telemetry: { tags },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(report.tags).toEqual([
|
|
254
|
+
"one",
|
|
255
|
+
"a".repeat(64),
|
|
256
|
+
...Array.from({ length: 18 }, (_, index) => `tag:${index}`),
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("trims the whitespace a 64-character cut leaves behind", () => {
|
|
261
|
+
const report = createRunReport({
|
|
262
|
+
threadId: "thread",
|
|
263
|
+
status: "completed",
|
|
264
|
+
telemetry: { tags: [`${"b".repeat(63)} tail`] },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(report.tags).toEqual(["b".repeat(63)]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("lowercases valid trace IDs, omits invalid IDs, and omits undefined keys", () => {
|
|
271
|
+
expect(
|
|
272
|
+
createRunReport({
|
|
273
|
+
threadId: "thread",
|
|
274
|
+
status: "completed",
|
|
275
|
+
traceId: "AABBCCDDEEFF00112233445566778899",
|
|
276
|
+
}).trace_id,
|
|
277
|
+
).toBe("aabbccddeeff00112233445566778899");
|
|
278
|
+
expect(
|
|
279
|
+
createRunReport({
|
|
280
|
+
threadId: "thread",
|
|
281
|
+
status: "completed",
|
|
282
|
+
traceId: "aabbccdd",
|
|
283
|
+
}),
|
|
284
|
+
).toEqual({ thread_id: "thread", status: "completed" });
|
|
285
|
+
expect(
|
|
286
|
+
createRunReport({
|
|
287
|
+
threadId: "thread",
|
|
288
|
+
status: "completed",
|
|
289
|
+
}),
|
|
290
|
+
).toEqual({ thread_id: "thread", status: "completed" });
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
10
294
|
describe("truncateRunTelemetryText", () => {
|
|
11
295
|
it("passes text at or under the cap through unchanged", () => {
|
|
12
296
|
expect(truncateRunTelemetryText("hello")).toBe("hello");
|
|
@@ -169,3 +453,44 @@ describe("normalizeRunTelemetryUsage", () => {
|
|
|
169
453
|
).toBeUndefined();
|
|
170
454
|
});
|
|
171
455
|
});
|
|
456
|
+
|
|
457
|
+
describe("extractRunTelemetryModelId", () => {
|
|
458
|
+
it("prefers an explicit model ID over every fallback", () => {
|
|
459
|
+
expect(
|
|
460
|
+
extractRunTelemetryModelId({
|
|
461
|
+
modelId: "explicit",
|
|
462
|
+
custom: { modelId: "custom" },
|
|
463
|
+
steps: [{ response: { modelId: "step" } }],
|
|
464
|
+
}),
|
|
465
|
+
).toBe("explicit");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("falls back to the custom bag before the steps", () => {
|
|
469
|
+
expect(
|
|
470
|
+
extractRunTelemetryModelId({
|
|
471
|
+
custom: { modelId: "custom" },
|
|
472
|
+
steps: [{ response: { modelId: "step" } }],
|
|
473
|
+
}),
|
|
474
|
+
).toBe("custom");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("reads the first step that carries a response model ID", () => {
|
|
478
|
+
expect(
|
|
479
|
+
extractRunTelemetryModelId({
|
|
480
|
+
steps: [{ usage: { inputTokens: 1 } }, { response: { modelId: "b" } }],
|
|
481
|
+
}),
|
|
482
|
+
).toBe("b");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("returns undefined when no channel carries one", () => {
|
|
486
|
+
expect(extractRunTelemetryModelId(undefined)).toBeUndefined();
|
|
487
|
+
expect(extractRunTelemetryModelId({})).toBeUndefined();
|
|
488
|
+
expect(
|
|
489
|
+
extractRunTelemetryModelId({
|
|
490
|
+
modelId: 7,
|
|
491
|
+
custom: { modelId: null },
|
|
492
|
+
steps: [null, "step", { response: null }, { response: { modelId: 7 } }],
|
|
493
|
+
}),
|
|
494
|
+
).toBeUndefined();
|
|
495
|
+
});
|
|
496
|
+
});
|