assistant-cloud 0.1.41 → 0.1.43

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 (40) hide show
  1. package/README.md +3 -3
  2. package/dist/AssistantCloud.js +2 -2
  3. package/dist/AssistantCloud.js.map +1 -1
  4. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  5. package/dist/AssistantCloudAPI.js +5 -5
  6. package/dist/AssistantCloudAPI.js.map +1 -1
  7. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  8. package/dist/AssistantCloudAuthStrategy.js +68 -17
  9. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  10. package/dist/AssistantCloudRuns.d.ts +3 -13
  11. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  12. package/dist/AssistantCloudRuns.js.map +1 -1
  13. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  14. package/dist/CloudMessagePersistence.js +19 -11
  15. package/dist/CloudMessagePersistence.js.map +1 -1
  16. package/dist/generateThreadTitle.d.ts +15 -0
  17. package/dist/generateThreadTitle.d.ts.map +1 -0
  18. package/dist/generateThreadTitle.js +25 -0
  19. package/dist/generateThreadTitle.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +3 -1
  22. package/dist/runTelemetry.d.ts +61 -0
  23. package/dist/runTelemetry.d.ts.map +1 -0
  24. package/dist/runTelemetry.js +82 -0
  25. package/dist/runTelemetry.js.map +1 -0
  26. package/package.json +5 -5
  27. package/src/AssistantCloud.ts +1 -1
  28. package/src/AssistantCloudAPI.ts +9 -8
  29. package/src/AssistantCloudAuthStrategy.ts +140 -46
  30. package/src/AssistantCloudRuns.ts +3 -14
  31. package/src/CloudMessagePersistence.ts +23 -19
  32. package/src/generateThreadTitle.test.ts +71 -0
  33. package/src/generateThreadTitle.ts +38 -0
  34. package/src/index.ts +10 -0
  35. package/src/runTelemetry.test.ts +171 -0
  36. package/src/runTelemetry.ts +144 -0
  37. package/src/tests/AssistantCloud.test.ts +39 -0
  38. package/src/tests/AssistantCloudAPI.test.ts +25 -0
  39. package/src/tests/AssistantCloudAuthStrategy.test.ts +284 -10
  40. package/src/tests/CloudMessagePersistence.test.ts +93 -0
