assistant-cloud 0.1.42 → 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 +79 -18
  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 +53 -18
  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 +79 -9
  48. package/dist/runTelemetry.d.ts.map +1 -1
  49. package/dist/runTelemetry.js +152 -6
  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 +153 -46
  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 +65 -29
  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 +366 -0
  75. package/src/runTelemetry.ts +271 -16
  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 +354 -11
  81. package/src/tests/CloudMessagePersistence.test.ts +208 -0
@@ -1 +1 @@
1
- {"version":3,"file":"runTelemetry.js","names":[],"sources":["../src/runTelemetry.ts"],"sourcesContent":["import type { SamplingCallData } from \"./instrumentMcpSampling\";\n\nconst MAX_TELEMETRY_TEXT_LENGTH = 50_000;\n\nconst BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;\n\nexport type AssistantCloudRunReportToolCall = {\n tool_name: string;\n tool_call_id: string;\n tool_args?: string;\n tool_result?: string;\n tool_source?: \"mcp\" | \"frontend\" | \"backend\";\n start_ms?: number;\n end_ms?: number;\n sampling_calls?: SamplingCallData[];\n};\n\n/**\n * Clamps a string to the size the runs endpoint accepts for a single span\n * field.\n */\nexport function truncateRunTelemetryText(value: string): string {\n if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;\n return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);\n}\n\nfunction safeStringify(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n return truncateRunTelemetryText(JSON.stringify(value));\n } catch {\n return undefined;\n }\n}\n\nfunction summarizeMcpResult(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n const parsed = typeof value === \"string\" ? JSON.parse(value) : value;\n if (Array.isArray(parsed)) {\n const summarized = parsed.map((item) => {\n if (item && typeof item === \"object\" && item.type) {\n if (\n (item.type === \"image\" || item.type === \"audio\") &&\n typeof item.data === \"string\" &&\n BASE64_PATTERN.test(item.data.slice(0, 200))\n ) {\n const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);\n return { ...item, data: `[${item.type}: ${sizeKB}KB]` };\n }\n }\n return item;\n });\n return truncateRunTelemetryText(JSON.stringify(summarized));\n }\n } catch {\n // not JSON array, fall through\n }\n return safeStringify(value);\n}\n\nexport type RunTelemetryToolCallInit = {\n toolName: string;\n toolCallId: string;\n args?: unknown;\n /**\n * Pre-serialized arguments, used in place of serializing `args`. Values over\n * the span size are clamped before they are included in the report.\n */\n argsText?: string | undefined;\n result?: unknown;\n toolSource?: \"mcp\" | \"frontend\" | \"backend\" | undefined;\n};\n\n/**\n * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`\n * source has its result summarized, because MCP content blocks carry inline\n * base64 image and audio payloads that would otherwise dominate the report.\n */\nexport function createRunTelemetryToolCall(\n init: RunTelemetryToolCallInit,\n): AssistantCloudRunReportToolCall {\n const { toolName, toolCallId, args, argsText, result, toolSource } = init;\n const call: AssistantCloudRunReportToolCall = {\n tool_name: toolName,\n tool_call_id: toolCallId,\n };\n const toolArgs =\n argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);\n if (toolArgs !== undefined) call.tool_args = toolArgs;\n const toolResult =\n toolSource === \"mcp\" ? summarizeMcpResult(result) : safeStringify(result);\n if (toolResult !== undefined) call.tool_result = toolResult;\n if (toolSource) call.tool_source = toolSource;\n return call;\n}\n\nexport type RunTelemetryUsage = {\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport type RunTelemetryUsageInit = RunTelemetryUsage & {\n promptTokens?: number;\n completionTokens?: number;\n};\n\n/**\n * Resolves the token counts a provider reports under either the current or the\n * legacy prompt/completion names. Returns undefined when no count is present,\n * so callers can tell an empty usage object from a zeroed one.\n */\nexport function normalizeRunTelemetryUsage(\n usage: RunTelemetryUsageInit,\n): RunTelemetryUsage | undefined {\n const inputTokens = usage.inputTokens ?? usage.promptTokens;\n const outputTokens = usage.outputTokens ?? usage.completionTokens;\n\n if (\n inputTokens == null &&\n outputTokens == null &&\n usage.reasoningTokens == null &&\n usage.cachedInputTokens == null\n ) {\n return undefined;\n }\n\n return {\n ...(inputTokens != null ? { inputTokens } : undefined),\n ...(outputTokens != null ? { outputTokens } : undefined),\n ...(usage.reasoningTokens != null\n ? { reasoningTokens: usage.reasoningTokens }\n : undefined),\n ...(usage.cachedInputTokens != null\n ? { cachedInputTokens: usage.cachedInputTokens }\n : undefined),\n };\n}\n"],"mappings":";AAEA,MAAM,4BAA4B;AAElC,MAAM,iBAAiB;;;;;AAiBvB,SAAgB,yBAAyB,OAAuB;CAC9D,IAAI,MAAM,UAAU,2BAA2B,OAAO;CACtD,OAAO,MAAM,MAAM,GAAG,yBAAyB;AACjD;AAEA,SAAS,cAAc,OAAoC;CACzD,IAAI,SAAS,MAAM,OAAO,KAAA;CAC1B,IAAI;EACF,OAAO,yBAAyB,KAAK,UAAU,KAAK,CAAC;CACvD,QAAQ;EACN;CACF;AACF;AAEA,SAAS,mBAAmB,OAAoC;CAC9D,IAAI,SAAS,MAAM,OAAO,KAAA;CAC1B,IAAI;EACF,MAAM,SAAS,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;EAC/D,IAAI,MAAM,QAAQ,MAAM,GAAG;GACzB,MAAM,aAAa,OAAO,KAAK,SAAS;IACtC,IAAI,QAAQ,OAAO,SAAS,YAAY,KAAK,MAExC;UAAA,KAAK,SAAS,WAAW,KAAK,SAAS,YACxC,OAAO,KAAK,SAAS,YACrB,eAAe,KAAK,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC,GAC3C;MACA,MAAM,UAAW,KAAK,KAAK,SAAS,IAAK,IAAI,KAAA,CAAM,QAAQ,CAAC;MAC5D,OAAO;OAAE,GAAG;OAAM,MAAM,IAAI,KAAK,KAAK,IAAI,OAAO;MAAK;KACxD;;IAEF,OAAO;GACT,CAAC;GACD,OAAO,yBAAyB,KAAK,UAAU,UAAU,CAAC;EAC5D;CACF,QAAQ,CAER;CACA,OAAO,cAAc,KAAK;AAC5B;;;;;;AAoBA,SAAgB,2BACd,MACiC;CACjC,MAAM,EAAE,UAAU,YAAY,MAAM,UAAU,QAAQ,eAAe;CACrE,MAAM,OAAwC;EAC5C,WAAW;EACX,cAAc;CAChB;CACA,MAAM,WACJ,YAAY,OAAO,yBAAyB,QAAQ,IAAI,cAAc,IAAI;CAC5E,IAAI,aAAa,KAAA,GAAW,KAAK,YAAY;CAC7C,MAAM,aACJ,eAAe,QAAQ,mBAAmB,MAAM,IAAI,cAAc,MAAM;CAC1E,IAAI,eAAe,KAAA,GAAW,KAAK,cAAc;CACjD,IAAI,YAAY,KAAK,cAAc;CACnC,OAAO;AACT;;;;;;AAmBA,SAAgB,2BACd,OAC+B;CAC/B,MAAM,cAAc,MAAM,eAAe,MAAM;CAC/C,MAAM,eAAe,MAAM,gBAAgB,MAAM;CAEjD,IACE,eAAe,QACf,gBAAgB,QAChB,MAAM,mBAAmB,QACzB,MAAM,qBAAqB,MAE3B;CAGF,OAAO;EACL,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,KAAA;EAC5C,GAAI,gBAAgB,OAAO,EAAE,aAAa,IAAI,KAAA;EAC9C,GAAI,MAAM,mBAAmB,OACzB,EAAE,iBAAiB,MAAM,gBAAgB,IACzC,KAAA;EACJ,GAAI,MAAM,qBAAqB,OAC3B,EAAE,mBAAmB,MAAM,kBAAkB,IAC7C,KAAA;CACN;AACF"}
1
+ {"version":3,"file":"runTelemetry.js","names":[],"sources":["../src/runTelemetry.ts"],"sourcesContent":["import type { SamplingCallData } from \"./instrumentMcpSampling\";\nimport type { AssistantCloudRunReport } from \"./AssistantCloudRuns\";\n\nconst MAX_TELEMETRY_TEXT_LENGTH = 50_000;\n\nconst BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;\n\nexport type AssistantCloudRunReportToolCall = {\n tool_name: string;\n tool_call_id: string;\n tool_args?: string;\n tool_result?: string;\n tool_source?: \"mcp\" | \"frontend\" | \"backend\";\n start_ms?: number;\n end_ms?: number;\n sampling_calls?: SamplingCallData[];\n};\n\nexport type RunReportOutcome =\n | \"aborted\"\n | \"disconnected\"\n | \"length\"\n | \"content_filter\";\n\n/**\n * Maps a finish event to the report status and outcome. `fallbackStatus`\n * applies when the event carries neither a finish reason nor a failure flag.\n */\nexport function deriveRunOutcome(\n input: {\n finishReason?: string | undefined;\n isAbort?: boolean | undefined;\n isDisconnect?: boolean | undefined;\n isError?: boolean | undefined;\n },\n fallbackStatus: \"completed\" | \"incomplete\" = \"completed\",\n): {\n status: \"completed\" | \"incomplete\" | \"error\";\n outcome?: RunReportOutcome;\n} {\n if (input.isError) return { status: \"error\" };\n if (input.isAbort) return { status: \"incomplete\", outcome: \"aborted\" };\n if (input.isDisconnect) {\n return { status: \"incomplete\", outcome: \"disconnected\" };\n }\n switch (input.finishReason) {\n case \"length\":\n return { status: \"incomplete\", outcome: \"length\" };\n case \"content-filter\":\n case \"content_filter\":\n return { status: \"incomplete\", outcome: \"content_filter\" };\n case \"cancelled\":\n return { status: \"incomplete\", outcome: \"aborted\" };\n case \"error\":\n return { status: \"error\" };\n default:\n return {\n status: input.finishReason === undefined ? fallbackStatus : \"completed\",\n };\n }\n}\n\nconst MAX_RUN_ERROR_CODE_LENGTH = 64;\nconst MAX_RUN_ERROR_LENGTH = 2048;\n\n/**\n * Reads the message and code the runs endpoint stores for a failed run. The\n * code is the error's `code` when it has one, else its class name.\n */\nexport function describeRunError(error: unknown): {\n error?: string;\n errorCode?: string;\n} {\n if (error == null) return {};\n const record =\n typeof error === \"object\" ? (error as Record<string, unknown>) : undefined;\n const message =\n typeof error === \"string\"\n ? error\n : typeof record?.message === \"string\"\n ? record.message\n : undefined;\n const code =\n typeof record?.code === \"string\"\n ? record.code\n : typeof record?.name === \"string\" && record.name !== \"Error\"\n ? record.name\n : undefined;\n return {\n ...(message\n ? { error: message.slice(0, MAX_RUN_ERROR_LENGTH) }\n : undefined),\n ...(code\n ? { errorCode: code.slice(0, MAX_RUN_ERROR_CODE_LENGTH) }\n : undefined),\n };\n}\n\n/**\n * Clamps a string to the size the runs endpoint accepts for a single span\n * field.\n */\nexport function truncateRunTelemetryText(value: string): string {\n if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;\n return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);\n}\n\nfunction safeStringify(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n return truncateRunTelemetryText(JSON.stringify(value));\n } catch {\n return undefined;\n }\n}\n\nfunction summarizeMcpResult(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n const parsed = typeof value === \"string\" ? JSON.parse(value) : value;\n if (Array.isArray(parsed)) {\n const summarized = parsed.map((item) => {\n if (item && typeof item === \"object\" && item.type) {\n if (\n (item.type === \"image\" || item.type === \"audio\") &&\n typeof item.data === \"string\" &&\n BASE64_PATTERN.test(item.data.slice(0, 200))\n ) {\n const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);\n return { ...item, data: `[${item.type}: ${sizeKB}KB]` };\n }\n }\n return item;\n });\n return truncateRunTelemetryText(JSON.stringify(summarized));\n }\n } catch {\n // not JSON array, fall through\n }\n return safeStringify(value);\n}\n\nexport type RunTelemetryToolCallInit = {\n toolName: string;\n toolCallId: string;\n args?: unknown;\n /**\n * Pre-serialized arguments, used in place of serializing `args`. Values over\n * the span size are clamped before they are included in the report.\n */\n argsText?: string | undefined;\n result?: unknown;\n toolSource?: \"mcp\" | \"frontend\" | \"backend\" | undefined;\n};\n\n/**\n * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`\n * source has its result summarized, because MCP content blocks carry inline\n * base64 image and audio payloads that would otherwise dominate the report.\n */\nexport function createRunTelemetryToolCall(\n init: RunTelemetryToolCallInit,\n): AssistantCloudRunReportToolCall {\n const { toolName, toolCallId, args, argsText, result, toolSource } = init;\n const call: AssistantCloudRunReportToolCall = {\n tool_name: toolName,\n tool_call_id: toolCallId,\n };\n const toolArgs =\n argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);\n if (toolArgs !== undefined) call.tool_args = toolArgs;\n const toolResult =\n toolSource === \"mcp\" ? summarizeMcpResult(result) : safeStringify(result);\n if (toolResult !== undefined) call.tool_result = toolResult;\n if (toolSource) call.tool_source = toolSource;\n return call;\n}\n\n/**\n * Resolves the model ID a run reports, in the order an app can supply it: an\n * explicit `modelId`, the `custom` bag, then the per-step `response.modelId`\n * that a `messageMetadata` callback copies off the AI SDK's finish-step part.\n * The AI SDK puts no model ID on a UI message part, so message metadata is the\n * only channel one arrives on.\n */\nexport function extractRunTelemetryModelId(\n metadata: Record<string, unknown> | undefined,\n): string | undefined {\n if (!metadata) return undefined;\n if (typeof metadata.modelId === \"string\") return metadata.modelId;\n const custom = metadata.custom as Record<string, unknown> | undefined;\n if (typeof custom?.modelId === \"string\") return custom.modelId;\n\n const steps: unknown = metadata.steps;\n if (!Array.isArray(steps)) return undefined;\n for (const step of steps as unknown[]) {\n if (!step || typeof step !== \"object\") continue;\n const response = (step as Record<string, unknown>).response;\n if (!response || typeof response !== \"object\") continue;\n const modelId = (response as Record<string, unknown>).modelId;\n if (typeof modelId === \"string\") return modelId;\n }\n return undefined;\n}\n\nexport type RunTelemetryUsage = {\n inputTokens?: number | undefined;\n outputTokens?: number | undefined;\n reasoningTokens?: number | undefined;\n cachedInputTokens?: number | undefined;\n};\n\nexport type RunTelemetryUsageInit = RunTelemetryUsage & {\n promptTokens?: number | undefined;\n completionTokens?: number | undefined;\n inputTokenDetails?: { cacheReadTokens?: number };\n outputTokenDetails?: { reasoningTokens?: number };\n};\n\nexport type RunReportStepInit = {\n usage?: RunTelemetryUsageInit | undefined;\n toolCalls?: AssistantCloudRunReportToolCall[] | undefined;\n startMs?: number | undefined;\n endMs?: number | undefined;\n finishReason?: string | undefined;\n};\n\nexport type RunReportInit = {\n threadId: string;\n status: AssistantCloudRunReport[\"status\"];\n outcome?: RunReportOutcome | undefined;\n errorCode?: string | undefined;\n error?: string | undefined;\n messageId?: string | undefined;\n traceId?: string | undefined;\n modelId?: string | undefined;\n provider?: string | undefined;\n usage?: RunTelemetryUsageInit | undefined;\n steps?: RunReportStepInit[] | undefined;\n totalSteps?: number | undefined;\n toolCalls?: AssistantCloudRunReportToolCall[] | undefined;\n durationMs?: number | undefined;\n firstTokenMs?: number | undefined;\n outputText?: string | undefined;\n metadata?: Record<string, unknown> | undefined;\n telemetry?: {\n environment?: string | undefined;\n release?: string | undefined;\n tags?: readonly string[] | undefined;\n };\n};\n\nfunction assignUsage(\n report: AssistantCloudRunReport,\n usage: RunTelemetryUsageInit | undefined,\n): void {\n if (!usage) return;\n const normalized = normalizeRunTelemetryUsage(usage);\n if (!normalized) return;\n if (normalized.inputTokens !== undefined) {\n report.input_tokens = normalized.inputTokens;\n }\n if (normalized.outputTokens !== undefined) {\n report.output_tokens = normalized.outputTokens;\n }\n if (normalized.reasoningTokens !== undefined) {\n report.reasoning_tokens = normalized.reasoningTokens;\n }\n if (normalized.cachedInputTokens !== undefined) {\n report.cached_input_tokens = normalized.cachedInputTokens;\n }\n}\n\nfunction createRunReportStep(\n init: RunReportStepInit,\n): NonNullable<AssistantCloudRunReport[\"steps\"]>[number] {\n const step: NonNullable<AssistantCloudRunReport[\"steps\"]>[number] = {};\n const usage = init.usage ? normalizeRunTelemetryUsage(init.usage) : undefined;\n if (usage?.inputTokens !== undefined) step.input_tokens = usage.inputTokens;\n if (usage?.outputTokens !== undefined)\n step.output_tokens = usage.outputTokens;\n if (usage?.reasoningTokens !== undefined) {\n step.reasoning_tokens = usage.reasoningTokens;\n }\n if (usage?.cachedInputTokens !== undefined) {\n step.cached_input_tokens = usage.cachedInputTokens;\n }\n if (init.toolCalls !== undefined) step.tool_calls = init.toolCalls;\n if (init.startMs !== undefined) step.start_ms = init.startMs;\n if (init.endMs !== undefined) step.end_ms = init.endMs;\n if (init.finishReason !== undefined) {\n step.finish_reason = init.finishReason.slice(0, 32);\n }\n return step;\n}\n\nfunction normalizeRunReportTags(\n tags: readonly string[] | undefined,\n): string[] | undefined {\n if (!tags) return undefined;\n const result: string[] = [];\n const seen = new Set<string>();\n for (const tag of tags) {\n const normalized = tag.trim().slice(0, 64).trim();\n if (!normalized || seen.has(normalized)) continue;\n seen.add(normalized);\n result.push(normalized);\n if (result.length === 20) break;\n }\n return result.length > 0 ? result : undefined;\n}\n\nfunction normalizeRunReportMilliseconds(\n value: number | undefined,\n): number | undefined {\n if (value === undefined || !Number.isFinite(value)) return undefined;\n return Math.max(0, Math.round(value));\n}\n\nexport function createRunReport(init: RunReportInit): AssistantCloudRunReport {\n const report: AssistantCloudRunReport = {\n thread_id: init.threadId,\n status: init.status,\n };\n const traceId = init.traceId?.toLowerCase();\n if (traceId && /^[0-9a-f]{32}$/.test(traceId)) report.trace_id = traceId;\n if (init.outcome !== undefined) report.outcome_type = init.outcome;\n if (init.errorCode !== undefined) report.error_code = init.errorCode;\n if (init.error !== undefined) report.error = init.error;\n if (init.messageId !== undefined) report.message_id = init.messageId;\n if (init.modelId !== undefined) report.model_id = init.modelId;\n if (init.provider !== undefined) {\n report.provider = init.provider;\n report.provider_type = init.provider;\n }\n assignUsage(report, init.usage);\n if (init.steps !== undefined) {\n report.steps = init.steps.map(createRunReportStep);\n report.total_steps = init.steps.length;\n } else if (init.totalSteps !== undefined) {\n report.total_steps = init.totalSteps;\n }\n if (init.toolCalls !== undefined) report.tool_calls = init.toolCalls;\n const durationMs = normalizeRunReportMilliseconds(init.durationMs);\n if (durationMs !== undefined) report.duration_ms = durationMs;\n const firstTokenMs = normalizeRunReportMilliseconds(init.firstTokenMs);\n if (firstTokenMs !== undefined) report.first_token_ms = firstTokenMs;\n if (init.outputText !== undefined) {\n report.output_text = truncateRunTelemetryText(init.outputText);\n }\n if (init.metadata !== undefined) report.metadata = init.metadata;\n if (init.telemetry?.environment !== undefined) {\n report.environment = init.telemetry.environment;\n }\n if (init.telemetry?.release !== undefined) {\n report.release = init.telemetry.release;\n }\n const tags = normalizeRunReportTags(init.telemetry?.tags);\n if (tags !== undefined) report.tags = tags;\n return report;\n}\n\n/**\n * Resolves the token counts a provider reports under any of the names the AI\n * SDK has used: the current top-level ones, the legacy prompt/completion pair,\n * and the v7 token detail objects. Returns undefined when no count is present,\n * so callers can tell an empty usage object from a zeroed one.\n */\nexport function normalizeRunTelemetryUsage(\n usage: RunTelemetryUsageInit,\n): RunTelemetryUsage | undefined {\n const inputTokens = usage.inputTokens ?? usage.promptTokens;\n const outputTokens = usage.outputTokens ?? usage.completionTokens;\n // AI SDK v7 moved these under token detail objects; v6 kept them top-level.\n const reasoningTokens =\n usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens;\n const cachedInputTokens =\n usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens;\n\n if (\n inputTokens == null &&\n outputTokens == null &&\n reasoningTokens == null &&\n cachedInputTokens == null\n ) {\n return undefined;\n }\n\n return {\n ...(inputTokens != null ? { inputTokens } : undefined),\n ...(outputTokens != null ? { outputTokens } : undefined),\n ...(reasoningTokens != null ? { reasoningTokens } : undefined),\n ...(cachedInputTokens != null ? { cachedInputTokens } : undefined),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { ReadableSpan, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
+ //#region src/telemetry/index.d.ts
3
+ type AssistantCloudTraceExportOptions = {
4
+ apiKey: string;
5
+ baseUrl?: string;
6
+ headers?: Record<string, string>;
7
+ };
8
+ type AssistantCloudSpanProcessorOptions = {
9
+ filter?: (span: ReadableSpan) => boolean;
10
+ };
11
+ declare function assistantCloudTraceExportOptions({ apiKey, baseUrl, headers }: AssistantCloudTraceExportOptions): {
12
+ url: string;
13
+ headers: Record<string, string>;
14
+ };
15
+ declare function createAssistantCloudTraceExporter(options: AssistantCloudTraceExportOptions): SpanExporter;
16
+ declare function isAssistantCloudSpan(span: ReadableSpan): boolean;
17
+ declare function createAssistantCloudSpanProcessor(exporter: SpanExporter, options?: AssistantCloudSpanProcessorOptions): SpanProcessor;
18
+ declare function assistantCloudTraceMetadata(): {
19
+ traceId?: string;
20
+ };
21
+ declare function withAssistantCloudTraceMetadata<Part extends {
22
+ type: string;
23
+ } = {
24
+ type: string;
25
+ }>(messageMetadata?: (options: {
26
+ part: Part;
27
+ }) => Record<string, unknown> | undefined): (options: {
28
+ part: Part;
29
+ }) => Record<string, unknown> | undefined;
30
+ //#endregion
31
+ export { AssistantCloudSpanProcessorOptions, AssistantCloudTraceExportOptions, assistantCloudTraceExportOptions, assistantCloudTraceMetadata, createAssistantCloudSpanProcessor, createAssistantCloudTraceExporter, isAssistantCloudSpan, withAssistantCloudTraceMetadata };
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/telemetry/index.ts"],"mappings":";;KAYY;EACV;EACA;EACA,UAAU;;KAGA;EACV,UAAU,MAAM;;iBAGF,mCACd,QACA,SACA,WACC;EACD;EACA,SAAS;;iBAmBK,kCACd,SAAS,mCACR;iBAIa,qBAAqB,MAAM;iBAO3B,kCACd,UAAU,cACV,UAAS,qCACR;iBAca;EAAiC;;iBAOjC,gCACd;EAAe;;EAAmB;GAElC,mBAAmB;EACjB,MAAM;MACF,uCACJ;EAAW,MAAM;MAAW"}
@@ -0,0 +1,62 @@
1
+ import { trace } from "@opentelemetry/api";
2
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
3
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
4
+ //#region src/telemetry/index.ts
5
+ const DEFAULT_BACKEND_BASE_URL = "https://backend.assistant-api.com";
6
+ const AI_SPAN_PREFIXES = [
7
+ "gen_ai.",
8
+ "ai.",
9
+ "llm."
10
+ ];
11
+ function assistantCloudTraceExportOptions({ apiKey, baseUrl = DEFAULT_BACKEND_BASE_URL, headers }) {
12
+ if (!apiKey) throw new Error("An Assistant Cloud API key is required");
13
+ return {
14
+ url: `${trimTrailingSlashes(baseUrl)}/v1/traces`,
15
+ headers: {
16
+ Authorization: `Bearer ${apiKey}`,
17
+ ...headers
18
+ }
19
+ };
20
+ }
21
+ function trimTrailingSlashes(url) {
22
+ let end = url.length;
23
+ while (end > 0 && url[end - 1] === "/") end--;
24
+ return url.slice(0, end);
25
+ }
26
+ function createAssistantCloudTraceExporter(options) {
27
+ return new OTLPTraceExporter(assistantCloudTraceExportOptions(options));
28
+ }
29
+ function isAssistantCloudSpan(span) {
30
+ return typeof span.attributes["gen_ai.operation.name"] === "string" || AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix));
31
+ }
32
+ function createAssistantCloudSpanProcessor(exporter, options = {}) {
33
+ const processor = new BatchSpanProcessor(exporter);
34
+ const filter = options.filter ?? isAssistantCloudSpan;
35
+ return {
36
+ onStart: (span, parentContext) => processor.onStart(span, parentContext),
37
+ onEnd: (span) => {
38
+ if (filter(span)) processor.onEnd(span);
39
+ },
40
+ forceFlush: () => processor.forceFlush(),
41
+ shutdown: () => processor.shutdown()
42
+ };
43
+ }
44
+ function assistantCloudTraceMetadata() {
45
+ const spanContext = trace.getActiveSpan()?.spanContext();
46
+ return spanContext && trace.isSpanContextValid(spanContext) ? { traceId: spanContext.traceId } : {};
47
+ }
48
+ function withAssistantCloudTraceMetadata(messageMetadata) {
49
+ return (options) => {
50
+ const metadata = messageMetadata?.(options);
51
+ if (options.part.type !== "start") return metadata;
52
+ const trace = assistantCloudTraceMetadata();
53
+ return trace.traceId === void 0 ? metadata : {
54
+ ...metadata,
55
+ ...trace
56
+ };
57
+ };
58
+ }
59
+ //#endregion
60
+ export { assistantCloudTraceExportOptions, assistantCloudTraceMetadata, createAssistantCloudSpanProcessor, createAssistantCloudTraceExporter, isAssistantCloudSpan, withAssistantCloudTraceMetadata };
61
+
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/telemetry/index.ts"],"sourcesContent":["import { trace } from \"@opentelemetry/api\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport {\n BatchSpanProcessor,\n type ReadableSpan,\n type SpanExporter,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\n\nconst DEFAULT_BACKEND_BASE_URL = \"https://backend.assistant-api.com\";\nconst AI_SPAN_PREFIXES = [\"gen_ai.\", \"ai.\", \"llm.\"];\n\nexport type AssistantCloudTraceExportOptions = {\n apiKey: string;\n baseUrl?: string;\n headers?: Record<string, string>;\n};\n\nexport type AssistantCloudSpanProcessorOptions = {\n filter?: (span: ReadableSpan) => boolean;\n};\n\nexport function assistantCloudTraceExportOptions({\n apiKey,\n baseUrl = DEFAULT_BACKEND_BASE_URL,\n headers,\n}: AssistantCloudTraceExportOptions): {\n url: string;\n headers: Record<string, string>;\n} {\n if (!apiKey) throw new Error(\"An Assistant Cloud API key is required\");\n\n return {\n url: `${trimTrailingSlashes(baseUrl)}/v1/traces`,\n headers: {\n Authorization: `Bearer ${apiKey}`,\n ...headers,\n },\n };\n}\n\nfunction trimTrailingSlashes(url: string): string {\n let end = url.length;\n while (end > 0 && url[end - 1] === \"/\") end--;\n return url.slice(0, end);\n}\n\nexport function createAssistantCloudTraceExporter(\n options: AssistantCloudTraceExportOptions,\n): SpanExporter {\n return new OTLPTraceExporter(assistantCloudTraceExportOptions(options));\n}\n\nexport function isAssistantCloudSpan(span: ReadableSpan): boolean {\n return (\n typeof span.attributes[\"gen_ai.operation.name\"] === \"string\" ||\n AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))\n );\n}\n\nexport function createAssistantCloudSpanProcessor(\n exporter: SpanExporter,\n options: AssistantCloudSpanProcessorOptions = {},\n): SpanProcessor {\n const processor = new BatchSpanProcessor(exporter);\n const filter = options.filter ?? isAssistantCloudSpan;\n\n return {\n onStart: (span, parentContext) => processor.onStart(span, parentContext),\n onEnd: (span) => {\n if (filter(span)) processor.onEnd(span);\n },\n forceFlush: () => processor.forceFlush(),\n shutdown: () => processor.shutdown(),\n };\n}\n\nexport function assistantCloudTraceMetadata(): { traceId?: string } {\n const spanContext = trace.getActiveSpan()?.spanContext();\n return spanContext && trace.isSpanContextValid(spanContext)\n ? { traceId: spanContext.traceId }\n : {};\n}\n\nexport function withAssistantCloudTraceMetadata<\n Part extends { type: string } = { type: string },\n>(\n messageMetadata?: (options: {\n part: Part;\n }) => Record<string, unknown> | undefined,\n): (options: { part: Part }) => Record<string, unknown> | undefined {\n return (options) => {\n const metadata = messageMetadata?.(options);\n if (options.part.type !== \"start\") return metadata;\n const trace = assistantCloudTraceMetadata();\n return trace.traceId === undefined ? metadata : { ...metadata, ...trace };\n };\n}\n"],"mappings":";;;;AASA,MAAM,2BAA2B;AACjC,MAAM,mBAAmB;CAAC;CAAW;CAAO;AAAM;AAYlD,SAAgB,iCAAiC,EAC/C,QACA,UAAU,0BACV,WAIA;CACA,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,wCAAwC;CAErE,OAAO;EACL,KAAK,GAAG,oBAAoB,OAAO,EAAE;EACrC,SAAS;GACP,eAAe,UAAU;GACzB,GAAG;EACL;CACF;AACF;AAEA,SAAS,oBAAoB,KAAqB;CAChD,IAAI,MAAM,IAAI;CACd,OAAO,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK;CACxC,OAAO,IAAI,MAAM,GAAG,GAAG;AACzB;AAEA,SAAgB,kCACd,SACc;CACd,OAAO,IAAI,kBAAkB,iCAAiC,OAAO,CAAC;AACxE;AAEA,SAAgB,qBAAqB,MAA6B;CAChE,OACE,OAAO,KAAK,WAAW,6BAA6B,YACpD,iBAAiB,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,CAAC;AAElE;AAEA,SAAgB,kCACd,UACA,UAA8C,CAAC,GAChC;CACf,MAAM,YAAY,IAAI,mBAAmB,QAAQ;CACjD,MAAM,SAAS,QAAQ,UAAU;CAEjC,OAAO;EACL,UAAU,MAAM,kBAAkB,UAAU,QAAQ,MAAM,aAAa;EACvE,QAAQ,SAAS;GACf,IAAI,OAAO,IAAI,GAAG,UAAU,MAAM,IAAI;EACxC;EACA,kBAAkB,UAAU,WAAW;EACvC,gBAAgB,UAAU,SAAS;CACrC;AACF;AAEA,SAAgB,8BAAoD;CAClE,MAAM,cAAc,MAAM,cAAc,CAAC,EAAE,YAAY;CACvD,OAAO,eAAe,MAAM,mBAAmB,WAAW,IACtD,EAAE,SAAS,YAAY,QAAQ,IAC/B,CAAC;AACP;AAEA,SAAgB,gCAGd,iBAGkE;CAClE,QAAQ,YAAY;EAClB,MAAM,WAAW,kBAAkB,OAAO;EAC1C,IAAI,QAAQ,KAAK,SAAS,SAAS,OAAO;EAC1C,MAAM,QAAQ,4BAA4B;EAC1C,OAAO,MAAM,YAAY,KAAA,IAAY,WAAW;GAAE,GAAG;GAAU,GAAG;EAAM;CAC1E;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.1.42",
3
+ "version": "0.2.0",
4
4
  "description": "Cloud integration for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -16,6 +16,10 @@
