assistant-cloud 0.2.1 → 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 +24 -8
- package/dist/AssistantCloud.d.ts +16 -19
- 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 +74 -72
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +29 -32
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthTokens.d.ts +7 -9
- package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +35 -29
- package/dist/AssistantCloudEvents.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.js +66 -17
- package/dist/AssistantCloudEvents.js.map +1 -1
- package/dist/AssistantCloudFiles.d.ts +27 -29
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreadMessages.d.ts +11 -13
- package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreads.d.ts +12 -14
- package/dist/AssistantCloudProjectThreads.d.ts.map +1 -1
- package/dist/AssistantCloudProjects.d.ts +4 -7
- package/dist/AssistantCloudProjects.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +67 -56
- 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 +20 -23
- package/dist/AssistantCloudScores.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.d.ts +41 -38
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +16 -4
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +42 -44
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +56 -58
- 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 +56 -58
- 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 +10 -13
- package/dist/CloudRunReporter.d.ts.map +1 -1
- package/dist/CloudRunReporter.js +13 -4
- package/dist/CloudRunReporter.js.map +1 -1
- package/dist/FormattedCloudPersistence.d.ts +36 -39
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/ai-sdk/index.d.ts +14 -17
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/cloudResponse.d.ts +14 -17
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/generateThreadTitle.d.ts +9 -12
- 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 +36 -39
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/runTelemetry.d.ts +101 -94
- 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 +20 -23
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -9
- package/src/AssistantCloud.ts +17 -2
- package/src/AssistantCloudAPI.ts +4 -0
- package/src/AssistantCloudEvents.test.ts +157 -0
- package/src/AssistantCloudEvents.ts +82 -20
- package/src/AssistantCloudRuns.ts +35 -5
- package/src/AssistantCloudThreadMessages.test.ts +53 -2
- package/src/AssistantCloudThreadMessages.ts +34 -2
- package/src/BackendTranscriptCopy.test.ts +79 -0
- package/src/CloudEngagementReporter.ts +9 -3
- package/src/CloudMessagePersistence.ts +4 -0
- package/src/CloudRunReporter.ts +16 -4
- 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 +49 -2
- package/dist/tests/setup.d.ts +0 -1
- package/dist/tests/setup.js +0 -7
- package/dist/tests/setup.js.map +0 -1
|
@@ -43,14 +43,65 @@ describe("AssistantCloudThreadMessages responses", () => {
|
|
|
43
43
|
|
|
44
44
|
await expect(
|
|
45
45
|
messages.feedback("thread/1", "message/1", body),
|
|
46
|
-
).resolves.
|
|
46
|
+
).resolves.toStrictEqual({
|
|
47
47
|
feedback_id: "feedback-1",
|
|
48
48
|
type: "positive",
|
|
49
49
|
});
|
|
50
50
|
expect(makeRequest).toHaveBeenCalledWith(
|
|
51
51
|
"/threads/thread%2F1/messages/message%2F1/feedback",
|
|
52
|
-
{ method: "POST", body },
|
|
52
|
+
{ method: "POST", body: { type: "positive" } },
|
|
53
53
|
);
|
|
54
|
+
expect(makeRequest.mock.calls[0]![1].body).toStrictEqual({
|
|
55
|
+
type: "positive",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const bodyWithComment = {
|
|
59
|
+
type: "negative" as const,
|
|
60
|
+
comment: "The response missed a detail",
|
|
61
|
+
};
|
|
62
|
+
makeRequest.mockResolvedValueOnce({
|
|
63
|
+
feedback_id: "feedback-2",
|
|
64
|
+
type: "negative",
|
|
65
|
+
comment: "The response missed a detail",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
messages.feedback("thread-1", "message-1", bodyWithComment),
|
|
70
|
+
).resolves.toEqual({
|
|
71
|
+
feedback_id: "feedback-2",
|
|
72
|
+
type: "negative",
|
|
73
|
+
comment: "The response missed a detail",
|
|
74
|
+
});
|
|
75
|
+
expect(makeRequest).toHaveBeenLastCalledWith(
|
|
76
|
+
"/threads/thread-1/messages/message-1/feedback",
|
|
77
|
+
{ method: "POST", body: bodyWithComment },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
makeRequest.mockResolvedValueOnce({
|
|
81
|
+
feedback_id: "feedback-1",
|
|
82
|
+
type: "positive",
|
|
83
|
+
});
|
|
84
|
+
await messages.feedback("thread-1", "message-1", {
|
|
85
|
+
type: "positive",
|
|
86
|
+
comment: " ",
|
|
87
|
+
});
|
|
88
|
+
expect(makeRequest.mock.lastCall![1].body).toStrictEqual({
|
|
89
|
+
type: "positive",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
makeRequest.mockResolvedValueOnce({
|
|
93
|
+
feedback_id: "feedback-3",
|
|
94
|
+
type: "positive",
|
|
95
|
+
comment: null,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
messages.feedback("thread-1", "message-1", body),
|
|
100
|
+
).resolves.toStrictEqual({
|
|
101
|
+
feedback_id: "feedback-3",
|
|
102
|
+
type: "positive",
|
|
103
|
+
comment: null,
|
|
104
|
+
});
|
|
54
105
|
|
|
55
106
|
makeRequest.mockResolvedValueOnce({ type: "positive" });
|
|
56
107
|
|
|
@@ -19,6 +19,7 @@ export type CloudMessage = {
|
|
|
19
19
|
updated_at: Date;
|
|
20
20
|
format: "aui/v0" | string;
|
|
21
21
|
content: ReadonlyJSONObject;
|
|
22
|
+
external_id?: string | null | undefined;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
type AssistantCloudThreadMessageListQuery = {
|
|
@@ -35,6 +36,8 @@ type AssistantCloudThreadMessageCreateBody = {
|
|
|
35
36
|
parent_id: string | null;
|
|
36
37
|
format: "aui/v0" | string;
|
|
37
38
|
content: ReadonlyJSONObject;
|
|
39
|
+
external_id?: string | undefined;
|
|
40
|
+
parent_external_id?: string | undefined;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
type AssistantCloudMessageCreateResponse = {
|
|
@@ -49,11 +52,13 @@ const MESSAGE_FEEDBACK_TYPES = ["positive", "negative"] as const;
|
|
|
49
52
|
|
|
50
53
|
export type AssistantCloudThreadMessageFeedbackBody = {
|
|
51
54
|
type: "positive" | "negative";
|
|
55
|
+
comment?: string;
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
export type AssistantCloudThreadMessageFeedbackResponse = {
|
|
55
59
|
feedback_id: string;
|
|
56
60
|
type: "positive" | "negative";
|
|
61
|
+
comment?: string | null;
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
export const decodeCloudMessage = (
|
|
@@ -69,6 +74,10 @@ export const decodeCloudMessage = (
|
|
|
69
74
|
updated_at: readCloudTimestamp(message.updated_at, `${field}.updated_at`),
|
|
70
75
|
format: readCloudString(message.format, `${field}.format`),
|
|
71
76
|
content: readCloudJSONObject(message.content, `${field}.content`),
|
|
77
|
+
external_id: readCloudNullableString(
|
|
78
|
+
message.external_id ?? null,
|
|
79
|
+
`${field}.external_id`,
|
|
80
|
+
),
|
|
72
81
|
};
|
|
73
82
|
};
|
|
74
83
|
|
|
@@ -106,7 +115,20 @@ export class AssistantCloudThreadMessages {
|
|
|
106
115
|
const response = readCloudRecord(
|
|
107
116
|
await this.cloud.makeRequest(
|
|
108
117
|
`/threads/${encodeURIComponent(threadId)}/messages`,
|
|
109
|
-
{
|
|
118
|
+
{
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: {
|
|
121
|
+
parent_id: body.parent_id,
|
|
122
|
+
format: body.format,
|
|
123
|
+
content: body.content,
|
|
124
|
+
...(body.external_id !== undefined
|
|
125
|
+
? { external_id: body.external_id }
|
|
126
|
+
: undefined),
|
|
127
|
+
...(body.parent_external_id !== undefined
|
|
128
|
+
? { parent_external_id: body.parent_external_id }
|
|
129
|
+
: undefined),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
110
132
|
),
|
|
111
133
|
"thread message create response",
|
|
112
134
|
);
|
|
@@ -132,10 +154,17 @@ export class AssistantCloudThreadMessages {
|
|
|
132
154
|
messageId: string,
|
|
133
155
|
body: AssistantCloudThreadMessageFeedbackBody,
|
|
134
156
|
): Promise<AssistantCloudThreadMessageFeedbackResponse> {
|
|
157
|
+
const comment = body.comment?.trim();
|
|
135
158
|
const response = readCloudRecord(
|
|
136
159
|
await this.cloud.makeRequest(
|
|
137
160
|
`/threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}/feedback`,
|
|
138
|
-
{
|
|
161
|
+
{
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: {
|
|
164
|
+
type: body.type,
|
|
165
|
+
...(comment ? { comment } : undefined),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
139
168
|
),
|
|
140
169
|
"thread message feedback response",
|
|
141
170
|
);
|
|
@@ -143,6 +172,9 @@ export class AssistantCloudThreadMessages {
|
|
|
143
172
|
return {
|
|
144
173
|
feedback_id: readCloudString(response.feedback_id, "feedback_id"),
|
|
145
174
|
type: readCloudEnum(response.type, "type", MESSAGE_FEEDBACK_TYPES),
|
|
175
|
+
...("comment" in response
|
|
176
|
+
? { comment: readCloudNullableString(response.comment, "comment") }
|
|
177
|
+
: undefined),
|
|
146
178
|
};
|
|
147
179
|
}
|
|
148
180
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloud } from "./AssistantCloud";
|
|
3
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
4
|
+
import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
|
|
5
|
+
import { CloudMessagePersistence } from "./CloudMessagePersistence";
|
|
6
|
+
|
|
7
|
+
describe("backend transcript cloud client", () => {
|
|
8
|
+
it("sends external ids only when supplied", async () => {
|
|
9
|
+
const makeRequest = vi.fn().mockResolvedValue({ message_id: "cloud-1" });
|
|
10
|
+
const messages = new AssistantCloudThreadMessages({
|
|
11
|
+
makeRequest,
|
|
12
|
+
} as unknown as AssistantCloudAPI);
|
|
13
|
+
const body = { parent_id: null, format: "aui/v0", content: {} };
|
|
14
|
+
|
|
15
|
+
await messages.create("thread-1", body);
|
|
16
|
+
expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await messages.create("thread-1", {
|
|
22
|
+
...body,
|
|
23
|
+
external_id: "backend-1",
|
|
24
|
+
parent_external_id: "backend-0",
|
|
25
|
+
});
|
|
26
|
+
expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: {
|
|
29
|
+
...body,
|
|
30
|
+
external_id: "backend-1",
|
|
31
|
+
parent_external_id: "backend-0",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("decodes external ids and treats an absent field as null", async () => {
|
|
37
|
+
const makeRequest = vi.fn().mockResolvedValue({
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
id: "cloud-1",
|
|
41
|
+
parent_id: null,
|
|
42
|
+
height: 0,
|
|
43
|
+
created_at: "2026-01-01T00:00:00.000Z",
|
|
44
|
+
updated_at: "2026-01-01T00:00:00.000Z",
|
|
45
|
+
format: "aui/v0",
|
|
46
|
+
content: {},
|
|
47
|
+
external_id: "backend-1",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "cloud-2",
|
|
51
|
+
parent_id: "cloud-1",
|
|
52
|
+
height: 1,
|
|
53
|
+
created_at: "2026-01-01T00:00:00.000Z",
|
|
54
|
+
updated_at: "2026-01-01T00:00:00.000Z",
|
|
55
|
+
format: "aui/v0",
|
|
56
|
+
content: {},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
const messages = new AssistantCloudThreadMessages({
|
|
61
|
+
makeRequest,
|
|
62
|
+
} as unknown as AssistantCloudAPI);
|
|
63
|
+
|
|
64
|
+
const result = await messages.list("thread-1");
|
|
65
|
+
expect(result.messages.map((message) => message.external_id)).toEqual([
|
|
66
|
+
"backend-1",
|
|
67
|
+
null,
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("records a resolved local to cloud message mapping", async () => {
|
|
72
|
+
const persistence = new CloudMessagePersistence({} as AssistantCloud);
|
|
73
|
+
persistence.record("backend-1", "cloud-1");
|
|
74
|
+
|
|
75
|
+
expect(persistence.isPersisted("backend-1")).toBe(true);
|
|
76
|
+
expect(await persistence.getRemoteId("backend-1")).toBe("cloud-1");
|
|
77
|
+
expect(persistence.getResolvedRemoteId("backend-1")).toBe("cloud-1");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -13,13 +13,14 @@ export type EngagementEventIds = Pick<
|
|
|
13
13
|
* Turns the ids an integration knows (its own thread and message ids) into the
|
|
14
14
|
* ids the cloud stores. A send is the one event that may create the remote
|
|
15
15
|
* thread, so it asks for `awaitThread`; every other event reads the ids that
|
|
16
|
-
* already exist.
|
|
16
|
+
* already exist. Returning nothing declines the event: the thread is not one
|
|
17
|
+
* the integration knows, so there is nothing to attribute it to.
|
|
17
18
|
*/
|
|
18
19
|
export type EngagementIdResolver = (
|
|
19
20
|
threadId: string,
|
|
20
21
|
messageId: string | undefined,
|
|
21
22
|
options: { awaitThread: boolean },
|
|
22
|
-
) => EngagementEventIds | Promise<EngagementEventIds>;
|
|
23
|
+
) => EngagementEventIds | undefined | Promise<EngagementEventIds | undefined>;
|
|
23
24
|
|
|
24
25
|
type EngagementEventInit = Pick<AssistantCloudEvent, "value" | "props">;
|
|
25
26
|
|
|
@@ -86,9 +87,13 @@ export class CloudEngagementReporter {
|
|
|
86
87
|
public runStopped(threadId: string): void {
|
|
87
88
|
const startedAt = this.runStartedAt.get(threadId);
|
|
88
89
|
if (startedAt === undefined) return;
|
|
90
|
+
const stoppedAt = Date.now();
|
|
89
91
|
this.runStartedAt.delete(threadId);
|
|
92
|
+
// A stopped run is a run that ended, so the next send measures its idle
|
|
93
|
+
// time from here rather than from the last run allowed to finish.
|
|
94
|
+
remember(this.runEndedAt, threadId, stoppedAt);
|
|
90
95
|
this.track("run_stopped", threadId, undefined, {
|
|
91
|
-
value: Math.max(0,
|
|
96
|
+
value: Math.max(0, stoppedAt - startedAt),
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
|
|
@@ -223,6 +228,7 @@ export class CloudEngagementReporter {
|
|
|
223
228
|
void Promise.resolve()
|
|
224
229
|
.then(() => this.resolveIds(threadId, messageId, options))
|
|
225
230
|
.then((ids) => {
|
|
231
|
+
if (!ids) return;
|
|
226
232
|
if (kind === "thread_switched" && !ids.thread_id) return;
|
|
227
233
|
this.getCloud().events.track({ kind, ...init, ...ids });
|
|
228
234
|
})
|
|
@@ -111,6 +111,10 @@ export class CloudMessagePersistence {
|
|
|
111
111
|
return typeof entry === "string" ? entry : undefined;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
record(localId: string, remoteId: string): void {
|
|
115
|
+
this.idMapping.set(localId, remoteId);
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
/**
|
|
115
119
|
* Load messages from the cloud and populate the ID mapping.
|
|
116
120
|
*
|
package/src/CloudRunReporter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantCloud } from "./AssistantCloud";
|
|
2
|
+
import { CloudAPIError } from "./AssistantCloudAPI";
|
|
2
3
|
import { createRunReport, type RunReportInit } from "./runTelemetry";
|
|
3
4
|
|
|
4
5
|
export type CloudRunReportInit = Omit<RunReportInit, "telemetry">;
|
|
@@ -7,8 +8,8 @@ export type CloudRunReportInit = Omit<RunReportInit, "telemetry">;
|
|
|
7
8
|
* Sends run reports the way every client integration has to: nothing while
|
|
8
9
|
* telemetry is off, the cloud's environment, release and tags on every report,
|
|
9
10
|
* the `beforeReport` hook applied last, and a failed send that never surfaces.
|
|
10
|
-
* A report
|
|
11
|
-
* the
|
|
11
|
+
* A keyed report is deduplicated while in flight and after an attempt. A
|
|
12
|
+
* rate-limited attempt releases the key so a later observation can try again.
|
|
12
13
|
*/
|
|
13
14
|
export class CloudRunReporter {
|
|
14
15
|
private readonly reported = new Set<string>();
|
|
@@ -19,6 +20,7 @@ export class CloudRunReporter {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
public async report(init: CloudRunReportInit, key?: string): Promise<void> {
|
|
23
|
+
let claimedKey: string | undefined;
|
|
22
24
|
try {
|
|
23
25
|
const cloud = this.getCloud();
|
|
24
26
|
if (!cloud.telemetry.enabled) return;
|
|
@@ -29,10 +31,20 @@ export class CloudRunReporter {
|
|
|
29
31
|
const report = beforeReport ? beforeReport(initial) : initial;
|
|
30
32
|
if (!report) return;
|
|
31
33
|
|
|
32
|
-
if (key !== undefined)
|
|
34
|
+
if (key !== undefined) {
|
|
35
|
+
this.reported.add(key);
|
|
36
|
+
claimedKey = key;
|
|
37
|
+
}
|
|
33
38
|
await cloud.runs.report(report);
|
|
34
|
-
} catch {
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (claimedKey !== undefined && isRetryableReportError(error)) {
|
|
41
|
+
this.reported.delete(claimedKey);
|
|
42
|
+
}
|
|
35
43
|
return;
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
|
|
48
|
+
const isRetryableReportError = (error: unknown): boolean => {
|
|
49
|
+
return error instanceof CloudAPIError && error.status === 429;
|
|
50
|
+
};
|
package/src/ai-sdk/index.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("extractAISDKRunTelemetry", () => {
|
|
|
79
79
|
{},
|
|
80
80
|
],
|
|
81
81
|
samplingCalls: { call_2: [{ duration_ms: 20 }] },
|
|
82
|
-
modelId: "gpt-
|
|
82
|
+
modelId: "gpt-6-luna",
|
|
83
83
|
},
|
|
84
84
|
);
|
|
85
85
|
|
|
@@ -119,7 +119,7 @@ describe("extractAISDKRunTelemetry", () => {
|
|
|
119
119
|
totalSteps: 3,
|
|
120
120
|
outputText: "Cold.",
|
|
121
121
|
usage: { inputTokens: 10, outputTokens: 5 },
|
|
122
|
-
modelId: "gpt-
|
|
122
|
+
modelId: "gpt-6-luna",
|
|
123
123
|
metadata: message.metadata,
|
|
124
124
|
});
|
|
125
125
|
});
|
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({
|