assistant-cloud 0.1.43 → 0.2.0

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.
Files changed (81) hide show
  1. package/README.md +40 -0
  2. package/dist/AssistantCloud.d.ts +4 -0
  3. package/dist/AssistantCloud.d.ts.map +1 -1
  4. package/dist/AssistantCloud.js +11 -5
  5. package/dist/AssistantCloud.js.map +1 -1
  6. package/dist/AssistantCloudAPI.d.ts +17 -1
  7. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  8. package/dist/AssistantCloudAPI.js +20 -9
  9. package/dist/AssistantCloudAPI.js.map +1 -1
  10. package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
  11. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  12. package/dist/AssistantCloudAuthStrategy.js +11 -1
  13. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  14. package/dist/AssistantCloudEvents.d.ts +32 -0
  15. package/dist/AssistantCloudEvents.d.ts.map +1 -0
  16. package/dist/AssistantCloudEvents.js +115 -0
  17. package/dist/AssistantCloudEvents.js.map +1 -0
  18. package/dist/AssistantCloudFiles.d.ts +12 -1
  19. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  20. package/dist/AssistantCloudFiles.js +13 -1
  21. package/dist/AssistantCloudFiles.js.map +1 -1
  22. package/dist/AssistantCloudRuns.d.ts +11 -0
  23. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  24. package/dist/AssistantCloudRuns.js.map +1 -1
  25. package/dist/AssistantCloudScores.d.ts +27 -0
  26. package/dist/AssistantCloudScores.d.ts.map +1 -0
  27. package/dist/AssistantCloudScores.js +30 -0
  28. package/dist/AssistantCloudScores.js.map +1 -0
  29. package/dist/AssistantCloudThreadMessages.d.ts +11 -1
  30. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  31. package/dist/AssistantCloudThreadMessages.js +12 -1
  32. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  33. package/dist/AssistantCloudThreads.d.ts +8 -0
  34. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  35. package/dist/AssistantCloudThreads.js +9 -1
  36. package/dist/AssistantCloudThreads.js.map +1 -1
  37. package/dist/CloudMessagePersistence.d.ts +4 -0
  38. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  39. package/dist/CloudMessagePersistence.js +38 -11
  40. package/dist/CloudMessagePersistence.js.map +1 -1
  41. package/dist/cloudResponse.d.ts +4 -1
  42. package/dist/cloudResponse.d.ts.map +1 -1
  43. package/dist/cloudResponse.js +14 -1
  44. package/dist/cloudResponse.js.map +1 -1
  45. package/dist/index.d.ts +7 -3
  46. package/dist/index.js +5 -2
  47. package/dist/runTelemetry.d.ts +70 -7
  48. package/dist/runTelemetry.d.ts.map +1 -1
  49. package/dist/runTelemetry.js +144 -1
  50. package/dist/runTelemetry.js.map +1 -1
  51. package/dist/telemetry/index.d.ts +32 -0
  52. package/dist/telemetry/index.d.ts.map +1 -0
  53. package/dist/telemetry/index.js +62 -0
  54. package/dist/telemetry/index.js.map +1 -0
  55. package/package.json +28 -4
  56. package/src/AssistantCloud.ts +20 -8
  57. package/src/AssistantCloudAPI.ts +42 -10
  58. package/src/AssistantCloudAuthStrategy.ts +13 -0
  59. package/src/AssistantCloudEvents.test.ts +124 -0
  60. package/src/AssistantCloudEvents.ts +185 -0
  61. package/src/AssistantCloudFiles.test.ts +59 -0
  62. package/src/AssistantCloudFiles.ts +39 -0
  63. package/src/AssistantCloudRuns.ts +12 -1
  64. package/src/AssistantCloudScores.test.ts +113 -0
  65. package/src/AssistantCloudScores.ts +61 -0
  66. package/src/AssistantCloudThreadMessages.test.ts +51 -0
  67. package/src/AssistantCloudThreadMessages.ts +33 -0
  68. package/src/AssistantCloudThreads.test.ts +21 -0
  69. package/src/AssistantCloudThreads.ts +21 -0
  70. package/src/CloudMessagePersistence.ts +48 -16
  71. package/src/cloudResponse.test.ts +34 -1
  72. package/src/cloudResponse.ts +30 -0
  73. package/src/index.ts +24 -1
  74. package/src/runTelemetry.test.ts +325 -0
  75. package/src/runTelemetry.ts +257 -6
  76. package/src/telemetry/index.test.ts +252 -0
  77. package/src/telemetry/index.ts +98 -0
  78. package/src/tests/AssistantCloud.test.ts +22 -0
  79. package/src/tests/AssistantCloudAPI.test.ts +44 -4
  80. package/src/tests/AssistantCloudAuthStrategy.test.ts +71 -2
  81. package/src/tests/CloudMessagePersistence.test.ts +115 -0