16
16
  ".": {
17
17
  "types": "./dist/index.d.ts",
18
18
  "default": "./dist/index.js"
19
+ },
20
+ "./telemetry": {
21
+ "types": "./dist/telemetry/index.d.ts",
22
+ "default": "./dist/telemetry/index.js"
19
23
  }
20
24
  },
21
25
  "main": "./dist/index.js",
@@ -27,11 +31,31 @@
27
31
  ],
28
32
  "sideEffects": false,
29
33
  "dependencies": {
30
- "assistant-stream": "^0.3.40"
34
+ "assistant-stream": "^0.3.42"
35
+ },
36
+ "peerDependencies": {
37
+ "@opentelemetry/api": "^1.9.0",
38
+ "@opentelemetry/exporter-trace-otlp-http": ">=0.200.0",
39
+ "@opentelemetry/sdk-trace-base": "^2.1.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@opentelemetry/api": {
43
+ "optional": true
44
+ },
45
+ "@opentelemetry/exporter-trace-otlp-http": {
46
+ "optional": true
47
+ },
48
+ "@opentelemetry/sdk-trace-base": {
49
+ "optional": true
50
+ }
31
51
  },
32
52
  "devDependencies": {
33
- "@assistant-ui/x-buildutils": "0.0.24",
34
- "@types/node": "^26.2.0",
53
+ "@assistant-ui/x-buildutils": "0.0.26",
54
+ "@opentelemetry/api": "^1.9.0",
55
+ "@opentelemetry/exporter-trace-otlp-http": "^0.205.0",
56
+ "@opentelemetry/sdk-trace-base": "^2.1.0",
57
+ "@types/node": "^26.4.1",
58
+ "jsdom": "^30.0.1",
35
59
  "vitest": "^4.1.11"
36
60
  },
