UNPKG

assistant-cloud

Version:

Cloud integration for assistant-ui

276 lines (275 loc) • 12 kB
// src/runTelemetry.ts const MAX_TELEMETRY_TEXT_LENGTH = 5e4; const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/; /** * Maps a finish event to the report status and outcome. `fallbackStatus` * applies when the event carries neither a finish reason nor a failure flag. */ function deriveRunOutcome(input, fallbackStatus = "completed") { if (input.isError) return { status: "error" }; if (input.isAbort) return { status: "incomplete", outcome: "aborted" }; if (input.isDisconnect) return { status: "incomplete", outcome: "disconnected" }; switch (input.finishReason) { case "length": return { status: "incomplete", outcome: "length" }; case "content-filter": case "content_filter": return { status: "incomplete", outcome: "content_filter" }; case "cancelled": return { status: "incomplete", outcome: "aborted" }; case "error": return { status: "error" }; default: return { status: input.finishReason === void 0 ? fallbackStatus : "completed" }; } } const MAX_RUN_ERROR_CODE_LENGTH = 64; const MAX_RUN_ERROR_LENGTH = 2048; /** * Reads the message, code and outcome the runs endpoint stores for a failed * run. The code is the error's HTTP `statusCode` when it carries one, else its * `code`, else its class name. The outcome comes from the HTTP status. */ function describeRunError(error) { if (error == null) return {}; const record = typeof error === "object" ? error : void 0; const message = typeof error === "string" ? error : typeof record?.message === "string" ? record.message : void 0; const statusCode = typeof record?.statusCode === "number" && Number.isInteger(record.statusCode) ? record.statusCode : void 0; const code = statusCode !== void 0 ? String(statusCode) : typeof record?.code === "string" ? record.code : typeof record?.name === "string" && record.name !== "Error" ? record.name : void 0; const outcome = statusCode !== void 0 ? outcomeForStatusCode(statusCode) : void 0; return { ...message ? { error: message.slice(0, MAX_RUN_ERROR_LENGTH) } : void 0, ...code ? { errorCode: code.slice(0, MAX_RUN_ERROR_CODE_LENGTH) } : void 0, ...outcome ? { outcome } : void 0 }; } const outcomeForStatusCode = (statusCode) => { if (statusCode === 429) return "rate_limited"; if (statusCode === 400 || statusCode === 422) return "validation_failed"; if (statusCode >= 500 && statusCode < 600) return "server_error"; }; /** * Clamps a string to the size the runs endpoint accepts for a single span * field. */ function truncateRunTelemetryText(value) { if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value; return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH); } function safeStringify(value) { if (value == null) return void 0; try { return truncateRunTelemetryText(JSON.stringify(value)); } catch { return; } } const base64SizeKB = (value) => (value.length * 3 / 4 / 1024).toFixed(1); const isInlineBase64 = (value) => { if (typeof value !== "string") return false; const head = value.slice(0, 200); return head.length > 0 && head.length % 4 === 0 && BASE64_PATTERN.test(head); }; function summarizeMcpContentBlock(item) { if (!item || typeof item !== "object") return item; const block = item; if ((block.type === "image" || block.type === "audio") && isInlineBase64(block.data)) return { ...block, data: `[${String(block.type)}: ${base64SizeKB(block.data)}KB]` }; if (block.type === "resource" && block.resource && typeof block.resource === "object") { const resource = block.resource; if (isInlineBase64(resource.blob)) return { ...block, resource: { ...resource, blob: `[resource: ${base64SizeKB(resource.blob)}KB]` } }; } return item; } function summarizeMcpResult(value) { if (value == null) return void 0; try { const parsed = typeof value === "string" ? JSON.parse(value) : value; if (Array.isArray(parsed)) return truncateRunTelemetryText(JSON.stringify(parsed.map(summarizeMcpContentBlock))); if (typeof parsed === "object") { const content = parsed.content; if (Array.isArray(content)) return truncateRunTelemetryText(JSON.stringify({ ...parsed, content: content.map(summarizeMcpContentBlock) })); } } catch {} return safeStringify(value); } /** * Serializes one tool call into the shape the runs endpoint accepts. An `mcp` * source has its result summarized, because MCP content blocks carry inline * base64 image and audio payloads that would otherwise dominate the report. */ function createRunTelemetryToolCall(init) { const { toolName, toolCallId, args, argsText, result, toolSource } = init; const call = { tool_name: toolName, tool_call_id: toolCallId }; const toolArgs = argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args); if (toolArgs !== void 0) call.tool_args = toolArgs; const toolResult = toolSource === "mcp" ? summarizeMcpResult(result) : safeStringify(result); if (toolResult !== void 0) call.tool_result = toolResult; if (toolSource) call.tool_source = toolSource; return call; } /** * Resolves the model ID a run reports, in the order an app can supply it: an * explicit `modelId`, the `custom` bag, then the per-step `response.modelId` * that a `messageMetadata` callback copies off the AI SDK's finish-step part. * The AI SDK puts no model ID on a UI message part, so message metadata is the * only channel one arrives on. */ function extractRunTelemetryModelId(metadata) { if (!metadata) return void 0; if (typeof metadata.modelId === "string") return metadata.modelId; const custom = metadata.custom; if (typeof custom?.modelId === "string") return custom.modelId; const steps = metadata.steps; if (!Array.isArray(steps)) return void 0; for (const step of steps) { if (!step || typeof step !== "object") continue; const response = step.response; if (!response || typeof response !== "object") continue; const modelId = response.modelId; if (typeof modelId === "string") return modelId; } } function assignUsage(report, usage) { if (!usage) return; const normalized = normalizeRunTelemetryUsage(usage); if (!normalized) return; if (normalized.inputTokens !== void 0) report.input_tokens = normalized.inputTokens; if (normalized.outputTokens !== void 0) report.output_tokens = normalized.outputTokens; if (normalized.reasoningTokens !== void 0) report.reasoning_tokens = normalized.reasoningTokens; if (normalized.cachedInputTokens !== void 0) report.cached_input_tokens = normalized.cachedInputTokens; } function createRunReportStep(init) { const step = {}; const usage = init.usage ? normalizeRunTelemetryUsage(init.usage) : void 0; if (usage?.inputTokens !== void 0) step.input_tokens = usage.inputTokens; if (usage?.outputTokens !== void 0) step.output_tokens = usage.outputTokens; if (usage?.reasoningTokens !== void 0) step.reasoning_tokens = usage.reasoningTokens; if (usage?.cachedInputTokens !== void 0) step.cached_input_tokens = usage.cachedInputTokens; if (init.toolCalls !== void 0) step.tool_calls = init.toolCalls; if (init.startMs !== void 0) step.start_ms = init.startMs; if (init.endMs !== void 0) step.end_ms = init.endMs; if (init.finishReason !== void 0) step.finish_reason = init.finishReason.slice(0, 32); if (init.input !== void 0) step.input = truncateRunTelemetryText(init.input); return step; } function normalizeRunReportTags(tags) { if (!tags) return void 0; const result = []; const seen = /* @__PURE__ */ new Set(); for (const tag of tags) { const normalized = tag.trim().slice(0, 64).trim(); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); result.push(normalized); if (result.length === 20) break; } return result.length > 0 ? result : void 0; } function normalizeRunReportMilliseconds(value) { if (value === void 0 || !Number.isFinite(value)) return void 0; return Math.max(0, Math.round(value)); } function normalizeRunReportCost(value) { if (value === void 0 || !Number.isFinite(value) || value < 0) return; return value; } function assignCostDetails(report, costDetails) { if (!costDetails) return; const normalized = {}; const input = normalizeRunReportCost(costDetails.input); if (input !== void 0) normalized.input = input; const inputCachedTokens = normalizeRunReportCost(costDetails.inputCachedTokens); if (inputCachedTokens !== void 0) normalized.input_cached_tokens = inputCachedTokens; const output = normalizeRunReportCost(costDetails.output); if (output !== void 0) normalized.output = output; const total = normalizeRunReportCost(costDetails.total); if (total !== void 0) normalized.total = total; if (Object.keys(normalized).length > 0) report.cost_details = normalized; } function createRunReport(init) { const report = { thread_id: init.threadId, status: init.status }; const traceId = init.traceId?.toLowerCase(); if (traceId && /^[0-9a-f]{32}$/.test(traceId)) report.trace_id = traceId; const rootSpanId = init.rootSpanId?.toLowerCase(); if (rootSpanId && /^[0-9a-f]{16}$/.test(rootSpanId)) report.root_span_id = rootSpanId; if (init.outcome !== void 0) report.outcome_type = init.outcome; if (init.errorCode !== void 0) report.error_code = init.errorCode; if (init.error !== void 0) report.error = init.error; if (init.messageId !== void 0) report.message_id = init.messageId; if (init.userMessageId !== void 0) report.user_message_id = init.userMessageId; if (init.modelId !== void 0) report.model_id = init.modelId; if (init.provider !== void 0) { report.provider = init.provider; report.provider_type = init.provider; } assignUsage(report, init.usage); if (init.steps !== void 0) { report.steps = init.steps.map(createRunReportStep); report.total_steps = init.steps.length; } else if (init.totalSteps !== void 0) report.total_steps = init.totalSteps; if (init.toolCalls !== void 0) report.tool_calls = init.toolCalls; const durationMs = normalizeRunReportMilliseconds(init.durationMs); if (durationMs !== void 0) report.duration_ms = durationMs; const firstTokenMs = normalizeRunReportMilliseconds(init.firstTokenMs); if (firstTokenMs !== void 0) report.first_token_ms = firstTokenMs; const costUsd = normalizeRunReportCost(init.costUsd); if (costUsd !== void 0) report.cost_usd = costUsd; assignCostDetails(report, init.costDetails); if (init.outputText !== void 0) report.output_text = truncateRunTelemetryText(init.outputText); if (init.attributes !== void 0) report.attributes = init.attributes; if (init.metadata !== void 0) report.metadata = init.metadata; if (init.telemetry?.environment !== void 0) report.environment = init.telemetry.environment; if (init.telemetry?.release !== void 0) report.release = init.telemetry.release; const tags = normalizeRunReportTags(init.telemetry?.tags); if (tags !== void 0) report.tags = tags; return report; } /** * Resolves the token counts a provider reports under any of the names the AI * SDK has used: the current top-level ones, the legacy prompt/completion pair, * and the v7 token detail objects. Returns undefined when no count is present, * so callers can tell an empty usage object from a zeroed one. */ function normalizeRunTelemetryUsage(usage) { const inputTokens = usage.inputTokens ?? usage.promptTokens; const outputTokens = usage.outputTokens ?? usage.completionTokens; const reasoningTokens = usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens; const cachedInputTokens = usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens; if (inputTokens == null && outputTokens == null && reasoningTokens == null && cachedInputTokens == null) return; return { ...inputTokens != null ? { inputTokens } : void 0, ...outputTokens != null ? { outputTokens } : void 0, ...reasoningTokens != null ? { reasoningTokens } : void 0, ...cachedInputTokens != null ? { cachedInputTokens } : void 0 }; } export { createRunReport, createRunTelemetryToolCall, deriveRunOutcome, describeRunError, extractRunTelemetryModelId, normalizeRunTelemetryUsage, truncateRunTelemetryText }; //# sourceMappingURL=runTelemetry.js.map