@@ -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
+ });
@@ -1,4 +1,5 @@
1
1
  import type { SamplingCallData } from "./instrumentMcpSampling";
2
+ import type { AssistantCloudRunReport } from "./AssistantCloudRuns";
2
3
 
3
4
  const MAX_TELEMETRY_TEXT_LENGTH = 50_000;
4
5
 
@@ -15,6 +16,86 @@ export type AssistantCloudRunReportToolCall = {
15
16
  sampling_calls?: SamplingCallData[];
16
17
  };
17
18
 
19
+ export type RunReportOutcome =
20
+ | "aborted"
21
+ | "disconnected"
22
+ | "length"
23
+ | "content_filter";
24
+
25
+ /**
26
+ * Maps a finish event to the report status and outcome. `fallbackStatus`
27
+ * applies when the event carries neither a finish reason nor a failure flag.
28
+ */
29
+ export function deriveRunOutcome(
30
+ input: {
31
+ finishReason?: string | undefined;
32
+ isAbort?: boolean | undefined;
33
+ isDisconnect?: boolean | undefined;
34
+ isError?: boolean | undefined;
35
+ },
36
+ fallbackStatus: "completed" | "incomplete" = "completed",
37
+ ): {
38
+ status: "completed" | "incomplete" | "error";
39
+ outcome?: RunReportOutcome;
40
+ } {
41
+ if (input.isError) return { status: "error" };
42
+ if (input.isAbort) return { status: "incomplete", outcome: "aborted" };
43
+ if (input.isDisconnect) {
44
+ return { status: "incomplete", outcome: "disconnected" };
45
+ }
46
+ switch (input.finishReason) {
47
+ case "length":
48
+ return { status: "incomplete", outcome: "length" };
49
+ case "content-filter":
50
+ case "content_filter":
51
+ return { status: "incomplete", outcome: "content_filter" };
52
+ case "cancelled":
53
+ return { status: "incomplete", outcome: "aborted" };
54
+ case "error":
55
+ return { status: "error" };
56
+ default:
57
+ return {
58
+ status: input.finishReason === undefined ? fallbackStatus : "completed",
59
+ };
60
+ }
61
+ }
62
+
63
+ const MAX_RUN_ERROR_CODE_LENGTH = 64;
64
+ const MAX_RUN_ERROR_LENGTH = 2048;
65
+
66
+ /**
67
+ * Reads the message and code the runs endpoint stores for a failed run. The
68
+ * code is the error's `code` when it has one, else its class name.
69
+ */
70
+ export function describeRunError(error: unknown): {
71
+ error?: string;
72
+ errorCode?: string;
73
+ } {
74
+ if (error == null) return {};
75
+ const record =
76
+ typeof error === "object" ? (error as Record<string, unknown>) : undefined;
77
+ const message =
78
+ typeof error === "string"
79
+ ? error
80
+ : typeof record?.message === "string"
81
+ ? record.message
82
+ : undefined;
83
+ const code =
84
+ typeof record?.code === "string"
85
+ ? record.code
86
+ : typeof record?.name === "string" && record.name !== "Error"
87
+ ? record.name
88
+ : undefined;
89
+ return {
90
+ ...(message
91
+ ? { error: message.slice(0, MAX_RUN_ERROR_LENGTH) }
92
+ : undefined),
93
+ ...(code
94
+ ? { errorCode: code.slice(0, MAX_RUN_ERROR_CODE_LENGTH) }
95
+ : undefined),
96
+ };
97
+ }
98
+
18
99
  /**
19
100
  * Clamps a string to the size the runs endpoint accepts for a single span
20
101
  * field.
@@ -95,20 +176,190 @@ export function createRunTelemetryToolCall(
95
176
  return call;
96
177
  }
97
178
 
179
+ /**
180
+ * Resolves the model ID a run reports, in the order an app can supply it: an
181
+ * explicit `modelId`, the `custom` bag, then the per-step `response.modelId`
182
+ * that a `messageMetadata` callback copies off the AI SDK's finish-step part.
183
+ * The AI SDK puts no model ID on a UI message part, so message metadata is the
184
+ * only channel one arrives on.
185
+ */
186
+ export function extractRunTelemetryModelId(
187
+ metadata: Record<string, unknown> | undefined,
188
+ ): string | undefined {
189
+ if (!metadata) return undefined;
190
+ if (typeof metadata.modelId === "string") return metadata.modelId;
191
+ const custom = metadata.custom as Record<string, unknown> | undefined;
192
+ if (typeof custom?.modelId === "string") return custom.modelId;
193
+
194
+ const steps: unknown = metadata.steps;
195
+ if (!Array.isArray(steps)) return undefined;
196
+ for (const step of steps as unknown[]) {
197
+ if (!step || typeof step !== "object") continue;
198
+ const response = (step as Record<string, unknown>).response;
199
+ if (!response || typeof response !== "object") continue;
200
+ const modelId = (response as Record<string, unknown>).modelId;
201
+ if (typeof modelId === "string") return modelId;
202
+ }
203
+ return undefined;
204
+ }
205
+
98
206
  export type RunTelemetryUsage = {
99
- inputTokens?: number;
100
- outputTokens?: number;
101
- reasoningTokens?: number;
102
- cachedInputTokens?: number;
207
+ inputTokens?: number | undefined;
208
+ outputTokens?: number | undefined;
209
+ reasoningTokens?: number | undefined;
210
+ cachedInputTokens?: number | undefined;
103
211
  };