37
61
  "publishConfig": {
@@ -8,6 +8,8 @@ import { AssistantCloudProjects } from "./AssistantCloudProjects";
8
8
  import { AssistantCloudRuns } from "./AssistantCloudRuns";
9
9
  import { AssistantCloudThreads } from "./AssistantCloudThreads";
10
10
  import { AssistantCloudFiles } from "./AssistantCloudFiles";
11
+ import { AssistantCloudEvents } from "./AssistantCloudEvents";
12
+ import { AssistantCloudScores } from "./AssistantCloudScores";
11
13
 
12
14
  export class AssistantCloud {
13
15
  public readonly threads;
@@ -15,10 +17,23 @@ export class AssistantCloud {
15
17
  public readonly auth;
16
18
  public readonly runs;
17
19
  public readonly files;
20
+ public readonly events;
21
+ public readonly scores;
18
22
  public readonly telemetry: AssistantCloudTelemetryConfig;
19
23
 
20
24
  constructor(config: AssistantCloudConfig) {
21
25
  const api = new AssistantCloudAPI(config);
26
+ const t = config.telemetry;
27
+ this.telemetry =
28
+ t === false
29
+ ? { enabled: false }
30
+ : t === true || t === undefined
31
+ ? { enabled: true }
32
+ : {
33
+ ...t,
34
+ enabled: t.enabled !== false,
35
+ };
36
+
22
37
  this.threads = new AssistantCloudThreads(api);
23
38
  this.projects = new AssistantCloudProjects(api);
24
39
  this.auth = {
@@ -26,13 +41,10 @@ export class AssistantCloud {
26
41
  };
27
42
  this.runs = new AssistantCloudRuns(api);
28
43
  this.files = new AssistantCloudFiles(api);
29
-
30
- const t = config.telemetry;
31
- this.telemetry =
32
- t === false
33
- ? { enabled: false }
34
- : t === true || t === undefined
35
- ? { enabled: true }
36
- : { ...t, enabled: t.enabled !== false };
44
+ this.events = new AssistantCloudEvents(
45
+ api,
46
+ () => this.telemetry.enabled !== false && this.telemetry.events !== false,
47
+ );
48
+ this.scores = new AssistantCloudScores(api);
37
49
  }
38
50
  }
@@ -3,11 +3,25 @@ import {
3
3
  AssistantCloudJWTAuthStrategy,
4
4
  AssistantCloudAPIKeyAuthStrategy,
5
5
  AssistantCloudAnonymousAuthStrategy,
6
+ normalizeBaseUrl,
6
7
  } from "./AssistantCloudAuthStrategy";
7
8
  import type { AssistantCloudRunReport } from "./AssistantCloudRuns";
8
9
 
9
10
  export type AssistantCloudTelemetryConfig = {
11
+ /**
12
+ * Enables Assistant Cloud telemetry. Defaults to `true`. Set to `false` to
13
+ * disable both run reports and engagement events.
14
+ */
10
15
  enabled?: boolean;
16
+ /**
17
+ * Enables Assistant Cloud engagement events. Defaults to `true` when
18
+ * telemetry is enabled. Set to `false` to keep run reports while disabling
19
+ * engagement events.
20
+ */
21
+ events?: boolean;
22
+ release?: string;
23
+ environment?: string;
24
+ tags?: string[];
11
25
  /**
12
26
  * Called before each telemetry report is sent.
13
27
  * Return a modified report to enrich it (e.g. add `model_id`),
@@ -50,10 +64,19 @@ export type AssistantCloudConfig = (
50
64
 
51
65
  export class CloudAPIError extends Error {
52
66
  public readonly status: number;
53
-
54
- constructor(message: string, status: number) {
67
+ public readonly code?: string;
68
+ public readonly details?: Record<string, unknown>;
69
+
70
+ constructor(
71
+ message: string,
72
+ status: number,
73
+ code?: string,
74
+ details?: Record<string, unknown>,
75
+ ) {
55
76
  super(message);
56
77
  this.status = status;
78
+ if (code !== undefined) this.code = code;
79
+ if (details !== undefined) this.details = details;
57
80
  this.name = "CloudAPIError";
58
81
  }
59
82
  }
@@ -63,11 +86,7 @@ type MakeRequestOptions = {
63
86
  headers?: Record<string, string> | undefined;
64
87
  query?: Record<string, string | number | boolean> | undefined;
65
88
  body?: object | undefined;
66
- };
67
-
68
- const normalizeBaseUrl = (baseUrl: string) => {
69
- if (!baseUrl || !baseUrl.endsWith("/")) return baseUrl;
70
- return baseUrl.slice(0, -1);
89
+ keepalive?: boolean | undefined;
71
90
  };
72
91
 
73
92
  export class AssistantCloudAPI {
@@ -133,6 +152,7 @@ export class AssistantCloudAPI {
133
152
  method: options.method ?? "GET",
134
153
  headers,
135
154
  body: options.body ? JSON.stringify(options.body) : null,
155
+ ...(options.keepalive ? { keepalive: true } : {}),
136
156
  });
137
157
 
138
158
  this._auth.readAuthHeaders(response.headers);
@@ -140,15 +160,27 @@ export class AssistantCloudAPI {
140
160
  if (!response.ok) {
141
161
  const text = await response.text();
142
162
  let message: string | undefined;
163
+ let code: string | undefined;
164
+ let details: Record<string, unknown> | undefined;
143
165
  try {
144
- const body = JSON.parse(text);
145
- if (typeof body?.message === "string" && body.message.length > 0) {
146
- message = body.message;
166
+ const body = JSON.parse(text) as unknown;
167
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) {
168
+ const record = body as Record<string, unknown>;
169
+ if (typeof record.message === "string" && record.message.length > 0) {
170
+ message = record.message;
171
+ }
172
+ if (typeof record.error === "string") {
173
+ code = record.error;
174
+ details = { ...record };
175
+ delete details.error;
176
+ }
147
177
  }
148
178
  } catch {}
149
179
  throw new CloudAPIError(
150
180
  message ?? `Request failed with status ${response.status}, ${text}`,
151
181
  response.status,
182
+ code,
183
+ details,
152
184
  );
153
185
  }
154
186
 
@@ -4,6 +4,34 @@ import {
4
4
  readCloudString,
5
5
  } from "./cloudResponse";
6
6
 
7
+ const AUTH_TOKEN_REQUEST_TIMEOUT_MS = 30_000;
8
+
9
+ const withAuthTokenDeadline = async <T>(
10
+ operation: string,
11
+ run: (signal: AbortSignal) => Promise<T>,
12
+ ): Promise<T> => {
13
+ const controller = new AbortController();
14
+ let timedOut = false;
15
+ const timeout = setTimeout(() => {
16
+ timedOut = true;
17
+ controller.abort();
18
+ }, AUTH_TOKEN_REQUEST_TIMEOUT_MS);
19
+
20
+ try {
21
+ return await run(controller.signal);
22
+ } catch (error) {
23
+ if (timedOut) {
24
+ throw new Error(
25
+ `Assistant Cloud ${operation} timed out after ${AUTH_TOKEN_REQUEST_TIMEOUT_MS}ms`,
26
+ { cause: error },
27
+ );
28
+ }
29
+ throw error;
30
+ } finally {
31
+ clearTimeout(timeout);
32
+ }
33
+ };
34
+
7
35
  export type AssistantCloudAuthStrategy = {
8
36
  readonly strategy: "anon" | "jwt" | "api-key";
9
37
  getAuthHeaders(): Promise<Record<string, string> | false>;
@@ -228,6 +256,19 @@ const readRefreshToken = (baseUrl: string): RefreshToken | undefined => {
228
256
  }
229
257
  };
230
258
 
259
+ export const normalizeBaseUrl = (baseUrl: string): string => {
260
+ if (!baseUrl || !baseUrl.endsWith("/")) return baseUrl;
261
+ return baseUrl.slice(0, -1);
262
+ };
263
+
264
+ /** The refresh token of the anonymous identity this browser holds for `baseUrl`, or null when it has none. */
265
+ export const readAnonymousRefreshToken = (baseUrl: string): string | null => {
266
+ const refreshToken = readRefreshToken(normalizeBaseUrl(baseUrl));
267
+ if (!refreshToken) return null;
268
+ const refreshExpiry = new Date(refreshToken.expires_at).getTime();
269
+ return refreshExpiry - Date.now() > 30 * 1000 ? refreshToken.token : null;
270
+ };
271
+
231
272
  const writeRefreshToken = (
232
273
  baseUrl: string,
233
274
  refreshToken: RefreshToken,
@@ -247,6 +288,52 @@ const removeRefreshToken = (baseUrl: string): void => {
247
288
  } catch {}
248
289
  };
249
290
 
291
+ // In-flight sharing follows refresh-token storage scope to isolate server requests.
292
+ const anonymousAuthTokenRequests = new WeakMap<
293
+ Storage,
294
+ Map<string, Promise<string | null>>
295
+ >();
296
+
297
+ const getWebLockManager = (): LockManager | null => {
298
+ if (!("navigator" in globalThis)) return null;
299
+ return (
300
+ (globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks ??
301
+ null
302
+ );
303
+ };
304
+
305
+ const getAnonymousAuthLockName = (baseUrl: string): string =>
306
+ `assistant-cloud:anonymous-auth:${baseUrl}`;
307
+
308
+ const getSharedAnonymousAuthToken = (
309
+ baseUrl: string,
310
+ requestToken: () => Promise<string | null>,
311
+ ): Promise<string | null> => {
312
+ const storage = getLocalStorage();
313
+ if (!storage) return requestToken();
314
+
315
+ let storageRequests = anonymousAuthTokenRequests.get(storage);
316
+ if (!storageRequests) {
317
+ storageRequests = new Map();
318
+ anonymousAuthTokenRequests.set(storage, storageRequests);
319
+ }
320
+
321
+ const activeRequest = storageRequests.get(baseUrl);
322
+ if (activeRequest) return activeRequest;
323
+
324
+ const locks = getWebLockManager();
325
+ const request = locks
326
+ ? locks.request(getAnonymousAuthLockName(baseUrl), requestToken)
327
+ : requestToken();
328
+ const sharedRequest = request.finally(() => {
329
+ if (storageRequests.get(baseUrl) === sharedRequest) {
330
+ storageRequests.delete(baseUrl);
331
+ }
332
+ });
333
+ storageRequests.set(baseUrl, sharedRequest);
334
+ return sharedRequest;
335
+ };
336
+
250
337
  export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthStrategy {
251
338
  public readonly strategy = "anon";
252
339
 
@@ -255,70 +342,90 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
255
342
 
256
343
  constructor(baseUrl: string) {
257
344
  this.baseUrl = baseUrl;
258
- this.jwtStrategy = new AssistantCloudJWTAuthStrategy(async () => {
345
+ const requestAuthToken = async (): Promise<string | null> => {
259
346
  const currentTime = Date.now();
260
347
  const storedRefreshToken = readRefreshToken(this.baseUrl);
261
348
 
262
349
  if (storedRefreshToken) {
263
350
  const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();
264
351
  if (refreshExpiry - currentTime > 30 * 1000) {
265
- const response = await fetch(
266
- `${this.baseUrl}/v1/auth/tokens/refresh`,
267
- {
268
- method: "POST",
269
- headers: { "Content-Type": "application/json" },
270
- body: JSON.stringify({ refresh_token: storedRefreshToken.token }),
352
+ const refreshedAccessToken = await withAuthTokenDeadline(
353
+ "refresh token request",
354
+ async (signal) => {
355
+ const response = await fetch(
356
+ `${this.baseUrl}/v1/auth/tokens/refresh`,
357
+ {
358
+ method: "POST",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({
361
+ refresh_token: storedRefreshToken.token,
362
+ }),
363
+ signal,
364
+ },
365
+ );
366
+
367
+ if (response.ok) {
368
+ const { data, accessToken } = await readAuthTokenResponse(
369
+ response,
370
+ "refresh auth token response",
371
+ );
372
+ if (data.refresh_token != null) {
373
+ writeRefreshToken(
374
+ this.baseUrl,
375
+ readRefreshTokenResponse(
376
+ data.refresh_token,
377
+ "refresh auth token response.refresh_token",
378
+ ),
379
+ );
380
+ }
381
+ return accessToken;
382
+ }
383
+
384
+ if (response.status === 429 || response.status >= 500) {
385
+ throw new Error(
386
+ `Assistant Cloud token refresh failed with status ${response.status}`,
387
+ );
388
+ }
389
+
390
+ return null;
271
391
  },
272
392
  );
273
-
274
- if (response.ok) {
275
- const { data, accessToken } = await readAuthTokenResponse(
276
- response,
277
- "refresh auth token response",
278
- );
279
- if (data.refresh_token != null) {
280
- writeRefreshToken(
281
- this.baseUrl,
282
- readRefreshTokenResponse(
283
- data.refresh_token,
284
- "refresh auth token response.refresh_token",
285
- ),
286
- );
287
- }
288
- return accessToken;
289
- }
290
-
291
- if (response.status === 429 || response.status >= 500) {
292
- throw new Error(
293
- `Assistant Cloud token refresh failed with status ${response.status}`,
294
- );
295
- }
393
+ if (refreshedAccessToken !== null) return refreshedAccessToken;
296
394
  } else {
297
395
  removeRefreshToken(this.baseUrl);
298
396
  }
299
397
  }
300
398
 
301
399
  // No valid refresh token; request a new anonymous token
302
- const response = await fetch(`${this.baseUrl}/v1/auth/tokens/anonymous`, {
303
- method: "POST",
304
- });
400
+ return withAuthTokenDeadline(
401
+ "anonymous token request",
402
+ async (signal) => {
403
+ const response = await fetch(
404
+ `${this.baseUrl}/v1/auth/tokens/anonymous`,
405
+ { method: "POST", signal },
406
+ );
305
407
 
306
- if (!response.ok) return null;
408
+ if (!response.ok) return null;
307
409
 
308
- const { data, accessToken } = await readAuthTokenResponse(
309
- response,
310
- "anonymous auth token response",
311
- );
410
+ const { data, accessToken } = await readAuthTokenResponse(
411
+ response,
412
+ "anonymous auth token response",
413
+ );
312
414
 
313
- writeRefreshToken(
314
- this.baseUrl,
315
- readRefreshTokenResponse(
316
- data.refresh_token,
317
- "anonymous auth token response.refresh_token",
318
- ),
415
+ writeRefreshToken(
416
+ this.baseUrl,
417
+ readRefreshTokenResponse(
418
+ data.refresh_token,
419
+ "anonymous auth token response.refresh_token",
420
+ ),
421
+ );
422
+ return accessToken;
423
+ },
319
424
  );
320
- return accessToken;
321
- });
425
+ };
426
+ this.jwtStrategy = new AssistantCloudJWTAuthStrategy(() =>
427
+ getSharedAnonymousAuthToken(this.baseUrl, requestAuthToken),
428
+ );
322
429
  }
323
430
 
324
431
  public async getAuthHeaders(): Promise<Record<string, string> | false> {