assistant-cloud
Version:
Cloud integration for assistant-ui
792 lines (736 loc) • 23.1 kB
text/typescript
import { describe, expect, it, vi } from "vitest";
import type { AssistantCloud } from "./AssistantCloud";
import { CloudRunReporter } from "./CloudRunReporter";
import {
createRunReport,
createRunTelemetryToolCall,
deriveRunOutcome,
describeRunError,
extractRunTelemetryModelId,
normalizeRunTelemetryUsage,
truncateRunTelemetryText,
} from "./runTelemetry";
const MAX = 50_000;
describe("deriveRunOutcome", () => {
it.each([
[{ isError: true }, { status: "error" }],
[{ isAbort: true }, { status: "incomplete", outcome: "aborted" }],
[{ isDisconnect: true }, { status: "incomplete", outcome: "disconnected" }],
[{ finishReason: "length" }, { status: "incomplete", outcome: "length" }],
[
{ finishReason: "content-filter" },
{ status: "incomplete", outcome: "content_filter" },
],
[
{ finishReason: "content_filter" },
{ status: "incomplete", outcome: "content_filter" },
],
[
{ finishReason: "cancelled" },
{ status: "incomplete", outcome: "aborted" },
],
[{ finishReason: "stop" }, { status: "completed" }],
[{ finishReason: "tool-calls" }, { status: "completed" }],
[{}, { status: "completed" }],
[{ finishReason: "other" }, { status: "completed" }],
[{ finishReason: "error" }, { status: "error" }],
])("maps %o", (input, expected) => {
expect(deriveRunOutcome(input)).toEqual(expected);
});
it("uses the fallback status only when the event carries no finish reason", () => {
expect(deriveRunOutcome({}, "incomplete")).toEqual({
status: "incomplete",
});
expect(deriveRunOutcome({ finishReason: "stop" }, "incomplete")).toEqual({
status: "completed",
});
expect(deriveRunOutcome({ finishReason: "length" }, "incomplete")).toEqual({
status: "incomplete",
outcome: "length",
});
expect(deriveRunOutcome({ isError: true }, "incomplete")).toEqual({
status: "error",
});
});
});
describe("describeRunError", () => {
it("reads the message and the class name of an AI SDK error", () => {
const error = new Error("Rate limited");
error.name = "AI_APICallError";
expect(describeRunError(error)).toEqual({
error: "Rate limited",
errorCode: "AI_APICallError",
});
});
it("prefers an explicit code and skips the plain Error name", () => {
expect(
describeRunError(Object.assign(new Error("boom"), { code: "ETIMEDOUT" })),
).toEqual({ error: "boom", errorCode: "ETIMEDOUT" });
expect(describeRunError(new Error("boom"))).toEqual({ error: "boom" });
expect(describeRunError("boom")).toEqual({ error: "boom" });
expect(describeRunError(undefined)).toEqual({});
expect(describeRunError(42)).toEqual({});
});
it.each([
[429, "rate_limited"],
[400, "validation_failed"],
[422, "validation_failed"],
[500, "server_error"],
[503, "server_error"],
] as const)(
"reads HTTP status %i as code and outcome %s",
(statusCode, outcome) => {
const error = Object.assign(new Error("Too many requests"), {
name: "AI_APICallError",
statusCode,
});
expect(describeRunError(error)).toEqual({
error: "Too many requests",
errorCode: String(statusCode),
outcome,
});
},
);
it("keeps an HTTP status it cannot classify as the code alone", () => {
expect(
describeRunError({
code: "AI_APICallError",
message: "no",
statusCode: 401,
}),
).toEqual({ error: "no", errorCode: "401" });
});
it("clamps the message and code to what the runs endpoint accepts", () => {
const error = new Error("m".repeat(3000));
error.name = "c".repeat(100);
expect(describeRunError(error)).toEqual({
error: "m".repeat(2048),
errorCode: "c".repeat(64),
});
});
});
describe("createRunReport", () => {
it("serializes the user message ID only when supplied", async () => {
expect(
createRunReport({
threadId: "thread-1",
status: "error",
userMessageId: "remote-user-1",
}),
).toEqual({
thread_id: "thread-1",
status: "error",
user_message_id: "remote-user-1",
});
for (const options of [{}, { userMessageId: undefined }]) {
expect(
createRunReport({ threadId: "thread-1", status: "error", ...options }),
).not.toHaveProperty("user_message_id");
}
const report = vi.fn().mockResolvedValue({ run_id: "run-1" });
const reporter = new CloudRunReporter({
telemetry: { enabled: true },
runs: { report },
} as unknown as AssistantCloud);
await reporter.report({
threadId: "thread-1",
status: "error",
userMessageId: "remote-user-1",
});
await reporter.report({ threadId: "thread-1", status: "error" });
expect(report.mock.calls.map(([body]) => body)).toEqual([
{
thread_id: "thread-1",
status: "error",
user_message_id: "remote-user-1",
},
{ thread_id: "thread-1", status: "error" },
]);
});
const toolCall = {
tool_name: "weather",
tool_call_id: "call-1",
tool_args: '{"city":"Singapore"}',
};
it("creates the full aui/v0 report", () => {
expect(
createRunReport({
threadId: "aui-thread",
status: "incomplete",
outcome: "length",
errorCode: "model_limit",
error: "response cut off",
messageId: "cloud-message",
traceId: "AABBCCDDEEFF00112233445566778899",
modelId: "provider/model",
provider: "openai",
usage: {
promptTokens: 11,
completionTokens: 7,
reasoningTokens: 3,
cachedInputTokens: 2,
},
steps: [
{
usage: { inputTokens: 5, outputTokens: 4 },
toolCalls: [toolCall],
startMs: 10,
endMs: 20,
finishReason: "tool-calls",
},
{
usage: { inputTokens: 6, outputTokens: 3, reasoningTokens: 3 },
startMs: 21,
endMs: 42,
finishReason: "length".repeat(8),
},
],
toolCalls: [toolCall],
durationMs: 42.5,
firstTokenMs: -4,
outputText: "hello from aui/v0",
metadata: { tenant: "acme" },
telemetry: {
environment: "production",
release: "web-2026.09.09",
tags: [" region:sg ", "region:sg", "tier:paid"],
},
}),
).toEqual({
thread_id: "aui-thread",
status: "incomplete",
outcome_type: "length",
error_code: "model_limit",
error: "response cut off",
message_id: "cloud-message",
trace_id: "aabbccddeeff00112233445566778899",
model_id: "provider/model",
provider: "openai",
provider_type: "openai",
input_tokens: 11,
output_tokens: 7,
reasoning_tokens: 3,
cached_input_tokens: 2,
steps: [
{
input_tokens: 5,
output_tokens: 4,
tool_calls: [toolCall],
start_ms: 10,
end_ms: 20,
finish_reason: "tool-calls",
},
{
input_tokens: 6,
output_tokens: 3,
reasoning_tokens: 3,
start_ms: 21,
end_ms: 42,
finish_reason: "lengthlengthlengthlengthlengthle",
},
],
total_steps: 2,
tool_calls: [toolCall],
duration_ms: 43,
first_token_ms: 0,
output_text: "hello from aui/v0",
metadata: { tenant: "acme" },
environment: "production",
release: "web-2026.09.09",
tags: ["region:sg", "tier:paid"],
});
});
it("creates the full ai-sdk/v6 report", () => {
expect(
createRunReport({
threadId: "ai-sdk-thread",
status: "completed",
traceId: "00112233445566778899aabbccddeeff",
modelId: "gpt-6-sol",
provider: "gateway",
usage: {
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 10 },
outputTokenDetails: { reasoningTokens: 20 },
},
steps: [
{
usage: {
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 10 },
outputTokenDetails: { reasoningTokens: 20 },
},
finishReason: "stop",
},
],
durationMs: 125.2,
firstTokenMs: 12.6,
outputText: "hello from ai-sdk/v6",
telemetry: { tags: ["sdk:v6"] },
}),
).toEqual({
thread_id: "ai-sdk-thread",
status: "completed",
trace_id: "00112233445566778899aabbccddeeff",
model_id: "gpt-6-sol",
provider: "gateway",
provider_type: "gateway",
input_tokens: 100,
output_tokens: 50,
reasoning_tokens: 20,
cached_input_tokens: 10,
steps: [
{
input_tokens: 100,
output_tokens: 50,
reasoning_tokens: 20,
cached_input_tokens: 10,
finish_reason: "stop",
},
],
total_steps: 1,
duration_ms: 125,
first_token_ms: 13,
output_text: "hello from ai-sdk/v6",
tags: ["sdk:v6"],
});
});
it("normalizes tags and limits them to twenty 64-character values", () => {
const longTag = "a".repeat(70);
const tags = [" one ", "", "one", longTag];
for (let index = 0; index < 24; index++) tags.push(`tag:${index}`);
const report = createRunReport({
threadId: "thread",
status: "completed",
telemetry: { tags },
});
expect(report.tags).toEqual([
"one",
"a".repeat(64),
...Array.from({ length: 18 }, (_, index) => `tag:${index}`),
]);
});
it("trims the whitespace a 64-character cut leaves behind", () => {
const report = createRunReport({
threadId: "thread",
status: "completed",
telemetry: { tags: [`${"b".repeat(63)} tail`] },
});
expect(report.tags).toEqual(["b".repeat(63)]);
});
it("lowercases valid trace IDs, omits invalid IDs, and omits undefined keys", () => {
expect(
createRunReport({
threadId: "thread",
status: "completed",
traceId: "AABBCCDDEEFF00112233445566778899",
}).trace_id,
).toBe("aabbccddeeff00112233445566778899");
expect(
createRunReport({
threadId: "thread",
status: "completed",
traceId: "aabbccdd",
}),
).toEqual({ thread_id: "thread", status: "completed" });
expect(
createRunReport({
threadId: "thread",
status: "completed",
}),
).toEqual({ thread_id: "thread", status: "completed" });
});
it("normalizes caller-supplied run cost, attributes, and root span", () => {
expect(
createRunReport({
threadId: "thread",
status: "completed",
rootSpanId: "AABBCCDDEEFF0011",
costUsd: 0.012,
costDetails: {
input: 0.002,
inputCachedTokens: 0.001,
output: 0.009,
total: 0.012,
},
attributes: { tenant: "acme" },
}),
).toEqual({
thread_id: "thread",
status: "completed",
root_span_id: "aabbccddeeff0011",
cost_usd: 0.012,
cost_details: {
input: 0.002,
input_cached_tokens: 0.001,
output: 0.009,
total: 0.012,
},
attributes: { tenant: "acme" },
});
});
it("keeps valid cost members beside invalid ones", () => {
expect(
createRunReport({
threadId: "thread",
status: "completed",
costUsd: 0,
costDetails: {
input: 0.002,
inputCachedTokens: Number.NaN,
output: -1,
total: 0,
},
}),
).toEqual({
thread_id: "thread",
status: "completed",
cost_usd: 0,
cost_details: { input: 0.002, total: 0 },
});
});
it("omits invalid run cost and root span values", () => {
expect(
createRunReport({
threadId: "thread",
status: "completed",
rootSpanId: "not-a-span-id",
costUsd: Number.POSITIVE_INFINITY,
costDetails: {
input: -1,
inputCachedTokens: Number.NaN,
output: Number.NEGATIVE_INFINITY,
},
}),
).toEqual({ thread_id: "thread", status: "completed" });
});
it("keeps a server outcome type and truncates the step input", () => {
expect(
createRunReport({
threadId: "thread",
status: "error",
outcome: "timeout",
steps: [{ input: "a".repeat(MAX + 1) }],
}),
).toEqual({
thread_id: "thread",
status: "error",
outcome_type: "timeout",
steps: [{ input: "a".repeat(MAX) }],
total_steps: 1,
});
});
});
describe("truncateRunTelemetryText", () => {
it("passes text at or under the cap through unchanged", () => {
expect(truncateRunTelemetryText("hello")).toBe("hello");
const exact = "a".repeat(MAX);
expect(truncateRunTelemetryText(exact)).toBe(exact);
});
it("clamps text over the cap", () => {
expect(truncateRunTelemetryText("a".repeat(MAX + 1))).toHaveLength(MAX);
});
});
describe("createRunTelemetryToolCall", () => {
it("serializes args and omits tool_source when the caller gives none", () => {
expect(
createRunTelemetryToolCall({
toolName: "calculator",
toolCallId: "call-1",
args: { a: 1 },
result: { sum: 1 },
}),
).toEqual({
tool_name: "calculator",
tool_call_id: "call-1",
tool_args: '{"a":1}',
tool_result: '{"sum":1}',
});
});
it("clamps serialized args and results", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
args: { blob: "a".repeat(MAX) },
result: { blob: "a".repeat(MAX) },
});
expect(call.tool_args).toHaveLength(MAX);
expect(call.tool_result).toHaveLength(MAX);
});
it("clamps pre-serialized argsText to the cap", () => {
const argsText = "a".repeat(MAX + 10);
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
argsText,
args: { ignored: true },
});
expect(call.tool_args).toBe(argsText.slice(0, MAX));
});
it("omits fields whose value cannot be serialized", () => {
const circular: Record<string, unknown> = {};
circular.self = circular;
expect(
createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
args: circular,
result: undefined,
}),
).toEqual({ tool_name: "t", tool_call_id: "call-1" });
});
it("summarizes base64 image and audio blocks in an mcp result", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: [
{ type: "text", text: "keep me" },
{ type: "image", data: "A".repeat(4096) },
],
});
expect(call.tool_source).toBe("mcp");
expect(call.tool_result).toContain("keep me");
expect(call.tool_result).toContain("[image: 3.0KB]");
expect(call.tool_result).not.toContain("A".repeat(200));
});
it("summarizes a short base64 payload the same as a long one", () => {
// `aGk=` is how this repo's own MCP fixtures spell a whole image.
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: {
content: [
{ type: "image", data: "aGk=", mimeType: "image/png" },
{ type: "audio", data: "aGk=", mimeType: "audio/wav" },
{
type: "resource",
resource: { uri: "file:///a.pdf", blob: "aGk=" },
},
],
},
});
expect(call.tool_result).not.toContain("aGk=");
expect(call.tool_result).toContain("[image:");
expect(call.tool_result).toContain("[audio:");
expect(call.tool_result).toContain("[resource:");
// a field the grammar does not define as base64 is left alone
expect(call.tool_result).toContain("image/png");
});
it("leaves plain text in an mcp result untouched", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: { content: [{ type: "text", text: "test" }] },
});
expect(call.tool_result).toContain("test");
});
it("summarizes base64 blocks inside an mcp CallToolResult envelope", () => {
// what @modelcontextprotocol/sdk callTool actually returns
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: {
content: [
{ type: "text", text: "keep me" },
{ type: "image", data: "A".repeat(4096) },
],
isError: false,
},
});
expect(call.tool_result).toContain("keep me");
expect(call.tool_result).toContain("[image: 3.0KB]");
expect(call.tool_result).not.toContain("A".repeat(200));
});
it("keeps the envelope's sibling fields when summarizing its content", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: {
content: [{ type: "image", data: "A".repeat(4096) }],
isError: true,
structuredContent: { ok: false },
},
});
expect(call.tool_result).toContain("[image: 3.0KB]");
expect(call.tool_result).toContain('"isError":true');
expect(call.tool_result).toContain('"structuredContent":{"ok":false}');
});
it("summarizes a CallToolResult that arrived as a JSON string", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: JSON.stringify({
content: [{ type: "audio", data: "A".repeat(4096) }],
}),
});
expect(call.tool_result).toContain("[audio: 3.0KB]");
expect(call.tool_result).not.toContain("A".repeat(200));
});
it("leaves an object without content blocks alone", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: { content: "not blocks", other: 1 },
});
expect(call.tool_result).toContain('"content":"not blocks"');
expect(call.tool_result).toContain('"other":1');
});
it("summarizes the base64 blob of an embedded resource", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: {
content: [
{
type: "resource",
resource: {
uri: "file:///report.pdf",
mimeType: "application/pdf",
blob: "A".repeat(4096),
},
},
],
},
});
expect(call.tool_result).toContain("[resource: 3.0KB]");
expect(call.tool_result).toContain("file:///report.pdf");
expect(call.tool_result).not.toContain("A".repeat(200));
});
it("leaves a text-bearing embedded resource alone", () => {
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "mcp",
result: {
content: [
{
type: "resource",
resource: { uri: "file:///a.txt", text: "plain text" },
},
],
},
});
expect(call.tool_result).toContain("plain text");
});
it("leaves a non-mcp result unsummarized", () => {
const result = [{ type: "image", data: "A".repeat(4096) }];
const call = createRunTelemetryToolCall({
toolName: "t",
toolCallId: "call-1",
toolSource: "frontend",
result,
});
expect(call.tool_source).toBe("frontend");
expect(call.tool_result).toBe(JSON.stringify(result));
});
});
describe("normalizeRunTelemetryUsage", () => {
it("prefers the current names over the legacy ones", () => {
expect(
normalizeRunTelemetryUsage({
inputTokens: 1,
outputTokens: 2,
promptTokens: 90,
completionTokens: 90,
}),
).toEqual({ inputTokens: 1, outputTokens: 2 });
});
it("falls back to the legacy prompt and completion names", () => {
expect(
normalizeRunTelemetryUsage({ promptTokens: 3, completionTokens: 4 }),
).toEqual({ inputTokens: 3, outputTokens: 4 });
});
it("keeps a zero count and omits an absent one", () => {
expect(
normalizeRunTelemetryUsage({ inputTokens: 0, cachedInputTokens: 5 }),
).toEqual({ inputTokens: 0, cachedInputTokens: 5 });
});
it("reads the AI SDK v7 token detail objects", () => {
expect(
normalizeRunTelemetryUsage({
inputTokens: 12,
outputTokens: 7,
inputTokenDetails: { cacheReadTokens: 5 },
outputTokenDetails: { reasoningTokens: 3 },
}),
).toEqual({
inputTokens: 12,
outputTokens: 7,
reasoningTokens: 3,
cachedInputTokens: 5,
});
});
it("prefers the top-level detail counts over the nested ones", () => {
expect(
normalizeRunTelemetryUsage({
reasoningTokens: 3,
cachedInputTokens: 5,
inputTokenDetails: { cacheReadTokens: 90 },
outputTokenDetails: { reasoningTokens: 90 },
}),
).toEqual({ reasoningTokens: 3, cachedInputTokens: 5 });
});
it("returns a usage object when only the nested counts are present", () => {
expect(
normalizeRunTelemetryUsage({
inputTokenDetails: { cacheReadTokens: 5 },
}),
).toEqual({ cachedInputTokens: 5 });
});
it("returns undefined when no count is present", () => {
expect(normalizeRunTelemetryUsage({})).toBeUndefined();
expect(
normalizeRunTelemetryUsage({
inputTokenDetails: {},
outputTokenDetails: {},
}),
).toBeUndefined();
});
});
describe("extractRunTelemetryModelId", () => {
it("prefers an explicit model ID over every fallback", () => {
expect(
extractRunTelemetryModelId({
modelId: "explicit",
custom: { modelId: "custom" },
steps: [{ response: { modelId: "step" } }],
}),
).toBe("explicit");
});
it("falls back to the custom bag before the steps", () => {
expect(
extractRunTelemetryModelId({
custom: { modelId: "custom" },
steps: [{ response: { modelId: "step" } }],
}),
).toBe("custom");
});
it("reads the first step that carries a response model ID", () => {
expect(
extractRunTelemetryModelId({
steps: [{ usage: { inputTokens: 1 } }, { response: { modelId: "b" } }],
}),
).toBe("b");
});
it("returns undefined when no channel carries one", () => {
expect(extractRunTelemetryModelId(undefined)).toBeUndefined();
expect(extractRunTelemetryModelId({})).toBeUndefined();
expect(
extractRunTelemetryModelId({
modelId: 7,
custom: { modelId: null },
steps: [null, "step", { response: null }, { response: { modelId: 7 } }],
}),
).toBeUndefined();
});
});