assistant-cloud 0.1.41 → 0.1.42
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.
- package/README.md +3 -3
- package/dist/AssistantCloud.js +2 -2
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +5 -5
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +3 -13
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/generateThreadTitle.d.ts +15 -0
- package/dist/generateThreadTitle.d.ts.map +1 -0
- package/dist/generateThreadTitle.js +25 -0
- package/dist/generateThreadTitle.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/runTelemetry.d.ts +54 -0
- package/dist/runTelemetry.d.ts.map +1 -0
- package/dist/runTelemetry.js +79 -0
- package/dist/runTelemetry.js.map +1 -0
- package/package.json +4 -4
- package/src/AssistantCloud.ts +1 -1
- package/src/AssistantCloudAPI.ts +9 -8
- package/src/AssistantCloudRuns.ts +3 -14
- package/src/generateThreadTitle.test.ts +71 -0
- package/src/generateThreadTitle.ts +38 -0
- package/src/index.ts +10 -0
- package/src/runTelemetry.test.ts +130 -0
- package/src/runTelemetry.ts +140 -0
- package/src/tests/AssistantCloud.test.ts +39 -0
- package/src/tests/AssistantCloudAPI.test.ts +25 -0
package/README.md
CHANGED
|
@@ -9,16 +9,16 @@ Server- and client-side SDK for [Assistant Cloud](https://cloud.assistant-ui.com
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install @assistant-ui/react @assistant-ui/
|
|
12
|
+
npm install @assistant-ui/react @assistant-ui/ai-sdk assistant-cloud
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
Pass an `AssistantCloud` instance to your runtime hook (typically `useChatRuntime` from `@assistant-ui/
|
|
17
|
+
Pass an `AssistantCloud` instance to your runtime hook (typically `useChatRuntime` from `@assistant-ui/ai-sdk`):
|
|
18
18
|
|
|
19
19
|
```tsx
|
|
20
20
|
import { AssistantCloud, AssistantRuntimeProvider } from "@assistant-ui/react";
|
|
21
|
-
import { useChatRuntime } from "@assistant-ui/
|
|
21
|
+
import { useChatRuntime } from "@assistant-ui/ai-sdk";
|
|
22
22
|
|
|
23
23
|
const cloud = new AssistantCloud({
|
|
24
24
|
baseUrl: process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL!,
|
package/dist/AssistantCloud.js
CHANGED
|
@@ -21,8 +21,8 @@ var AssistantCloud = class {
|
|
|
21
21
|
this.files = new AssistantCloudFiles(api);
|
|
22
22
|
const t = config.telemetry;
|
|
23
23
|
this.telemetry = t === false ? { enabled: false } : t === true || t === void 0 ? { enabled: true } : {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
...t,
|
|
25
|
+
enabled: t.enabled !== false
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloud.js","names":[],"sources":["../src/AssistantCloud.ts"],"sourcesContent":["import {\n AssistantCloudAPI,\n type AssistantCloudConfig,\n type AssistantCloudTelemetryConfig,\n} from \"./AssistantCloudAPI\";\nimport { AssistantCloudAuthTokens } from \"./AssistantCloudAuthTokens\";\nimport { AssistantCloudProjects } from \"./AssistantCloudProjects\";\nimport { AssistantCloudRuns } from \"./AssistantCloudRuns\";\nimport { AssistantCloudThreads } from \"./AssistantCloudThreads\";\nimport { AssistantCloudFiles } from \"./AssistantCloudFiles\";\n\nexport class AssistantCloud {\n public readonly threads;\n public readonly projects;\n public readonly auth;\n public readonly runs;\n public readonly files;\n public readonly telemetry: AssistantCloudTelemetryConfig;\n\n constructor(config: AssistantCloudConfig) {\n const api = new AssistantCloudAPI(config);\n this.threads = new AssistantCloudThreads(api);\n this.projects = new AssistantCloudProjects(api);\n this.auth = {\n tokens: new AssistantCloudAuthTokens(api),\n };\n this.runs = new AssistantCloudRuns(api);\n this.files = new AssistantCloudFiles(api);\n\n const t = config.telemetry;\n this.telemetry =\n t === false\n ? { enabled: false }\n : t === true || t === undefined\n ? { enabled: true }\n : { enabled: t.enabled !== false
|
|
1
|
+
{"version":3,"file":"AssistantCloud.js","names":[],"sources":["../src/AssistantCloud.ts"],"sourcesContent":["import {\n AssistantCloudAPI,\n type AssistantCloudConfig,\n type AssistantCloudTelemetryConfig,\n} from \"./AssistantCloudAPI\";\nimport { AssistantCloudAuthTokens } from \"./AssistantCloudAuthTokens\";\nimport { AssistantCloudProjects } from \"./AssistantCloudProjects\";\nimport { AssistantCloudRuns } from \"./AssistantCloudRuns\";\nimport { AssistantCloudThreads } from \"./AssistantCloudThreads\";\nimport { AssistantCloudFiles } from \"./AssistantCloudFiles\";\n\nexport class AssistantCloud {\n public readonly threads;\n public readonly projects;\n public readonly auth;\n public readonly runs;\n public readonly files;\n public readonly telemetry: AssistantCloudTelemetryConfig;\n\n constructor(config: AssistantCloudConfig) {\n const api = new AssistantCloudAPI(config);\n this.threads = new AssistantCloudThreads(api);\n this.projects = new AssistantCloudProjects(api);\n this.auth = {\n tokens: new AssistantCloudAuthTokens(api),\n };\n this.runs = new AssistantCloudRuns(api);\n this.files = new AssistantCloudFiles(api);\n\n const t = config.telemetry;\n this.telemetry =\n t === false\n ? { enabled: false }\n : t === true || t === undefined\n ? { enabled: true }\n : { ...t, enabled: t.enabled !== false };\n }\n}\n"],"mappings":";;;;;;;AAWA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAA8B;EACxC,MAAM,MAAM,IAAI,kBAAkB,MAAM;EACxC,KAAK,UAAU,IAAI,sBAAsB,GAAG;EAC5C,KAAK,WAAW,IAAI,uBAAuB,GAAG;EAC9C,KAAK,OAAO,EACV,QAAQ,IAAI,yBAAyB,GAAG,EAC1C;EACA,KAAK,OAAO,IAAI,mBAAmB,GAAG;EACtC,KAAK,QAAQ,IAAI,oBAAoB,GAAG;EAExC,MAAM,IAAI,OAAO;EACjB,KAAK,YACH,MAAM,QACF,EAAE,SAAS,MAAM,IACjB,MAAM,QAAQ,MAAM,KAAA,IAClB,EAAE,SAAS,KAAK,IAChB;GAAE,GAAG;GAAG,SAAS,EAAE,YAAY;EAAM;CAC/C;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloudAPI.d.ts","names":[],"sources":["../src/AssistantCloudAPI.ts"],"mappings":";;;KAQY;EACV;;;;;;EAMA,gBACE,QAAQ,4BACL;;KAGK;EAEN;EACA,iBAAiB;;EAGjB;EACA;EACA;EACA;;EAGA;EACA;;;;;;;;;;;;;EAcJ,sBAAsB;;cAGX,sBAAsB;WACjB;EAEJ,YAAA,iBAAiB;;KAO1B;EACH;EACA,UAAU;EACV,QAAQ;EACR;;cAQW;EACJ,OAAO;EACP;EAEK,YAAA,QAAQ;EAuBP,kBAAc;EAId,eACX,kBACA,UAAS,qBAAuB,QAAA;
|
|
1
|
+
{"version":3,"file":"AssistantCloudAPI.d.ts","names":[],"sources":["../src/AssistantCloudAPI.ts"],"mappings":";;;KAQY;EACV;;;;;;EAMA,gBACE,QAAQ,4BACL;;KAGK;EAEN;EACA,iBAAiB;;EAGjB;EACA;EACA;EACA;;EAGA;EACA;;;;;;;;;;;;;EAcJ,sBAAsB;;cAGX,sBAAsB;WACjB;EAEJ,YAAA,iBAAiB;;KAO1B;EACH;EACA,UAAU;EACV,QAAQ;EACR;;cAQW;EACJ,OAAO;EACP;EAEK,YAAA,QAAQ;EAuBP,kBAAc;EAId,eACX,kBACA,UAAS,qBAAuB,QAAA;EAoDrB,YAAY,kBAAkB,UAAS,qBAAuB"}
|
|
@@ -54,12 +54,12 @@ var AssistantCloudAPI = class {
|
|
|
54
54
|
this._auth.readAuthHeaders(response.headers);
|
|
55
55
|
if (!response.ok) {
|
|
56
56
|
const text = await response.text();
|
|
57
|
+
let message;
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
59
|
+
const body = JSON.parse(text);
|
|
60
|
+
if (typeof body?.message === "string" && body.message.length > 0) message = body.message;
|
|
61
|
+
} catch {}
|
|
62
|
+
throw new CloudAPIError(message ?? `Request failed with status ${response.status}, ${text}`, response.status);
|
|
63
63
|
}
|
|
64
64
|
return response;
|
|
65
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloudAPI.js","names":[],"sources":["../src/AssistantCloudAPI.ts"],"sourcesContent":["import {\n type AssistantCloudAuthStrategy,\n AssistantCloudJWTAuthStrategy,\n AssistantCloudAPIKeyAuthStrategy,\n AssistantCloudAnonymousAuthStrategy,\n} from \"./AssistantCloudAuthStrategy\";\nimport type { AssistantCloudRunReport } from \"./AssistantCloudRuns\";\n\nexport type AssistantCloudTelemetryConfig = {\n enabled?: boolean;\n /**\n * Called before each telemetry report is sent.\n * Return a modified report to enrich it (e.g. add `model_id`),\n * or return `null` to skip the report.\n */\n beforeReport?: (\n report: AssistantCloudRunReport,\n ) => AssistantCloudRunReport | null;\n};\n\nexport type AssistantCloudConfig = (\n | {\n baseUrl: string;\n authToken: () => Promise<string | null>;\n }\n | {\n baseUrl?: string;\n apiKey: string;\n userId: string;\n workspaceId: string;\n }\n | {\n baseUrl: string;\n anonymous: true;\n }\n) & {\n /**\n * Client-side run telemetry reporting. Default: `true`.\n *\n * When enabled, the SDK automatically reports run metadata (status, step\n * count, tool calls, and token usage) to Assistant Cloud after each\n * assistant message is saved. No message content is sent.\n *\n * - `true` / `undefined` — enabled with defaults\n * - `false` — disabled\n * - `{ beforeReport }` — enabled with a hook to enrich or filter reports\n */\n telemetry?: boolean | AssistantCloudTelemetryConfig;\n};\n\nexport class CloudAPIError extends Error {\n public readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n this.name = \"CloudAPIError\";\n }\n}\n\ntype MakeRequestOptions = {\n method?: \"POST\" | \"PUT\" | \"DELETE\" | undefined;\n headers?: Record<string, string> | undefined;\n query?: Record<string, string | number | boolean> | undefined;\n body?: object | undefined;\n};\n\nconst normalizeBaseUrl = (baseUrl: string) => {\n if (!baseUrl || !baseUrl.endsWith(\"/\")) return baseUrl;\n return baseUrl.slice(0, -1);\n};\n\nexport class AssistantCloudAPI {\n public _auth: AssistantCloudAuthStrategy;\n public _baseUrl;\n\n constructor(config: AssistantCloudConfig) {\n if (\"authToken\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudJWTAuthStrategy(config.authToken);\n } else if (\"apiKey\" in config) {\n this._baseUrl = normalizeBaseUrl(\n config.baseUrl ?? \"https://backend.assistant-api.com\",\n );\n this._auth = new AssistantCloudAPIKeyAuthStrategy(\n config.apiKey,\n config.userId,\n config.workspaceId,\n );\n } else if (\"anonymous\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudAnonymousAuthStrategy(this._baseUrl);\n } else {\n throw new Error(\n \"Invalid configuration: Must provide authToken, apiKey, or anonymous configuration\",\n );\n }\n }\n\n public async initializeAuth() {\n return !!(await this._auth.getAuthHeaders());\n }\n\n public async makeRawRequest(\n endpoint: string,\n options: MakeRequestOptions = {},\n ) {\n const authHeaders = await this._auth.getAuthHeaders();\n if (!authHeaders) throw new Error(\"Authorization failed\");\n\n const headers = {\n ...authHeaders,\n ...options.headers,\n \"Content-Type\": \"application/json\",\n };\n\n const queryParams = new URLSearchParams();\n if (options.query) {\n for (const [key, value] of Object.entries(options.query)) {\n if (value === false) continue;\n if (value === true) {\n queryParams.set(key, \"true\");\n } else {\n queryParams.set(key, value.toString());\n }\n }\n }\n\n const url = new URL(`${this._baseUrl}/v1${endpoint}`);\n url.search = queryParams.toString();\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body ? JSON.stringify(options.body) : null,\n });\n\n this._auth.readAuthHeaders(response.headers);\n\n if (!response.ok) {\n const text = await response.text();\n try {\n const body = JSON.parse(text);\n
|
|
1
|
+
{"version":3,"file":"AssistantCloudAPI.js","names":[],"sources":["../src/AssistantCloudAPI.ts"],"sourcesContent":["import {\n type AssistantCloudAuthStrategy,\n AssistantCloudJWTAuthStrategy,\n AssistantCloudAPIKeyAuthStrategy,\n AssistantCloudAnonymousAuthStrategy,\n} from \"./AssistantCloudAuthStrategy\";\nimport type { AssistantCloudRunReport } from \"./AssistantCloudRuns\";\n\nexport type AssistantCloudTelemetryConfig = {\n enabled?: boolean;\n /**\n * Called before each telemetry report is sent.\n * Return a modified report to enrich it (e.g. add `model_id`),\n * or return `null` to skip the report.\n */\n beforeReport?: (\n report: AssistantCloudRunReport,\n ) => AssistantCloudRunReport | null;\n};\n\nexport type AssistantCloudConfig = (\n | {\n baseUrl: string;\n authToken: () => Promise<string | null>;\n }\n | {\n baseUrl?: string;\n apiKey: string;\n userId: string;\n workspaceId: string;\n }\n | {\n baseUrl: string;\n anonymous: true;\n }\n) & {\n /**\n * Client-side run telemetry reporting. Default: `true`.\n *\n * When enabled, the SDK automatically reports run metadata (status, step\n * count, tool calls, and token usage) to Assistant Cloud after each\n * assistant message is saved. No message content is sent.\n *\n * - `true` / `undefined` — enabled with defaults\n * - `false` — disabled\n * - `{ beforeReport }` — enabled with a hook to enrich or filter reports\n */\n telemetry?: boolean | AssistantCloudTelemetryConfig;\n};\n\nexport class CloudAPIError extends Error {\n public readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n this.name = \"CloudAPIError\";\n }\n}\n\ntype MakeRequestOptions = {\n method?: \"POST\" | \"PUT\" | \"DELETE\" | undefined;\n headers?: Record<string, string> | undefined;\n query?: Record<string, string | number | boolean> | undefined;\n body?: object | undefined;\n};\n\nconst normalizeBaseUrl = (baseUrl: string) => {\n if (!baseUrl || !baseUrl.endsWith(\"/\")) return baseUrl;\n return baseUrl.slice(0, -1);\n};\n\nexport class AssistantCloudAPI {\n public _auth: AssistantCloudAuthStrategy;\n public _baseUrl;\n\n constructor(config: AssistantCloudConfig) {\n if (\"authToken\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudJWTAuthStrategy(config.authToken);\n } else if (\"apiKey\" in config) {\n this._baseUrl = normalizeBaseUrl(\n config.baseUrl ?? \"https://backend.assistant-api.com\",\n );\n this._auth = new AssistantCloudAPIKeyAuthStrategy(\n config.apiKey,\n config.userId,\n config.workspaceId,\n );\n } else if (\"anonymous\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudAnonymousAuthStrategy(this._baseUrl);\n } else {\n throw new Error(\n \"Invalid configuration: Must provide authToken, apiKey, or anonymous configuration\",\n );\n }\n }\n\n public async initializeAuth() {\n return !!(await this._auth.getAuthHeaders());\n }\n\n public async makeRawRequest(\n endpoint: string,\n options: MakeRequestOptions = {},\n ) {\n const authHeaders = await this._auth.getAuthHeaders();\n if (!authHeaders) throw new Error(\"Authorization failed\");\n\n const headers = {\n ...authHeaders,\n ...options.headers,\n \"Content-Type\": \"application/json\",\n };\n\n const queryParams = new URLSearchParams();\n if (options.query) {\n for (const [key, value] of Object.entries(options.query)) {\n if (value === false) continue;\n if (value === true) {\n queryParams.set(key, \"true\");\n } else {\n queryParams.set(key, value.toString());\n }\n }\n }\n\n const url = new URL(`${this._baseUrl}/v1${endpoint}`);\n url.search = queryParams.toString();\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body ? JSON.stringify(options.body) : null,\n });\n\n this._auth.readAuthHeaders(response.headers);\n\n if (!response.ok) {\n const text = await response.text();\n let message: string | undefined;\n try {\n const body = JSON.parse(text);\n if (typeof body?.message === \"string\" && body.message.length > 0) {\n message = body.message;\n }\n } catch {}\n throw new CloudAPIError(\n message ?? `Request failed with status ${response.status}, ${text}`,\n response.status,\n );\n }\n\n return response;\n }\n\n public async makeRequest(endpoint: string, options: MakeRequestOptions = {}) {\n const response = await this.makeRawRequest(endpoint, options);\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n )\n return undefined;\n\n const text = await response.text();\n if (text.trim() === \"\") return undefined;\n\n return JSON.parse(text);\n }\n}\n"],"mappings":";;AAkDA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CAEA,YAAY,SAAiB,QAAgB;EAC3C,MAAM,OAAO;EACb,KAAK,SAAS;EACd,KAAK,OAAO;CACd;AACF;AASA,MAAM,oBAAoB,YAAoB;CAC5C,IAAI,CAAC,WAAW,CAAC,QAAQ,SAAS,GAAG,GAAG,OAAO;CAC/C,OAAO,QAAQ,MAAM,GAAG,EAAE;AAC5B;AAEA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CAEA,YAAY,QAA8B;EACxC,IAAI,eAAe,QAAQ;GACzB,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,8BAA8B,OAAO,SAAS;EACjE,OAAO,IAAI,YAAY,QAAQ;GAC7B,KAAK,WAAW,iBACd,OAAO,WAAW,mCACpB;GACA,KAAK,QAAQ,IAAI,iCACf,OAAO,QACP,OAAO,QACP,OAAO,WACT;EACF,OAAO,IAAI,eAAe,QAAQ;GAChC,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,oCAAoC,KAAK,QAAQ;EACpE,OACE,MAAM,IAAI,MACR,mFACF;CAEJ;CAEA,MAAa,iBAAiB;EAC5B,OAAO,CAAC,CAAE,MAAM,KAAK,MAAM,eAAe;CAC5C;CAEA,MAAa,eACX,UACA,UAA8B,CAAC,GAC/B;EACA,MAAM,cAAc,MAAM,KAAK,MAAM,eAAe;EACpD,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,sBAAsB;EAExD,MAAM,UAAU;GACd,GAAG;GACH,GAAG,QAAQ;GACX,gBAAgB;EAClB;EAEA,MAAM,cAAc,IAAI,gBAAgB;EACxC,IAAI,QAAQ,OACV,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,KAAK,GAAG;GACxD,IAAI,UAAU,OAAO;GACrB,IAAI,UAAU,MACZ,YAAY,IAAI,KAAK,MAAM;QAE3B,YAAY,IAAI,KAAK,MAAM,SAAS,CAAC;EAEzC;EAGF,MAAM,MAAM,IAAI,IAAI,GAAG,KAAK,SAAS,KAAK,UAAU;EACpD,IAAI,SAAS,YAAY,SAAS;EAElC,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ,QAAQ,UAAU;GAC1B;GACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;EACtD,CAAC;EAED,KAAK,MAAM,gBAAgB,SAAS,OAAO;EAE3C,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,KAAK;GACjC,IAAI;GACJ,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,IAAI;IAC5B,IAAI,OAAO,MAAM,YAAY,YAAY,KAAK,QAAQ,SAAS,GAC7D,UAAU,KAAK;GAEnB,QAAQ,CAAC;GACT,MAAM,IAAI,cACR,WAAW,8BAA8B,SAAS,OAAO,IAAI,QAC7D,SAAS,MACX;EACF;EAEA,OAAO;CACT;CAEA,MAAa,YAAY,UAAkB,UAA8B,CAAC,GAAG;EAC3E,MAAM,WAAW,MAAM,KAAK,eAAe,UAAU,OAAO;EAC5D,IACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,gBAAgB,MAAM,KAE3C,OAAO,KAAA;EAET,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO,KAAA;EAE/B,OAAO,KAAK,MAAM,IAAI;CACxB;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AssistantCloudRunReportToolCall } from "./runTelemetry.js";
|
|
2
2
|
import { AssistantCloudAPI } from "./AssistantCloudAPI.js";
|
|
3
3
|
import { AssistantStream } from "assistant-stream";
|
|
4
4
|
//#region src/AssistantCloudRuns.d.ts
|
|
@@ -7,27 +7,17 @@ type AssistantCloudRunsStreamBody = {
|
|
|
7
7
|
assistant_id: "system/thread_title";
|
|
8
8
|
messages: readonly unknown[];
|
|
9
9
|
};
|
|
10
|
-
type ReportToolCall = {
|
|
11
|
-
tool_name: string;
|
|
12
|
-
tool_call_id: string;
|
|
13
|
-
tool_args?: string;
|
|
14
|
-
tool_result?: string;
|
|
15
|
-
tool_source?: "mcp" | "frontend" | "backend";
|
|
16
|
-
start_ms?: number;
|
|
17
|
-
end_ms?: number;
|
|
18
|
-
sampling_calls?: SamplingCallData[];
|
|
19
|
-
};
|
|
20
10
|
type AssistantCloudRunReport = {
|
|
21
11
|
thread_id: string;
|
|
22
12
|
status: "completed" | "incomplete" | "error";
|
|
23
13
|
total_steps?: number;
|
|
24
|
-
tool_calls?:
|
|
14
|
+
tool_calls?: AssistantCloudRunReportToolCall[];
|
|
25
15
|
steps?: {
|
|
26
16
|
input_tokens?: number;
|
|
27
17
|
output_tokens?: number;
|
|
28
18
|
reasoning_tokens?: number;
|
|
29
19
|
cached_input_tokens?: number;
|
|
30
|
-
tool_calls?:
|
|
20
|
+
tool_calls?: AssistantCloudRunReportToolCall[];
|
|
31
21
|
start_ms?: number;
|
|
32
22
|
end_ms?: number;
|
|
33
23
|
}[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloudRuns.d.ts","names":[],"sources":["../src/AssistantCloudRuns.ts"],"mappings":";;;;KASK;EACH;EACA;EACA;;
|
|
1
|
+
{"version":3,"file":"AssistantCloudRuns.d.ts","names":[],"sources":["../src/AssistantCloudRuns.ts"],"mappings":";;;;KASK;EACH;EACA;EACA;;KAMU;EACV;EACA;EACA;EACA,aAAa;EACb;IACE;IACA;IACA;IACA;IACA,aAAa;IACb;IACA;;EAEF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;;cAGA;UACH;EAEI,YAAA,OAAO;EAIZ,+BAA+B;;;;;;;;;;;EAmBzB,OACX,MAAM,+BACL,QAAQ;EAkCE,OACX,MAAM,0BACL;IAAU"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloudRuns.js","names":[],"sources":["../src/AssistantCloudRuns.ts"],"sourcesContent":["import type { AssistantCloudAPI } from \"./AssistantCloudAPI\";\nimport type {
|
|
1
|
+
{"version":3,"file":"AssistantCloudRuns.js","names":[],"sources":["../src/AssistantCloudRuns.ts"],"sourcesContent":["import type { AssistantCloudAPI } from \"./AssistantCloudAPI\";\nimport type { AssistantCloudRunReportToolCall } from \"./runTelemetry\";\nimport { AssistantStream, PlainTextDecoder } from \"assistant-stream\";\nimport {\n CloudResponseError,\n readCloudRecord,\n readCloudString,\n} from \"./cloudResponse\";\n\ntype AssistantCloudRunsStreamBody = {\n thread_id: string;\n assistant_id: \"system/thread_title\";\n messages: readonly unknown[]; // TODO type\n};\n\n// NOTE: Keep this payload shape aligned with the strict runtime validator in\n// assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts\n// (createRunSchema). New telemetry fields must be added in both repos together.\nexport type AssistantCloudRunReport = {\n thread_id: string;\n status: \"completed\" | \"incomplete\" | \"error\";\n total_steps?: number;\n tool_calls?: AssistantCloudRunReportToolCall[];\n steps?: {\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n tool_calls?: AssistantCloudRunReportToolCall[];\n start_ms?: number;\n end_ms?: number;\n }[];\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n model_id?: string;\n provider_type?: string;\n duration_ms?: number;\n output_text?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport class AssistantCloudRuns {\n private cloud: AssistantCloudAPI;\n\n constructor(cloud: AssistantCloudAPI) {\n this.cloud = cloud;\n }\n\n public __internal_getAssistantOptions(assistantId: string) {\n return {\n api: `${this.cloud._baseUrl}/v1/runs/stream`,\n headers: async () => {\n const headers = await this.cloud._auth.getAuthHeaders();\n if (!headers) throw new Error(\"Authorization failed\");\n return {\n ...headers,\n Accept: \"text/plain\",\n };\n },\n body: {\n assistant_id: assistantId,\n response_format: \"vercel-ai-data-stream/v1\",\n thread_id: \"unstable_todo\",\n },\n };\n }\n\n public async stream(\n body: AssistantCloudRunsStreamBody,\n ): Promise<AssistantStream> {\n const response = await this.cloud.makeRawRequest(\"/runs/stream\", {\n method: \"POST\",\n headers: {\n Accept: \"text/plain\",\n },\n body,\n });\n\n if (!response.body) {\n throw new CloudResponseError(\n 'Invalid Assistant Cloud response for \"run stream\": expected a response body',\n );\n }\n\n const receivedContentType = response.headers.get(\"content-type\");\n const contentType = receivedContentType\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"text/plain\") {\n await response.body.cancel().catch(() => undefined);\n throw new CloudResponseError(\n `Invalid Assistant Cloud response for \"run stream\": expected a \"text/plain\" content type, received ${\n receivedContentType\n ? `\"${receivedContentType}\"`\n : \"no Content-Type header\"\n }`,\n );\n }\n\n return AssistantStream.fromResponse(response, new PlainTextDecoder());\n }\n\n public async report(\n body: AssistantCloudRunReport,\n ): Promise<{ run_id: string }> {\n const response = readCloudRecord(\n await this.cloud.makeRequest(\"/runs\", { method: \"POST\", body }),\n \"run report response\",\n );\n\n return { run_id: readCloudString(response.run_id, \"run_id\") };\n }\n}\n"],"mappings":";;;AA2CA,IAAa,qBAAb,MAAgC;CAC9B;CAEA,YAAY,OAA0B;EACpC,KAAK,QAAQ;CACf;CAEA,+BAAsC,aAAqB;EACzD,OAAO;GACL,KAAK,GAAG,KAAK,MAAM,SAAS;GAC5B,SAAS,YAAY;IACnB,MAAM,UAAU,MAAM,KAAK,MAAM,MAAM,eAAe;IACtD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,sBAAsB;IACpD,OAAO;KACL,GAAG;KACH,QAAQ;IACV;GACF;GACA,MAAM;IACJ,cAAc;IACd,iBAAiB;IACjB,WAAW;GACb;EACF;CACF;CAEA,MAAa,OACX,MAC0B;EAC1B,MAAM,WAAW,MAAM,KAAK,MAAM,eAAe,gBAAgB;GAC/D,QAAQ;GACR,SAAS,EACP,QAAQ,aACV;GACA;EACF,CAAC;EAED,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,mBACR,+EACF;EAGF,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAK/D,IAJoB,qBAChB,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAChB,KAAK,CAAC,CACP,YAAY,MACK,cAAc;GAChC,MAAM,SAAS,KAAK,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;GAClD,MAAM,IAAI,mBACR,qGACE,sBACI,IAAI,oBAAoB,KACxB,0BAER;EACF;EAEA,OAAO,gBAAgB,aAAa,UAAU,IAAI,iBAAiB,CAAC;CACtE;CAEA,MAAa,OACX,MAC6B;EAC7B,MAAM,WAAW,gBACf,MAAM,KAAK,MAAM,YAAY,SAAS;GAAE,QAAQ;GAAQ;EAAK,CAAC,GAC9D,qBACF;EAEA,OAAO,EAAE,QAAQ,gBAAgB,SAAS,QAAQ,QAAQ,EAAE;CAC9D;AACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AssistantCloud } from "./AssistantCloud.js";
|
|
2
|
+
//#region src/generateThreadTitle.d.ts
|
|
3
|
+
declare function generateThreadTitle(cloud: AssistantCloud, options: {
|
|
4
|
+
threadId: string;
|
|
5
|
+
messages: readonly {
|
|
6
|
+
role: string;
|
|
7
|
+
content: readonly {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}[];
|
|
11
|
+
}[];
|
|
12
|
+
}): Promise<string | null>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { generateThreadTitle };
|
|
15
|
+
//# sourceMappingURL=generateThreadTitle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateThreadTitle.d.ts","names":[],"sources":["../src/generateThreadTitle.ts"],"mappings":";;iBAEsB,oBACpB,OAAO,gBACP;EACE;EACA;IACE;IACA;MAAoB;MAAc;;;IAGrC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/generateThreadTitle.ts
|
|
2
|
+
async function generateThreadTitle(cloud, options) {
|
|
3
|
+
const stream = await cloud.runs.stream({
|
|
4
|
+
thread_id: options.threadId,
|
|
5
|
+
assistant_id: "system/thread_title",
|
|
6
|
+
messages: options.messages
|
|
7
|
+
});
|
|
8
|
+
let title = "";
|
|
9
|
+
const reader = stream.getReader();
|
|
10
|
+
try {
|
|
11
|
+
while (true) {
|
|
12
|
+
const { done, value: chunk } = await reader.read();
|
|
13
|
+
if (done) break;
|
|
14
|
+
if (chunk.type === "text-delta") title += chunk.textDelta;
|
|
15
|
+
}
|
|
16
|
+
} finally {
|
|
17
|
+
reader.releaseLock();
|
|
18
|
+
}
|
|
19
|
+
if (title) await cloud.threads.update(options.threadId, { title });
|
|
20
|
+
return title || null;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { generateThreadTitle };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=generateThreadTitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateThreadTitle.js","names":[],"sources":["../src/generateThreadTitle.ts"],"sourcesContent":["import type { AssistantCloud } from \"./AssistantCloud\";\n\nexport async function generateThreadTitle(\n cloud: AssistantCloud,\n options: {\n threadId: string;\n messages: readonly {\n role: string;\n content: readonly { type: \"text\"; text: string }[];\n }[];\n },\n): Promise<string | null> {\n const stream = await cloud.runs.stream({\n thread_id: options.threadId,\n assistant_id: \"system/thread_title\",\n messages: options.messages,\n });\n\n let title = \"\";\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value: chunk } = await reader.read();\n if (done) break;\n if (chunk.type === \"text-delta\") {\n title += chunk.textDelta;\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n if (title) {\n await cloud.threads.update(options.threadId, { title });\n }\n\n return title || null;\n}\n"],"mappings":";AAEA,eAAsB,oBACpB,OACA,SAOwB;CACxB,MAAM,SAAS,MAAM,MAAM,KAAK,OAAO;EACrC,WAAW,QAAQ;EACnB,cAAc;EACd,UAAU,QAAQ;CACpB,CAAC;CAED,IAAI,QAAQ;CACZ,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI;EACF,OAAO,MAAM;GACX,MAAM,EAAE,MAAM,OAAO,UAAU,MAAM,OAAO,KAAK;GACjD,IAAI,MAAM;GACV,IAAI,MAAM,SAAS,cACjB,SAAS,MAAM;EAEnB;CACF,UAAU;EACR,OAAO,YAAY;CACrB;CAEA,IAAI,OACF,MAAM,MAAM,QAAQ,OAAO,QAAQ,UAAU,EAAE,MAAM,CAAC;CAGxD,OAAO,SAAS;AAClB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpSamplingHandler, SamplingCallData, createSamplingCollector, wrapSamplingHandler } from "./instrumentMcpSampling.js";
|
|
2
|
+
import { AssistantCloudRunReportToolCall, RunTelemetryToolCallInit, RunTelemetryUsage, RunTelemetryUsageInit, createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText } from "./runTelemetry.js";
|
|
2
3
|
import { AssistantCloudRunReport } from "./AssistantCloudRuns.js";
|
|
3
4
|
import { AssistantCloudTelemetryConfig, CloudAPIError } from "./AssistantCloudAPI.js";
|
|
4
5
|
import { CloudMessage } from "./AssistantCloudThreadMessages.js";
|
|
@@ -6,4 +7,5 @@ import { AssistantCloud } from "./AssistantCloud.js";
|
|
|
6
7
|
import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
|
|
7
8
|
import { MessageFormatAdapter, createFormattedPersistence } from "./FormattedCloudPersistence.js";
|
|
8
9
|
import { CloudResponseError } from "./cloudResponse.js";
|
|
9
|
-
|
|
10
|
+
import { generateThreadTitle } from "./generateThreadTitle.js";
|
|
11
|
+
export { AssistantCloud, type AssistantCloudRunReport, type AssistantCloudRunReportToolCall, type AssistantCloudTelemetryConfig, CloudAPIError, type CloudMessage, CloudMessagePersistence, CloudResponseError, type McpSamplingHandler, type MessageFormatAdapter, type RunTelemetryToolCallInit, type RunTelemetryUsage, type RunTelemetryUsageInit, type SamplingCallData, createFormattedPersistence, createRunTelemetryToolCall, createSamplingCollector, generateThreadTitle, normalizeRunTelemetryUsage, truncateRunTelemetryText, wrapSamplingHandler };
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,7 @@ import { CloudAPIError } from "./AssistantCloudAPI.js";
|
|
|
3
3
|
import { AssistantCloud } from "./AssistantCloud.js";
|
|
4
4
|
import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
|
|
5
5
|
import { createFormattedPersistence } from "./FormattedCloudPersistence.js";
|
|
6
|
+
import { generateThreadTitle } from "./generateThreadTitle.js";
|
|
7
|
+
import { createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText } from "./runTelemetry.js";
|
|
6
8
|
import { createSamplingCollector, wrapSamplingHandler } from "./instrumentMcpSampling.js";
|
|
7
|
-
export { AssistantCloud, CloudAPIError, CloudMessagePersistence, CloudResponseError, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
|
|
9
|
+
export { AssistantCloud, CloudAPIError, CloudMessagePersistence, CloudResponseError, createFormattedPersistence, createRunTelemetryToolCall, createSamplingCollector, generateThreadTitle, normalizeRunTelemetryUsage, truncateRunTelemetryText, wrapSamplingHandler };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SamplingCallData } from "./instrumentMcpSampling.js";
|
|
2
|
+
//#region src/runTelemetry.d.ts
|
|
3
|
+
type AssistantCloudRunReportToolCall = {
|
|
4
|
+
tool_name: string;
|
|
5
|
+
tool_call_id: string;
|
|
6
|
+
tool_args?: string;
|
|
7
|
+
tool_result?: string;
|
|
8
|
+
tool_source?: "mcp" | "frontend" | "backend";
|
|
9
|
+
start_ms?: number;
|
|
10
|
+
end_ms?: number;
|
|
11
|
+
sampling_calls?: SamplingCallData[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Clamps a string to the size the runs endpoint accepts for a single span
|
|
15
|
+
* field.
|
|
16
|
+
*/
|
|
17
|
+
declare function truncateRunTelemetryText(value: string): string;
|
|
18
|
+
type RunTelemetryToolCallInit = {
|
|
19
|
+
toolName: string;
|
|
20
|
+
toolCallId: string;
|
|
21
|
+
args?: unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Pre-serialized arguments, used in place of serializing `args`. Values over
|
|
24
|
+
* the span size are clamped before they are included in the report.
|
|
25
|
+
*/
|
|
26
|
+
argsText?: string | undefined;
|
|
27
|
+
result?: unknown;
|
|
28
|
+
toolSource?: "mcp" | "frontend" | "backend" | undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
|
|
32
|
+
* source has its result summarized, because MCP content blocks carry inline
|
|
33
|
+
* base64 image and audio payloads that would otherwise dominate the report.
|
|
34
|
+
*/
|
|
35
|
+
declare function createRunTelemetryToolCall(init: RunTelemetryToolCallInit): AssistantCloudRunReportToolCall;
|
|
36
|
+
type RunTelemetryUsage = {
|
|
37
|
+
inputTokens?: number;
|
|
38
|
+
outputTokens?: number;
|
|
39
|
+
reasoningTokens?: number;
|
|
40
|
+
cachedInputTokens?: number;
|
|
41
|
+
};
|
|
42
|
+
type RunTelemetryUsageInit = RunTelemetryUsage & {
|
|
43
|
+
promptTokens?: number;
|
|
44
|
+
completionTokens?: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the token counts a provider reports under either the current or the
|
|
48
|
+
* legacy prompt/completion names. Returns undefined when no count is present,
|
|
49
|
+
* so callers can tell an empty usage object from a zeroed one.
|
|
50
|
+
*/
|
|
51
|
+
declare function normalizeRunTelemetryUsage(usage: RunTelemetryUsageInit): RunTelemetryUsage | undefined;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { AssistantCloudRunReportToolCall, RunTelemetryToolCallInit, RunTelemetryUsage, RunTelemetryUsageInit, createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText };
|
|
54
|
+
//# sourceMappingURL=runTelemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runTelemetry.d.ts","names":[],"sources":["../src/runTelemetry.ts"],"mappings":";;KAMY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iBAAiB;;;;;;iBAOH,yBAAyB;KAwC7B;EACV;EACA;EACA;;;;;EAKA;EACA;EACA;;;;;;;iBAQc,2BACd,MAAM,2BACL;KAgBS;EACV;EACA;EACA;EACA;;KAGU,wBAAwB;EAClC;EACA;;;;;;;iBAQc,2BACd,OAAO,wBACN"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/runTelemetry.ts
|
|
2
|
+
const MAX_TELEMETRY_TEXT_LENGTH = 5e4;
|
|
3
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;
|
|
4
|
+
/**
|
|
5
|
+
* Clamps a string to the size the runs endpoint accepts for a single span
|
|
6
|
+
* field.
|
|
7
|
+
*/
|
|
8
|
+
function truncateRunTelemetryText(value) {
|
|
9
|
+
if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;
|
|
10
|
+
return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);
|
|
11
|
+
}
|
|
12
|
+
function safeStringify(value) {
|
|
13
|
+
if (value == null) return void 0;
|
|
14
|
+
try {
|
|
15
|
+
return truncateRunTelemetryText(JSON.stringify(value));
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function summarizeMcpResult(value) {
|
|
21
|
+
if (value == null) return void 0;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
24
|
+
if (Array.isArray(parsed)) {
|
|
25
|
+
const summarized = parsed.map((item) => {
|
|
26
|
+
if (item && typeof item === "object" && item.type) {
|
|
27
|
+
if ((item.type === "image" || item.type === "audio") && typeof item.data === "string" && BASE64_PATTERN.test(item.data.slice(0, 200))) {
|
|
28
|
+
const sizeKB = (item.data.length * 3 / 4 / 1024).toFixed(1);
|
|
29
|
+
return {
|
|
30
|
+
...item,
|
|
31
|
+
data: `[${item.type}: ${sizeKB}KB]`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return item;
|
|
36
|
+
});
|
|
37
|
+
return truncateRunTelemetryText(JSON.stringify(summarized));
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
return safeStringify(value);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
|
|
44
|
+
* source has its result summarized, because MCP content blocks carry inline
|
|
45
|
+
* base64 image and audio payloads that would otherwise dominate the report.
|
|
46
|
+
*/
|
|
47
|
+
function createRunTelemetryToolCall(init) {
|
|
48
|
+
const { toolName, toolCallId, args, argsText, result, toolSource } = init;
|
|
49
|
+
const call = {
|
|
50
|
+
tool_name: toolName,
|
|
51
|
+
tool_call_id: toolCallId
|
|
52
|
+
};
|
|
53
|
+
const toolArgs = argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);
|
|
54
|
+
if (toolArgs !== void 0) call.tool_args = toolArgs;
|
|
55
|
+
const toolResult = toolSource === "mcp" ? summarizeMcpResult(result) : safeStringify(result);
|
|
56
|
+
if (toolResult !== void 0) call.tool_result = toolResult;
|
|
57
|
+
if (toolSource) call.tool_source = toolSource;
|
|
58
|
+
return call;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the token counts a provider reports under either the current or the
|
|
62
|
+
* legacy prompt/completion names. Returns undefined when no count is present,
|
|
63
|
+
* so callers can tell an empty usage object from a zeroed one.
|
|
64
|
+
*/
|
|
65
|
+
function normalizeRunTelemetryUsage(usage) {
|
|
66
|
+
const inputTokens = usage.inputTokens ?? usage.promptTokens;
|
|
67
|
+
const outputTokens = usage.outputTokens ?? usage.completionTokens;
|
|
68
|
+
if (inputTokens == null && outputTokens == null && usage.reasoningTokens == null && usage.cachedInputTokens == null) return;
|
|
69
|
+
return {
|
|
70
|
+
...inputTokens != null ? { inputTokens } : void 0,
|
|
71
|
+
...outputTokens != null ? { outputTokens } : void 0,
|
|
72
|
+
...usage.reasoningTokens != null ? { reasoningTokens: usage.reasoningTokens } : void 0,
|
|
73
|
+
...usage.cachedInputTokens != null ? { cachedInputTokens: usage.cachedInputTokens } : void 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=runTelemetry.js.map
|
|
@@ -0,0 +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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-cloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "Cloud integration for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"assistant-stream": "^0.3.
|
|
30
|
+
"assistant-stream": "^0.3.40"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"@assistant-ui/x-buildutils": "0.0.24",
|
|
33
34
|
"@types/node": "^26.2.0",
|
|
34
|
-
"vitest": "^4.1.
|
|
35
|
-
"@assistant-ui/x-buildutils": "0.0.23"
|
|
35
|
+
"vitest": "^4.1.11"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public",
|
package/src/AssistantCloud.ts
CHANGED
package/src/AssistantCloudAPI.ts
CHANGED
|
@@ -139,16 +139,17 @@ export class AssistantCloudAPI {
|
|
|
139
139
|
|
|
140
140
|
if (!response.ok) {
|
|
141
141
|
const text = await response.text();
|
|
142
|
+
let message: string | undefined;
|
|
142
143
|
try {
|
|
143
144
|
const body = JSON.parse(text);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
if (typeof body?.message === "string" && body.message.length > 0) {
|
|
146
|
+
message = body.message;
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
throw new CloudAPIError(
|
|
150
|
+
message ?? `Request failed with status ${response.status}, ${text}`,
|
|
151
|
+
response.status,
|
|
152
|
+
);
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
return response;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AssistantCloudRunReportToolCall } from "./runTelemetry";
|
|
3
3
|
import { AssistantStream, PlainTextDecoder } from "assistant-stream";
|
|
4
4
|
import {
|
|
5
5
|
CloudResponseError,
|
|
@@ -13,17 +13,6 @@ type AssistantCloudRunsStreamBody = {
|
|
|
13
13
|
messages: readonly unknown[]; // TODO type
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
type ReportToolCall = {
|
|
17
|
-
tool_name: string;
|
|
18
|
-
tool_call_id: string;
|
|
19
|
-
tool_args?: string;
|
|
20
|
-
tool_result?: string;
|
|
21
|
-
tool_source?: "mcp" | "frontend" | "backend";
|
|
22
|
-
start_ms?: number;
|
|
23
|
-
end_ms?: number;
|
|
24
|
-
sampling_calls?: SamplingCallData[];
|
|
25
|
-
};
|
|
26
|
-
|
|
27
16
|
// NOTE: Keep this payload shape aligned with the strict runtime validator in
|
|
28
17
|
// assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts
|
|
29
18
|
// (createRunSchema). New telemetry fields must be added in both repos together.
|
|
@@ -31,13 +20,13 @@ export type AssistantCloudRunReport = {
|
|
|
31
20
|
thread_id: string;
|
|
32
21
|
status: "completed" | "incomplete" | "error";
|
|
33
22
|
total_steps?: number;
|
|
34
|
-
tool_calls?:
|
|
23
|
+
tool_calls?: AssistantCloudRunReportToolCall[];
|
|
35
24
|
steps?: {
|
|
36
25
|
input_tokens?: number;
|
|
37
26
|
output_tokens?: number;
|
|
38
27
|
reasoning_tokens?: number;
|
|
39
28
|
cached_input_tokens?: number;
|
|
40
|
-
tool_calls?:
|
|
29
|
+
tool_calls?: AssistantCloudRunReportToolCall[];
|
|
41
30
|
start_ms?: number;
|
|
42
31
|
end_ms?: number;
|
|
43
32
|
}[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloud } from "./AssistantCloud";
|
|
3
|
+
import { generateThreadTitle } from "./generateThreadTitle";
|
|
4
|
+
|
|
5
|
+
const titleStream = (...chunks: { type: string; textDelta?: string }[]) =>
|
|
6
|
+
new ReadableStream({
|
|
7
|
+
start(controller) {
|
|
8
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
9
|
+
controller.close();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const createCloud = (stream: ReadableStream<unknown>) => {
|
|
14
|
+
const update = vi.fn().mockResolvedValue(undefined);
|
|
15
|
+
const run = vi.fn().mockResolvedValue(stream);
|
|
16
|
+
const cloud = {
|
|
17
|
+
threads: { update },
|
|
18
|
+
runs: { stream: run },
|
|
19
|
+
} as unknown as AssistantCloud;
|
|
20
|
+
return { cloud, update, run };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("generateThreadTitle", () => {
|
|
24
|
+
it("accumulates text deltas and updates the thread title", async () => {
|
|
25
|
+
const { cloud, run, update } = createCloud(
|
|
26
|
+
titleStream(
|
|
27
|
+
{ type: "text-delta", textDelta: "Weather " },
|
|
28
|
+
{ type: "text-delta", textDelta: "chat" },
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await expect(
|
|
33
|
+
generateThreadTitle(cloud, {
|
|
34
|
+
threadId: "thread-1",
|
|
35
|
+
messages: [
|
|
36
|
+
{
|
|
37
|
+
role: "user",
|
|
38
|
+
content: [{ type: "text", text: "What is the weather today?" }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
).resolves.toBe("Weather chat");
|
|
43
|
+
|
|
44
|
+
expect(run).toHaveBeenCalledExactlyOnceWith({
|
|
45
|
+
thread_id: "thread-1",
|
|
46
|
+
assistant_id: "system/thread_title",
|
|
47
|
+
messages: [
|
|
48
|
+
{
|
|
49
|
+
role: "user",
|
|
50
|
+
content: [{ type: "text", text: "What is the weather today?" }],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
expect(update).toHaveBeenCalledExactlyOnceWith("thread-1", {
|
|
55
|
+
title: "Weather chat",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns null without updating when the stream has no text", async () => {
|
|
60
|
+
const { cloud, update } = createCloud(titleStream());
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
generateThreadTitle(cloud, {
|
|
64
|
+
threadId: "thread-1",
|
|
65
|
+
messages: [],
|
|
66
|
+
}),
|
|
67
|
+
).resolves.toBeNull();
|
|
68
|
+
|
|
69
|
+
expect(update).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AssistantCloud } from "./AssistantCloud";
|
|
2
|
+
|
|
3
|
+
export async function generateThreadTitle(
|
|
4
|
+
cloud: AssistantCloud,
|
|
5
|
+
options: {
|
|
6
|
+
threadId: string;
|
|
7
|
+
messages: readonly {
|
|
8
|
+
role: string;
|
|
9
|
+
content: readonly { type: "text"; text: string }[];
|
|
10
|
+
}[];
|
|
11
|
+
},
|
|
12
|
+
): Promise<string | null> {
|
|
13
|
+
const stream = await cloud.runs.stream({
|
|
14
|
+
thread_id: options.threadId,
|
|
15
|
+
assistant_id: "system/thread_title",
|
|
16
|
+
messages: options.messages,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let title = "";
|
|
20
|
+
const reader = stream.getReader();
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
const { done, value: chunk } = await reader.read();
|
|
24
|
+
if (done) break;
|
|
25
|
+
if (chunk.type === "text-delta") {
|
|
26
|
+
title += chunk.textDelta;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
reader.releaseLock();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (title) {
|
|
34
|
+
await cloud.threads.update(options.threadId, { title });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return title || null;
|
|
38
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,17 @@ export type { CloudMessage } from "./AssistantCloudThreadMessages";
|
|
|
2
2
|
export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
|
|
3
3
|
export { CloudAPIError } from "./AssistantCloudAPI";
|
|
4
4
|
export { CloudResponseError } from "./cloudResponse";
|
|
5
|
+
export { generateThreadTitle } from "./generateThreadTitle";
|
|
5
6
|
export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
|
|
7
|
+
export {
|
|
8
|
+
createRunTelemetryToolCall,
|
|
9
|
+
normalizeRunTelemetryUsage,
|
|
10
|
+
truncateRunTelemetryText,
|
|
11
|
+
type AssistantCloudRunReportToolCall,
|
|
12
|
+
type RunTelemetryToolCallInit,
|
|
13
|
+
type RunTelemetryUsage,
|
|
14
|
+
type RunTelemetryUsageInit,
|
|
15
|
+
} from "./runTelemetry";
|
|
6
16
|
export { AssistantCloud } from "./AssistantCloud";
|
|
7
17
|
export { CloudMessagePersistence } from "./CloudMessagePersistence";
|
|
8
18
|
export {
|
|
@@ -0,0 +1,130 @@
|
|
|
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("returns undefined when no count is present", () => {
|
|
128
|
+
expect(normalizeRunTelemetryUsage({})).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolves the token counts a provider reports under either the current or the
|
|
112
|
+
* legacy prompt/completion names. Returns undefined when no count is present,
|
|
113
|
+
* so callers can tell an empty usage object from a zeroed one.
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeRunTelemetryUsage(
|
|
116
|
+
usage: RunTelemetryUsageInit,
|
|
117
|
+
): RunTelemetryUsage | undefined {
|
|
118
|
+
const inputTokens = usage.inputTokens ?? usage.promptTokens;
|
|
119
|
+
const outputTokens = usage.outputTokens ?? usage.completionTokens;
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
inputTokens == null &&
|
|
123
|
+
outputTokens == null &&
|
|
124
|
+
usage.reasoningTokens == null &&
|
|
125
|
+
usage.cachedInputTokens == null
|
|
126
|
+
) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
...(inputTokens != null ? { inputTokens } : undefined),
|
|
132
|
+
...(outputTokens != null ? { outputTokens } : undefined),
|
|
133
|
+
...(usage.reasoningTokens != null
|
|
134
|
+
? { reasoningTokens: usage.reasoningTokens }
|
|
135
|
+
: undefined),
|
|
136
|
+
...(usage.cachedInputTokens != null
|
|
137
|
+
? { cachedInputTokens: usage.cachedInputTokens }
|
|
138
|
+
: undefined),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -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,
|