104
212
 
105
213
  export type RunTelemetryUsageInit = RunTelemetryUsage & {
106
- promptTokens?: number;
107
- completionTokens?: number;
214
+ promptTokens?: number | undefined;
215
+ completionTokens?: number | undefined;
108
216
  inputTokenDetails?: { cacheReadTokens?: number };
109
217
  outputTokenDetails?: { reasoningTokens?: number };
110
218
  };
111
219
 
220
+ export type RunReportStepInit = {
221
+ usage?: RunTelemetryUsageInit | undefined;
222
+ toolCalls?: AssistantCloudRunReportToolCall[] | undefined;
223
+ startMs?: number | undefined;
224
+ endMs?: number | undefined;
225
+ finishReason?: string | undefined;
226
+ };
227
+
228
+ export type RunReportInit = {
229
+ threadId: string;
230
+ status: AssistantCloudRunReport["status"];
231
+ outcome?: RunReportOutcome | undefined;
232
+ errorCode?: string | undefined;
233
+ error?: string | undefined;
234
+ messageId?: string | undefined;
235
+ traceId?: string | undefined;
236
+ modelId?: string | undefined;
237
+ provider?: string | undefined;
238
+ usage?: RunTelemetryUsageInit | undefined;
239
+ steps?: RunReportStepInit[] | undefined;
240
+ totalSteps?: number | undefined;
241
+ toolCalls?: AssistantCloudRunReportToolCall[] | undefined;
242
+ durationMs?: number | undefined;
243
+ firstTokenMs?: number | undefined;
244
+ outputText?: string | undefined;
245
+ metadata?: Record<string, unknown> | undefined;
246
+ telemetry?: {
247
+ environment?: string | undefined;
248
+ release?: string | undefined;
249
+ tags?: readonly string[] | undefined;
250
+ };
251
+ };
252
+
253
+ function assignUsage(
254
+ report: AssistantCloudRunReport,
255
+ usage: RunTelemetryUsageInit | undefined,
256
+ ): void {
257
+ if (!usage) return;
258
+ const normalized = normalizeRunTelemetryUsage(usage);
259
+ if (!normalized) return;
260
+ if (normalized.inputTokens !== undefined) {
261
+ report.input_tokens = normalized.inputTokens;
262
+ }
263
+ if (normalized.outputTokens !== undefined) {
264
+ report.output_tokens = normalized.outputTokens;
265
+ }
266
+ if (normalized.reasoningTokens !== undefined) {
267
+ report.reasoning_tokens = normalized.reasoningTokens;
268
+ }
269
+ if (normalized.cachedInputTokens !== undefined) {
270
+ report.cached_input_tokens = normalized.cachedInputTokens;
271
+ }
272
+ }
273
+
274
+ function createRunReportStep(
275
+ init: RunReportStepInit,
276
+ ): NonNullable<AssistantCloudRunReport["steps"]>[number] {
277
+ const step: NonNullable<AssistantCloudRunReport["steps"]>[number] = {};
278
+ const usage = init.usage ? normalizeRunTelemetryUsage(init.usage) : undefined;
279
+ if (usage?.inputTokens !== undefined) step.input_tokens = usage.inputTokens;
280
+ if (usage?.outputTokens !== undefined)
281
+ step.output_tokens = usage.outputTokens;
282
+ if (usage?.reasoningTokens !== undefined) {
283
+ step.reasoning_tokens = usage.reasoningTokens;
284
+ }
285
+ if (usage?.cachedInputTokens !== undefined) {
286
+ step.cached_input_tokens = usage.cachedInputTokens;
287
+ }
288
+ if (init.toolCalls !== undefined) step.tool_calls = init.toolCalls;
289
+ if (init.startMs !== undefined) step.start_ms = init.startMs;
290
+ if (init.endMs !== undefined) step.end_ms = init.endMs;
291
+ if (init.finishReason !== undefined) {
292
+ step.finish_reason = init.finishReason.slice(0, 32);
293
+ }
294
+ return step;
295
+ }
296
+
297
+ function normalizeRunReportTags(
298
+ tags: readonly string[] | undefined,
299
+ ): string[] | undefined {
300
+ if (!tags) return undefined;
301
+ const result: string[] = [];
302
+ const seen = new Set<string>();
303
+ for (const tag of tags) {
304
+ const normalized = tag.trim().slice(0, 64).trim();
305
+ if (!normalized || seen.has(normalized)) continue;
306
+ seen.add(normalized);
307
+ result.push(normalized);
308
+ if (result.length === 20) break;
309
+ }
310
+ return result.length > 0 ? result : undefined;
311
+ }
312
+
313
+ function normalizeRunReportMilliseconds(
314
+ value: number | undefined,
315
+ ): number | undefined {
316
+ if (value === undefined || !Number.isFinite(value)) return undefined;
317
+ return Math.max(0, Math.round(value));
318
+ }
319
+
320
+ export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
321
+ const report: AssistantCloudRunReport = {
322
+ thread_id: init.threadId,
323
+ status: init.status,
324
+ };
325
+ const traceId = init.traceId?.toLowerCase();
326
+ if (traceId && /^[0-9a-f]{32}$/.test(traceId)) report.trace_id = traceId;
327
+ if (init.outcome !== undefined) report.outcome_type = init.outcome;
328
+ if (init.errorCode !== undefined) report.error_code = init.errorCode;
329
+ if (init.error !== undefined) report.error = init.error;
330
+ if (init.messageId !== undefined) report.message_id = init.messageId;
331
+ if (init.modelId !== undefined) report.model_id = init.modelId;
332
+ if (init.provider !== undefined) {
333
+ report.provider = init.provider;
334
+ report.provider_type = init.provider;
335
+ }
336
+ assignUsage(report, init.usage);
337
+ if (init.steps !== undefined) {
338
+ report.steps = init.steps.map(createRunReportStep);
339
+ report.total_steps = init.steps.length;
340
+ } else if (init.totalSteps !== undefined) {
341
+ report.total_steps = init.totalSteps;
342
+ }
343
+ if (init.toolCalls !== undefined) report.tool_calls = init.toolCalls;
344
+ const durationMs = normalizeRunReportMilliseconds(init.durationMs);
345
+ if (durationMs !== undefined) report.duration_ms = durationMs;
346
+ const firstTokenMs = normalizeRunReportMilliseconds(init.firstTokenMs);
347
+ if (firstTokenMs !== undefined) report.first_token_ms = firstTokenMs;
348
+ if (init.outputText !== undefined) {
349
+ report.output_text = truncateRunTelemetryText(init.outputText);
350
+ }
351
+ if (init.metadata !== undefined) report.metadata = init.metadata;
352
+ if (init.telemetry?.environment !== undefined) {
353
+ report.environment = init.telemetry.environment;
354
+ }
355
+ if (init.telemetry?.release !== undefined) {
356
+ report.release = init.telemetry.release;
357
+ }
358
+ const tags = normalizeRunReportTags(init.telemetry?.tags);
359
+ if (tags !== undefined) report.tags = tags;
360
+ return report;
361
+ }
362
+
112
363
  /**
113
364
  * Resolves the token counts a provider reports under any of the names the AI
114
365
  * SDK has used: the current top-level ones, the legacy prompt/completion pair,