@@ -0,0 +1,171 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createRunTelemetryToolCall,
4
+ normalizeRunTelemetryUsage,
5
+ truncateRunTelemetryText,
6
+ } from "./runTelemetry";
7
+
8
+ const MAX = 50_000;
9
+
10
+ describe("truncateRunTelemetryText", () => {
11
+ it("passes text at or under the cap through unchanged", () => {
12
+ expect(truncateRunTelemetryText("hello")).toBe("hello");
13
+ const exact = "a".repeat(MAX);
14
+ expect(truncateRunTelemetryText(exact)).toBe(exact);
15
+ });
16
+
17
+ it("clamps text over the cap", () => {
18
+ expect(truncateRunTelemetryText("a".repeat(MAX + 1))).toHaveLength(MAX);
19
+ });
20
+ });
21
+
22
+ describe("createRunTelemetryToolCall", () => {
23
+ it("serializes args and omits tool_source when the caller gives none", () => {
24
+ expect(
25
+ createRunTelemetryToolCall({
26
+ toolName: "calculator",
27
+ toolCallId: "call-1",
28
+ args: { a: 1 },
29
+ result: { sum: 1 },
30
+ }),
31
+ ).toEqual({
32
+ tool_name: "calculator",
33
+ tool_call_id: "call-1",
34
+ tool_args: '{"a":1}',
35
+ tool_result: '{"sum":1}',
36
+ });
37
+ });
38
+
39
+ it("clamps serialized args and results", () => {
40
+ const call = createRunTelemetryToolCall({
41
+ toolName: "t",
42
+ toolCallId: "call-1",
43
+ args: { blob: "a".repeat(MAX) },
44
+ result: { blob: "a".repeat(MAX) },
45
+ });
46
+ expect(call.tool_args).toHaveLength(MAX);
47
+ expect(call.tool_result).toHaveLength(MAX);
48
+ });
49
+
50
+ it("clamps pre-serialized argsText to the cap", () => {
51
+ const argsText = "a".repeat(MAX + 10);
52
+ const call = createRunTelemetryToolCall({
53
+ toolName: "t",
54
+ toolCallId: "call-1",
55
+ argsText,
56
+ args: { ignored: true },
57
+ });
58
+ expect(call.tool_args).toBe(argsText.slice(0, MAX));
59
+ });
60
+
61
+ it("omits fields whose value cannot be serialized", () => {
62
+ const circular: Record<string, unknown> = {};
63
+ circular.self = circular;
64
+ expect(
65
+ createRunTelemetryToolCall({
66
+ toolName: "t",
67
+ toolCallId: "call-1",
68
+ args: circular,
69
+ result: undefined,
70
+ }),
71
+ ).toEqual({ tool_name: "t", tool_call_id: "call-1" });
72
+ });
73
+
74
+ it("summarizes base64 image and audio blocks in an mcp result", () => {
75
+ const call = createRunTelemetryToolCall({
76
+ toolName: "t",
77
+ toolCallId: "call-1",
78
+ toolSource: "mcp",
79
+ result: [
80
+ { type: "text", text: "keep me" },
81
+ { type: "image", data: "A".repeat(4096) },
82
+ ],
83
+ });
84
+ expect(call.tool_source).toBe("mcp");
85
+ expect(call.tool_result).toContain("keep me");
86
+ expect(call.tool_result).toContain("[image: 3.0KB]");
87
+ expect(call.tool_result).not.toContain("A".repeat(200));
88
+ });
89
+
90
+ it("leaves a non-mcp result unsummarized", () => {
91
+ const result = [{ type: "image", data: "A".repeat(4096) }];
92
+ const call = createRunTelemetryToolCall({
93
+ toolName: "t",
94
+ toolCallId: "call-1",
95
+ toolSource: "frontend",
96
+ result,
97
+ });
98
+ expect(call.tool_source).toBe("frontend");
99
+ expect(call.tool_result).toBe(JSON.stringify(result));
100
+ });
101
+ });
102
+
103
+ describe("normalizeRunTelemetryUsage", () => {
104
+ it("prefers the current names over the legacy ones", () => {
105
+ expect(
106
+ normalizeRunTelemetryUsage({
107
+ inputTokens: 1,
108
+ outputTokens: 2,
109
+ promptTokens: 90,
110
+ completionTokens: 90,
111
+ }),
112
+ ).toEqual({ inputTokens: 1, outputTokens: 2 });
113
+ });
114
+
115
+ it("falls back to the legacy prompt and completion names", () => {
116
+ expect(
117
+ normalizeRunTelemetryUsage({ promptTokens: 3, completionTokens: 4 }),
118
+ ).toEqual({ inputTokens: 3, outputTokens: 4 });
119
+ });
120
+
121
+ it("keeps a zero count and omits an absent one", () => {
122
+ expect(
123
+ normalizeRunTelemetryUsage({ inputTokens: 0, cachedInputTokens: 5 }),
124
+ ).toEqual({ inputTokens: 0, cachedInputTokens: 5 });
125
+ });
126
+
127
+ it("reads the AI SDK v7 token detail objects", () => {
128
+ expect(
129
+ normalizeRunTelemetryUsage({
130
+ inputTokens: 12,
131
+ outputTokens: 7,
132
+ inputTokenDetails: { cacheReadTokens: 5 },
133
+ outputTokenDetails: { reasoningTokens: 3 },
134
+ }),
135
+ ).toEqual({
136
+ inputTokens: 12,
137
+ outputTokens: 7,
138
+ reasoningTokens: 3,
139
+ cachedInputTokens: 5,
140
+ });
141
+ });
142
+
143
+ it("prefers the top-level detail counts over the nested ones", () => {
144
+ expect(
145
+ normalizeRunTelemetryUsage({
146
+ reasoningTokens: 3,
147
+ cachedInputTokens: 5,
148
+ inputTokenDetails: { cacheReadTokens: 90 },
149
+ outputTokenDetails: { reasoningTokens: 90 },
150
+ }),
151
+ ).toEqual({ reasoningTokens: 3, cachedInputTokens: 5 });
152
+ });
153
+
154
+ it("returns a usage object when only the nested counts are present", () => {
155
+ expect(
156
+ normalizeRunTelemetryUsage({
157
+ inputTokenDetails: { cacheReadTokens: 5 },
158
+ }),
159
+ ).toEqual({ cachedInputTokens: 5 });
160
+ });
161
+
162
+ it("returns undefined when no count is present", () => {
163
+ expect(normalizeRunTelemetryUsage({})).toBeUndefined();
164
+ expect(
165
+ normalizeRunTelemetryUsage({
166
+ inputTokenDetails: {},
167
+ outputTokenDetails: {},
168
+ }),
169
+ ).toBeUndefined();
170
+ });
171
+ });
@@ -0,0 +1,144 @@
1
+ import type { SamplingCallData } from "./instrumentMcpSampling";
2
+
3
+ const MAX_TELEMETRY_TEXT_LENGTH = 50_000;
4
+
5
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;
6
+
7
+ export type AssistantCloudRunReportToolCall = {
8
+ tool_name: string;
9
+ tool_call_id: string;
10
+ tool_args?: string;
11
+ tool_result?: string;
12
+ tool_source?: "mcp" | "frontend" | "backend";
13
+ start_ms?: number;
14
+ end_ms?: number;
15
+ sampling_calls?: SamplingCallData[];
16
+ };
17
+
18
+ /**
19
+ * Clamps a string to the size the runs endpoint accepts for a single span
20
+ * field.
21
+ */
22
+ export function truncateRunTelemetryText(value: string): string {
23
+ if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;
24
+ return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);
25
+ }
26
+
27
+ function safeStringify(value: unknown): string | undefined {
28
+ if (value == null) return undefined;
29
+ try {
30
+ return truncateRunTelemetryText(JSON.stringify(value));
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ function summarizeMcpResult(value: unknown): string | undefined {
37
+ if (value == null) return undefined;
38
+ try {
39
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
40
+ if (Array.isArray(parsed)) {
41
+ const summarized = parsed.map((item) => {
42
+ if (item && typeof item === "object" && item.type) {
43
+ if (
44
+ (item.type === "image" || item.type === "audio") &&
45
+ typeof item.data === "string" &&
46
+ BASE64_PATTERN.test(item.data.slice(0, 200))
47
+ ) {
48
+ const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);
49
+ return { ...item, data: `[${item.type}: ${sizeKB}KB]` };
50
+ }
51
+ }
52
+ return item;
53
+ });
54
+ return truncateRunTelemetryText(JSON.stringify(summarized));
55
+ }
56
+ } catch {
57
+ // not JSON array, fall through
58
+ }
59
+ return safeStringify(value);
60
+ }
61
+
62
+ export type RunTelemetryToolCallInit = {
63
+ toolName: string;
64
+ toolCallId: string;
65
+ args?: unknown;
66
+ /**
67
+ * Pre-serialized arguments, used in place of serializing `args`. Values over
68
+ * the span size are clamped before they are included in the report.
69
+ */
70
+ argsText?: string | undefined;
71
+ result?: unknown;
72
+ toolSource?: "mcp" | "frontend" | "backend" | undefined;
73
+ };
74
+
75
+ /**
76
+ * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
77
+ * source has its result summarized, because MCP content blocks carry inline
78
+ * base64 image and audio payloads that would otherwise dominate the report.
79
+ */
80
+ export function createRunTelemetryToolCall(
81
+ init: RunTelemetryToolCallInit,
82
+ ): AssistantCloudRunReportToolCall {
83
+ const { toolName, toolCallId, args, argsText, result, toolSource } = init;
84
+ const call: AssistantCloudRunReportToolCall = {
85
+ tool_name: toolName,
86
+ tool_call_id: toolCallId,
87
+ };
88
+ const toolArgs =
89
+ argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);
90
+ if (toolArgs !== undefined) call.tool_args = toolArgs;
91
+ const toolResult =
92
+ toolSource === "mcp" ? summarizeMcpResult(result) : safeStringify(result);
93
+ if (toolResult !== undefined) call.tool_result = toolResult;
94
+ if (toolSource) call.tool_source = toolSource;
95
+ return call;
96
+ }
97
+
98
+ export type RunTelemetryUsage = {
99
+ inputTokens?: number;
100
+ outputTokens?: number;
101
+ reasoningTokens?: number;
102
+ cachedInputTokens?: number;
103
+ };
104
+
105
+ export type RunTelemetryUsageInit = RunTelemetryUsage & {
106
+ promptTokens?: number;
107
+ completionTokens?: number;
108
+ inputTokenDetails?: { cacheReadTokens?: number };
109
+ outputTokenDetails?: { reasoningTokens?: number };
110
+ };
111
+
112
+ /**
113
+ * Resolves the token counts a provider reports under any of the names the AI
114
+ * SDK has used: the current top-level ones, the legacy prompt/completion pair,
115
+ * and the v7 token detail objects. Returns undefined when no count is present,
116
+ * so callers can tell an empty usage object from a zeroed one.
117
+ */
118
+ export function normalizeRunTelemetryUsage(
119
+ usage: RunTelemetryUsageInit,
120
+ ): RunTelemetryUsage | undefined {
121
+ const inputTokens = usage.inputTokens ?? usage.promptTokens;
122
+ const outputTokens = usage.outputTokens ?? usage.completionTokens;
123
+ // AI SDK v7 moved these under token detail objects; v6 kept them top-level.
124
+ const reasoningTokens =
125
+ usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens;
126
+ const cachedInputTokens =
127
+ usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens;
128
+
129
+ if (
130
+ inputTokens == null &&
131
+ outputTokens == null &&
132
+ reasoningTokens == null &&
133
+ cachedInputTokens == null
134
+ ) {
135
+ return undefined;
136
+ }
137
+
138
+ return {
139
+ ...(inputTokens != null ? { inputTokens } : undefined),
140
+ ...(outputTokens != null ? { outputTokens } : undefined),
141
+ ...(reasoningTokens != null ? { reasoningTokens } : undefined),
142
+ ...(cachedInputTokens != null ? { cachedInputTokens } : undefined),
143
+ };
144
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AssistantCloud } from "../AssistantCloud";
3
+ import type { AssistantCloudTelemetryConfig } from "../AssistantCloudAPI";
4
+
5
+ const createCloud = (
6
+ telemetry?: ConstructorParameters<typeof AssistantCloud>[0]["telemetry"],
7
+ ) =>
8
+ new AssistantCloud({
9
+ apiKey: "test-key",
10
+ userId: "user-id",
11
+ workspaceId: "workspace-id",
12
+ ...(telemetry !== undefined ? { telemetry } : {}),
13
+ });
14
+
15
+ describe("AssistantCloud telemetry config", () => {
16
+ it("defaults to enabled", () => {
17
+ expect(createCloud().telemetry.enabled).toBe(true);
18
+ expect(createCloud(true).telemetry.enabled).toBe(true);
19
+ });
20
+
21
+ it("disables when configured off", () => {
22
+ expect(createCloud(false).telemetry.enabled).toBe(false);
23
+ expect(createCloud({ enabled: false }).telemetry.enabled).toBe(false);
24
+ });
25
+
26
+ it("stays enabled when the config object carries an undefined enabled", () => {
27
+ const beforeReport: NonNullable<
28
+ AssistantCloudTelemetryConfig["beforeReport"]
29
+ > = (report) => report;
30
+ // JS consumers (and TS apps without exactOptionalPropertyTypes) can pass
31
+ // an explicitly-undefined enabled, e.g. { enabled: cfg.enabled }.
32
+ const telemetry = createCloud({
33
+ enabled: undefined,
34
+ beforeReport,
35
+ } as unknown as AssistantCloudTelemetryConfig).telemetry;
36
+ expect(telemetry.enabled).toBe(true);
37
+ expect(telemetry.beforeReport).toBe(beforeReport);
38
+ });
39
+ });
@@ -169,6 +169,31 @@ describe("AssistantCloudAPI", () => {
169
169
  expect(error.status).toBe(400);
170
170
  });
171
171
 
172
+ it("falls back to the response text when the JSON error body has no message", async () => {
173
+ const fetchMock = vi.fn().mockResolvedValue({
174
+ ok: false,
175
+ status: 429,
176
+ headers: new Headers(),
177
+ text: vi
178
+ .fn()
179
+ .mockResolvedValue(JSON.stringify({ error: "rate limited" })),
180
+ });
181
+ vi.stubGlobal("fetch", fetchMock);
182
+
183
+ const api = new AssistantCloudAPI({
184
+ apiKey: "test-key",
185
+ userId: "u-1",
186
+ workspaceId: "w-1",
187
+ });
188
+
189
+ const error = await api.makeRawRequest("/threads").catch((e) => e);
190
+ expect(error).toBeInstanceOf(CloudAPIError);
191
+ expect(error.message).toBe(
192
+ 'Request failed with status 429, {"error":"rate limited"}',
193
+ );
194
+ expect(error.status).toBe(429);
195
+ });
196
+
172
197
  it("throws generic error with status for non-JSON error responses", async () => {
173
198
  const fetchMock = vi.fn().mockResolvedValue({
174
199
  ok: false,