@assistant-ui/react-ai-sdk 1.3.31 → 1.3.33
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/dist/assistant-stream/dist/core/tool/schema-utils.js +5 -2
- package/dist/assistant-stream/dist/core/tool/schema-utils.js.map +1 -1
- package/dist/assistant-stream/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/client.d.ts +10 -0
- package/dist/client.js +9 -0
- package/dist/frontendTools.d.ts +1 -1
- package/dist/frontendTools.d.ts.map +1 -1
- package/dist/frontendTools.js +3 -27
- package/dist/frontendTools.js.map +1 -1
- package/dist/generativeTools.d.ts +33 -3
- package/dist/generativeTools.d.ts.map +1 -1
- package/dist/generativeTools.js +117 -10
- package/dist/generativeTools.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.native.d.ts +10 -0
- package/dist/index.native.js +9 -0
- package/dist/injectQuoteContext.js.map +1 -1
- package/dist/mcp-stdio.node.d.ts +2 -0
- package/dist/mcp-stdio.node.js +2 -0
- package/dist/mcp-stdio.unsupported.d.ts +7 -0
- package/dist/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/mcp-stdio.unsupported.js +11 -0
- package/dist/mcp-stdio.unsupported.js.map +1 -0
- package/dist/modelContentEnvelope.d.ts +3 -3
- package/dist/modelContentEnvelope.d.ts.map +1 -1
- package/dist/modelContentEnvelope.js.map +1 -1
- package/dist/toolOutputConversion.d.ts +34 -0
- package/dist/toolOutputConversion.d.ts.map +1 -0
- package/dist/toolOutputConversion.js +31 -0
- package/dist/toolOutputConversion.js.map +1 -0
- package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.d.ts +10 -0
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +6 -4
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts +1 -0
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +3 -2
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.js.map +1 -1
- package/dist/ui/use-chat/useStreamingTiming.js +1 -1
- package/dist/ui/utils/convertMessage.d.ts +1 -1
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/dist/ui/utils/sliceMessagesUntil.js.map +1 -1
- package/dist/ui/utils/toCreateMessage.js +4 -0
- package/dist/ui/utils/toCreateMessage.js.map +1 -1
- package/dist/usage.js.map +1 -1
- package/package.json +27 -10
- package/src/client.ts +18 -0
- package/src/frontendTools.ts +4 -27
- package/src/generativeTools.test.ts +389 -0
- package/src/generativeTools.ts +253 -19
- package/src/index.native.ts +3 -0
- package/src/index.ts +4 -16
- package/src/mcp-stdio.node.ts +1 -0
- package/src/mcp-stdio.unsupported.ts +12 -0
- package/src/modelContentEnvelope.ts +7 -5
- package/src/toolOutputConversion.ts +29 -0
- package/src/ui/use-chat/useAISDKRuntime.test.ts +105 -0
- package/src/ui/use-chat/useAISDKRuntime.ts +14 -1
- package/src/ui/use-chat/useChatRuntime.ts +3 -0
- package/src/ui/utils/toCreateMessage.test.ts +54 -0
- package/src/ui/utils/toCreateMessage.ts +5 -0
package/dist/usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","names":[],"sources":["../src/usage.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/react\" />\nimport { useAuiState } from \"@assistant-ui/store\";\n\nexport type ThreadTokenUsage = {\n totalTokens?: number;\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport interface TokenUsageExtractableMessage {\n role?: string;\n metadata?: unknown;\n}\n\ntype UsageRecord = Record<string, unknown>;\n\nconst USAGE_KEYS = [\n \"inputTokens\",\n \"outputTokens\",\n \"reasoningTokens\",\n \"cachedInputTokens\",\n \"totalTokens\",\n] as const satisfies (keyof ThreadTokenUsage)[];\n\nfunction asRecord(value: unknown): UsageRecord | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n return undefined;\n return value as UsageRecord;\n}\n\nfunction asPositiveTokenCount(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n return undefined;\n }\n return value;\n}\n\nfunction computeTotalTokens(usage: ThreadTokenUsage): number | undefined {\n if (usage.totalTokens !== undefined) return usage.totalTokens;\n if (usage.inputTokens !== undefined && usage.outputTokens !== undefined) {\n return usage.inputTokens + usage.outputTokens;\n }\n return undefined;\n}\n\nfunction normalizeUsage(value: unknown): ThreadTokenUsage | undefined {\n const record = asRecord(value);\n if (!record) return undefined;\n\n const result: ThreadTokenUsage = {};\n let hasFields = false;\n for (const key of USAGE_KEYS) {\n const count = asPositiveTokenCount(record[key]);\n if (count !== undefined) {\n result[key] = count;\n hasFields = true;\n }\n }\n return hasFields ? result : undefined;\n}\n\nfunction withComputedTotal(\n usage: ThreadTokenUsage,\n): ThreadTokenUsage | undefined {\n const totalTokens = computeTotalTokens(usage);\n return { ...usage, ...(totalTokens !== undefined && { totalTokens }) };\n}\n\nfunction usageFromSteps(value: unknown): ThreadTokenUsage | undefined {\n const steps = Array.isArray(value) ? value : [];\n\n const sums: Record<string, number> = {};\n const present: Record<string, boolean> = {};\n let stepsWithUsage = 0;\n let stepsWithComputableTotal = 0;\n\n for (const step of steps) {\n const usage = normalizeUsage(asRecord(step)?.usage);\n if (!usage) continue;\n stepsWithUsage++;\n\n const stepTotal = computeTotalTokens(usage);\n if (stepTotal !== undefined) {\n sums.totalTokens = (sums.totalTokens ?? 0) + stepTotal;\n stepsWithComputableTotal++;\n }\n\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (usage[key] !== undefined) {\n sums[key] = (sums[key] ?? 0) + usage[key];\n present[key] = true;\n }\n }\n }\n\n if (stepsWithUsage === 0) return undefined;\n\n const result: ThreadTokenUsage = {};\n if (stepsWithComputableTotal === stepsWithUsage) {\n result.totalTokens = sums.totalTokens!;\n }\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (present[key]) {\n result[key] = sums[key]!;\n }\n }\n return result;\n}\n\nexport function getThreadMessageTokenUsage(\n message: TokenUsageExtractableMessage | undefined,\n): ThreadTokenUsage | undefined {\n if (!message || message.role !== \"assistant\") return undefined;\n\n const metadata = asRecord(message.metadata);\n if (!metadata) return undefined;\n\n const topLevelUsage = normalizeUsage(metadata.usage);\n if (topLevelUsage) return withComputedTotal(topLevelUsage);\n\n const legacyUsage = normalizeUsage(asRecord(metadata.custom)?.usage);\n if (legacyUsage) return withComputedTotal(legacyUsage);\n\n return usageFromSteps(metadata.steps);\n}\n\nexport function getLatestThreadTokenUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): ThreadTokenUsage | undefined {\n return getThreadMessageTokenUsage(findLatestMessageWithUsage(messages));\n}\n\nfunction findLatestMessageWithUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): TokenUsageExtractableMessage | undefined {\n if (!messages) return undefined;\n\n for (let idx = messages.length - 1; idx >= 0; idx -= 1) {\n const message = messages[idx];\n if (getThreadMessageTokenUsage(message)) {\n return message;\n }\n }\n\n return undefined;\n}\n\nexport function useThreadTokenUsage(): ThreadTokenUsage | undefined {\n const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));\n return getThreadMessageTokenUsage(msg);\n}\n"],"mappings":";;AAkBA,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,SAAS,OAAyC;CACzD,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAC5D,OAAO,KAAA;CACT,OAAO;AACT;AAEA,SAAS,qBAAqB,OAAoC;CAChE,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GAClE;CAEF,OAAO;AACT;AAEA,SAAS,mBAAmB,OAA6C;CACvE,IAAI,MAAM,gBAAgB,KAAA,GAAW,OAAO,MAAM;CAClD,IAAI,MAAM,gBAAgB,KAAA,KAAa,MAAM,iBAAiB,KAAA,GAC5D,OAAO,MAAM,cAAc,MAAM;AAGrC;AAEA,SAAS,eAAe,OAA8C;CACpE,MAAM,SAAS,SAAS,KAAK;CAC7B,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,MAAM,SAA2B,CAAC;CAClC,IAAI,YAAY;CAChB,KAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,QAAQ,qBAAqB,OAAO,IAAI;EAC9C,IAAI,UAAU,KAAA,GAAW;GACvB,OAAO,OAAO;GACd,YAAY;EACd;CACF;CACA,OAAO,YAAY,SAAS,KAAA;AAC9B;AAEA,SAAS,kBACP,OAC8B;CAC9B,MAAM,cAAc,mBAAmB,KAAK;CAC5C,OAAO;EAAE,GAAG;EAAO,GAAI,gBAAgB,KAAA,KAAa,EAAE,YAAY;CAAG;AACvE;AAEA,SAAS,eAAe,OAA8C;CACpE,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;CAE9C,MAAM,OAA+B,CAAC;CACtC,MAAM,UAAmC,CAAC;CAC1C,IAAI,iBAAiB;CACrB,IAAI,2BAA2B;CAE/B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,eAAe,SAAS,IAAI,
|
|
1
|
+
{"version":3,"file":"usage.js","names":[],"sources":["../src/usage.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/react\" />\nimport { useAuiState } from \"@assistant-ui/store\";\n\nexport type ThreadTokenUsage = {\n totalTokens?: number;\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport interface TokenUsageExtractableMessage {\n role?: string;\n metadata?: unknown;\n}\n\ntype UsageRecord = Record<string, unknown>;\n\nconst USAGE_KEYS = [\n \"inputTokens\",\n \"outputTokens\",\n \"reasoningTokens\",\n \"cachedInputTokens\",\n \"totalTokens\",\n] as const satisfies (keyof ThreadTokenUsage)[];\n\nfunction asRecord(value: unknown): UsageRecord | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n return undefined;\n return value as UsageRecord;\n}\n\nfunction asPositiveTokenCount(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n return undefined;\n }\n return value;\n}\n\nfunction computeTotalTokens(usage: ThreadTokenUsage): number | undefined {\n if (usage.totalTokens !== undefined) return usage.totalTokens;\n if (usage.inputTokens !== undefined && usage.outputTokens !== undefined) {\n return usage.inputTokens + usage.outputTokens;\n }\n return undefined;\n}\n\nfunction normalizeUsage(value: unknown): ThreadTokenUsage | undefined {\n const record = asRecord(value);\n if (!record) return undefined;\n\n const result: ThreadTokenUsage = {};\n let hasFields = false;\n for (const key of USAGE_KEYS) {\n const count = asPositiveTokenCount(record[key]);\n if (count !== undefined) {\n result[key] = count;\n hasFields = true;\n }\n }\n return hasFields ? result : undefined;\n}\n\nfunction withComputedTotal(\n usage: ThreadTokenUsage,\n): ThreadTokenUsage | undefined {\n const totalTokens = computeTotalTokens(usage);\n return { ...usage, ...(totalTokens !== undefined && { totalTokens }) };\n}\n\nfunction usageFromSteps(value: unknown): ThreadTokenUsage | undefined {\n const steps = Array.isArray(value) ? value : [];\n\n const sums: Record<string, number> = {};\n const present: Record<string, boolean> = {};\n let stepsWithUsage = 0;\n let stepsWithComputableTotal = 0;\n\n for (const step of steps) {\n const usage = normalizeUsage(asRecord(step)?.usage);\n if (!usage) continue;\n stepsWithUsage++;\n\n const stepTotal = computeTotalTokens(usage);\n if (stepTotal !== undefined) {\n sums.totalTokens = (sums.totalTokens ?? 0) + stepTotal;\n stepsWithComputableTotal++;\n }\n\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (usage[key] !== undefined) {\n sums[key] = (sums[key] ?? 0) + usage[key];\n present[key] = true;\n }\n }\n }\n\n if (stepsWithUsage === 0) return undefined;\n\n const result: ThreadTokenUsage = {};\n if (stepsWithComputableTotal === stepsWithUsage) {\n result.totalTokens = sums.totalTokens!;\n }\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (present[key]) {\n result[key] = sums[key]!;\n }\n }\n return result;\n}\n\nexport function getThreadMessageTokenUsage(\n message: TokenUsageExtractableMessage | undefined,\n): ThreadTokenUsage | undefined {\n if (!message || message.role !== \"assistant\") return undefined;\n\n const metadata = asRecord(message.metadata);\n if (!metadata) return undefined;\n\n const topLevelUsage = normalizeUsage(metadata.usage);\n if (topLevelUsage) return withComputedTotal(topLevelUsage);\n\n const legacyUsage = normalizeUsage(asRecord(metadata.custom)?.usage);\n if (legacyUsage) return withComputedTotal(legacyUsage);\n\n return usageFromSteps(metadata.steps);\n}\n\nexport function getLatestThreadTokenUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): ThreadTokenUsage | undefined {\n return getThreadMessageTokenUsage(findLatestMessageWithUsage(messages));\n}\n\nfunction findLatestMessageWithUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): TokenUsageExtractableMessage | undefined {\n if (!messages) return undefined;\n\n for (let idx = messages.length - 1; idx >= 0; idx -= 1) {\n const message = messages[idx];\n if (getThreadMessageTokenUsage(message)) {\n return message;\n }\n }\n\n return undefined;\n}\n\nexport function useThreadTokenUsage(): ThreadTokenUsage | undefined {\n const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));\n return getThreadMessageTokenUsage(msg);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-ai-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.33",
|
|
4
4
|
"description": "Vercel AI SDK adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -15,13 +15,29 @@
|
|
|
15
15
|
"author": "AgentbaseAI Inc.",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"type": "module",
|
|
18
|
+
"imports": {
|
|
19
|
+
"#mcp-stdio": {
|
|
20
|
+
"types": "./src/mcp-stdio.node.ts",
|
|
21
|
+
"react-native": "./dist/mcp-stdio.unsupported.js",
|
|
22
|
+
"edge-light": "./dist/mcp-stdio.unsupported.js",
|
|
23
|
+
"workerd": "./dist/mcp-stdio.unsupported.js",
|
|
24
|
+
"browser": "./dist/mcp-stdio.unsupported.js",
|
|
25
|
+
"node": "./dist/mcp-stdio.node.js",
|
|
26
|
+
"default": "./dist/mcp-stdio.unsupported.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
18
29
|
"exports": {
|
|
19
30
|
".": {
|
|
31
|
+
"react-native": {
|
|
32
|
+
"types": "./dist/index.native.d.ts",
|
|
33
|
+
"default": "./dist/index.native.js"
|
|
34
|
+
},
|
|
20
35
|
"types": "./dist/index.d.ts",
|
|
21
36
|
"default": "./dist/index.js"
|
|
22
37
|
}
|
|
23
38
|
},
|
|
24
39
|
"main": "./dist/index.js",
|
|
40
|
+
"react-native": "./dist/index.native.js",
|
|
25
41
|
"types": "./dist/index.d.ts",
|
|
26
42
|
"files": [
|
|
27
43
|
"dist",
|
|
@@ -30,10 +46,11 @@
|
|
|
30
46
|
],
|
|
31
47
|
"sideEffects": false,
|
|
32
48
|
"dependencies": {
|
|
33
|
-
"@ai-sdk/
|
|
34
|
-
"@
|
|
35
|
-
"@assistant-ui/
|
|
36
|
-
"
|
|
49
|
+
"@ai-sdk/mcp": "^1.0.46",
|
|
50
|
+
"@ai-sdk/react": "^3.0.199",
|
|
51
|
+
"@assistant-ui/core": "^0.2.11",
|
|
52
|
+
"@assistant-ui/store": "^0.2.14",
|
|
53
|
+
"ai": "^6.0.197",
|
|
37
54
|
"assistant-cloud": "*"
|
|
38
55
|
},
|
|
39
56
|
"peerDependencies": {
|
|
@@ -48,13 +65,13 @@
|
|
|
48
65
|
"devDependencies": {
|
|
49
66
|
"@testing-library/react": "^16.3.2",
|
|
50
67
|
"@types/json-schema": "^7.0.15",
|
|
51
|
-
"@types/react": "^19.2.
|
|
68
|
+
"@types/react": "^19.2.17",
|
|
52
69
|
"@types/react-dom": "^19.2.3",
|
|
53
70
|
"jsdom": "^29.1.1",
|
|
54
|
-
"react": "^19.2.
|
|
55
|
-
"vitest": "^4.1.
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
57
|
-
"assistant-stream": "0.3.
|
|
71
|
+
"react": "^19.2.7",
|
|
72
|
+
"vitest": "^4.1.8",
|
|
73
|
+
"@assistant-ui/x-buildutils": "0.0.12",
|
|
74
|
+
"assistant-stream": "0.3.21"
|
|
58
75
|
},
|
|
59
76
|
"publishConfig": {
|
|
60
77
|
"access": "public",
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="@assistant-ui/core/react" />
|
|
2
|
+
|
|
3
|
+
export { useAISDKRuntime } from "./ui/use-chat/useAISDKRuntime";
|
|
4
|
+
export { useChatRuntime } from "./ui/use-chat/useChatRuntime";
|
|
5
|
+
export type { UseChatRuntimeOptions } from "./ui/use-chat/useChatRuntime";
|
|
6
|
+
export { AssistantChatTransport } from "./ui/use-chat/AssistantChatTransport";
|
|
7
|
+
export {
|
|
8
|
+
RESUMABLE_STREAM_ID_HEADER,
|
|
9
|
+
createResumableSessionStorage,
|
|
10
|
+
} from "./ui/resumable";
|
|
11
|
+
export type {
|
|
12
|
+
AssistantChatResumableOptions,
|
|
13
|
+
ResumableClientStorage,
|
|
14
|
+
} from "./ui/resumable";
|
|
15
|
+
export { frontendTools } from "./frontendTools";
|
|
16
|
+
export { injectQuoteContext } from "./injectQuoteContext";
|
|
17
|
+
export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
|
|
18
|
+
export { getThreadMessageTokenUsage, useThreadTokenUsage } from "./usage";
|
package/src/frontendTools.ts
CHANGED
|
@@ -1,37 +1,14 @@
|
|
|
1
1
|
import { jsonSchema, type ToolSet } from "ai";
|
|
2
|
-
import type { ToolJSONSchema
|
|
2
|
+
import type { ToolJSONSchema } from "assistant-stream";
|
|
3
3
|
import { unwrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
4
|
-
|
|
5
|
-
const toAISDKContent = (parts: readonly ToolModelContentPart[]) => ({
|
|
6
|
-
type: "content" as const,
|
|
7
|
-
value: parts.map((part) => {
|
|
8
|
-
if (part.type === "text") {
|
|
9
|
-
return { type: "text" as const, text: part.text };
|
|
10
|
-
}
|
|
11
|
-
const isImage = part.mediaType.startsWith("image/");
|
|
12
|
-
return isImage
|
|
13
|
-
? {
|
|
14
|
-
type: "image-data" as const,
|
|
15
|
-
data: part.data,
|
|
16
|
-
mediaType: part.mediaType,
|
|
17
|
-
}
|
|
18
|
-
: {
|
|
19
|
-
type: "file-data" as const,
|
|
20
|
-
data: part.data,
|
|
21
|
-
mediaType: part.mediaType,
|
|
22
|
-
...(part.filename !== undefined && { filename: part.filename }),
|
|
23
|
-
};
|
|
24
|
-
}),
|
|
25
|
-
});
|
|
4
|
+
import { toAISDKContent, toAISDKDefaultOutput } from "./toolOutputConversion";
|
|
26
5
|
|
|
27
6
|
export const defaultToModelOutput = ({ output }: { output: unknown }) => {
|
|
28
|
-
const { modelContent } = unwrapModelContentEnvelope(output);
|
|
7
|
+
const { result, modelContent } = unwrapModelContentEnvelope(output);
|
|
29
8
|
if (modelContent !== undefined) {
|
|
30
9
|
return toAISDKContent(modelContent);
|
|
31
10
|
}
|
|
32
|
-
return
|
|
33
|
-
? { type: "text" as const, value: output }
|
|
34
|
-
: { type: "json" as const, value: (output ?? null) as any };
|
|
11
|
+
return toAISDKDefaultOutput(result);
|
|
35
12
|
};
|
|
36
13
|
|
|
37
14
|
export const frontendTools = (tools: Record<string, ToolJSONSchema>): ToolSet =>
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AISDKToolkit } from "./generativeTools";
|
|
3
|
+
import { wrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
close: vi.fn(),
|
|
7
|
+
tools: vi.fn(),
|
|
8
|
+
createMCPClient: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@ai-sdk/mcp", () => ({
|
|
12
|
+
createMCPClient: mocks.createMCPClient,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@ai-sdk/mcp/mcp-stdio", () => ({
|
|
16
|
+
Experimental_StdioMCPTransport: vi.fn((config) => ({
|
|
17
|
+
type: "stdio",
|
|
18
|
+
config,
|
|
19
|
+
})),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("AISDKToolkit.tools()", () => {
|
|
23
|
+
it("merges frontend tools with toolkit tools", async () => {
|
|
24
|
+
const toolSet = await new AISDKToolkit({
|
|
25
|
+
toolkit: {
|
|
26
|
+
serverTool: {
|
|
27
|
+
type: "backend",
|
|
28
|
+
description: "Server tool",
|
|
29
|
+
parameters: { type: "object", properties: {} },
|
|
30
|
+
execute: async () => "ok",
|
|
31
|
+
} as never,
|
|
32
|
+
},
|
|
33
|
+
}).tools({
|
|
34
|
+
frontend: {
|
|
35
|
+
clientTool: {
|
|
36
|
+
parameters: { type: "object", properties: {} },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(toolSet.clientTool).toBeDefined();
|
|
42
|
+
expect(toolSet.serverTool?.description).toBe("Server tool");
|
|
43
|
+
expect(toolSet.serverTool?.execute).toBeTypeOf("function");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps a flat toolkit tool named tools", async () => {
|
|
47
|
+
const toolSet = await new AISDKToolkit({
|
|
48
|
+
toolkit: {
|
|
49
|
+
tools: {
|
|
50
|
+
type: "backend",
|
|
51
|
+
description: "Actually a tool, not config",
|
|
52
|
+
parameters: { type: "object", properties: {} },
|
|
53
|
+
execute: async () => "ok",
|
|
54
|
+
} as never,
|
|
55
|
+
},
|
|
56
|
+
}).tools();
|
|
57
|
+
|
|
58
|
+
expect(toolSet.tools?.description).toBe("Actually a tool, not config");
|
|
59
|
+
expect(toolSet.tools?.execute).toBeTypeOf("function");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("converts provider tools without an execute function", async () => {
|
|
63
|
+
const toolSet = await new AISDKToolkit({
|
|
64
|
+
toolkit: {
|
|
65
|
+
web_search: {
|
|
66
|
+
type: "provider",
|
|
67
|
+
providerId: "openai.web_search_preview",
|
|
68
|
+
args: { searchContextSize: "low" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}).tools();
|
|
72
|
+
|
|
73
|
+
expect(toolSet.web_search).toMatchObject({
|
|
74
|
+
type: "provider",
|
|
75
|
+
id: "openai.web_search_preview",
|
|
76
|
+
args: { searchContextSize: "low" },
|
|
77
|
+
});
|
|
78
|
+
expect(toolSet.web_search).not.toHaveProperty("inputSchema");
|
|
79
|
+
expect(toolSet.web_search).not.toHaveProperty("execute");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("forwards provider tool parameters and providerOptions when present", async () => {
|
|
83
|
+
const toolSet = await new AISDKToolkit({
|
|
84
|
+
toolkit: {
|
|
85
|
+
web_search: {
|
|
86
|
+
type: "provider",
|
|
87
|
+
providerId: "openai.web_search_preview",
|
|
88
|
+
args: { searchContextSize: "low" },
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
query: { type: "string" },
|
|
93
|
+
},
|
|
94
|
+
required: ["query"],
|
|
95
|
+
},
|
|
96
|
+
providerOptions: {
|
|
97
|
+
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}).tools();
|
|
102
|
+
|
|
103
|
+
expect(toolSet.web_search).toMatchObject({
|
|
104
|
+
type: "provider",
|
|
105
|
+
id: "openai.web_search_preview",
|
|
106
|
+
args: { searchContextSize: "low" },
|
|
107
|
+
providerOptions: {
|
|
108
|
+
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect(toolSet.web_search).toHaveProperty("inputSchema");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("forwards explicit false supportsDeferredResults", async () => {
|
|
115
|
+
const toolSet = await new AISDKToolkit({
|
|
116
|
+
toolkit: {
|
|
117
|
+
web_search: {
|
|
118
|
+
type: "provider",
|
|
119
|
+
providerId: "openai.web_search_preview",
|
|
120
|
+
args: { searchContextSize: "low" },
|
|
121
|
+
supportsDeferredResults: false,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}).tools();
|
|
125
|
+
|
|
126
|
+
expect(toolSet.web_search).toMatchObject({
|
|
127
|
+
supportsDeferredResults: false,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("AISDKToolkit", () => {
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
mocks.close.mockReset();
|
|
135
|
+
mocks.tools.mockReset();
|
|
136
|
+
mocks.createMCPClient.mockReset();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("loads MCP tools through pooled clients", async () => {
|
|
140
|
+
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
141
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
142
|
+
tools: mocks.tools,
|
|
143
|
+
close: mocks.close,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const toolkit = new AISDKToolkit({
|
|
147
|
+
toolkit: {
|
|
148
|
+
local: {
|
|
149
|
+
type: "mcp",
|
|
150
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
156
|
+
await toolkit.tools();
|
|
157
|
+
|
|
158
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(mocks.createMCPClient).toHaveBeenCalledWith({
|
|
160
|
+
transport: {
|
|
161
|
+
type: "http",
|
|
162
|
+
url: "http://localhost:3001/mcp",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
expect(mocks.tools).toHaveBeenCalledTimes(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("closes pooled MCP clients", async () => {
|
|
169
|
+
mocks.tools.mockResolvedValue({});
|
|
170
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
171
|
+
tools: mocks.tools,
|
|
172
|
+
close: mocks.close,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const toolkit = new AISDKToolkit({
|
|
176
|
+
toolkit: {
|
|
177
|
+
local: {
|
|
178
|
+
type: "mcp",
|
|
179
|
+
server: { type: "sse", url: "http://localhost:3001/sse" },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await toolkit.tools();
|
|
185
|
+
await toolkit.close();
|
|
186
|
+
|
|
187
|
+
expect(mocks.close).toHaveBeenCalledTimes(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("clears pooled MCP clients even when initialization fails", async () => {
|
|
191
|
+
const error = new Error("connect failed");
|
|
192
|
+
const closeError = new Error("close failed");
|
|
193
|
+
const close = vi.fn().mockRejectedValue(closeError);
|
|
194
|
+
mocks.tools.mockResolvedValue({});
|
|
195
|
+
mocks.createMCPClient
|
|
196
|
+
.mockResolvedValueOnce({
|
|
197
|
+
tools: mocks.tools,
|
|
198
|
+
close,
|
|
199
|
+
})
|
|
200
|
+
.mockRejectedValueOnce(error);
|
|
201
|
+
|
|
202
|
+
const toolkit = new AISDKToolkit({
|
|
203
|
+
toolkit: {
|
|
204
|
+
first: {
|
|
205
|
+
type: "mcp",
|
|
206
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
207
|
+
},
|
|
208
|
+
second: {
|
|
209
|
+
type: "mcp",
|
|
210
|
+
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const toolsPromise = toolkit.tools();
|
|
216
|
+
await expect(toolkit.close()).rejects.toMatchObject({
|
|
217
|
+
errors: [error, closeError],
|
|
218
|
+
});
|
|
219
|
+
await expect(toolsPromise).rejects.toThrow(error);
|
|
220
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
221
|
+
|
|
222
|
+
await expect(toolkit.close()).resolves.toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("evicts failed MCP client initialization so later calls can retry", async () => {
|
|
226
|
+
const error = new Error("connect failed");
|
|
227
|
+
mocks.createMCPClient.mockRejectedValueOnce(error).mockResolvedValueOnce({
|
|
228
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
229
|
+
close: mocks.close,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const toolkit = new AISDKToolkit({
|
|
233
|
+
toolkit: {
|
|
234
|
+
local: {
|
|
235
|
+
type: "mcp",
|
|
236
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await expect(toolkit.tools()).rejects.toThrow(error);
|
|
242
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
243
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("rejects duplicate MCP tool names", async () => {
|
|
247
|
+
mocks.createMCPClient
|
|
248
|
+
.mockResolvedValueOnce({
|
|
249
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
250
|
+
close: mocks.close,
|
|
251
|
+
})
|
|
252
|
+
.mockResolvedValueOnce({
|
|
253
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
254
|
+
close: mocks.close,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const toolkit = new AISDKToolkit({
|
|
258
|
+
toolkit: {
|
|
259
|
+
first: {
|
|
260
|
+
type: "mcp",
|
|
261
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
262
|
+
},
|
|
263
|
+
second: {
|
|
264
|
+
type: "mcp",
|
|
265
|
+
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await expect(toolkit.tools()).rejects.toThrow(
|
|
271
|
+
/MCP tool name collision: "echo"/,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("includes provider tools alongside MCP tools", async () => {
|
|
276
|
+
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
277
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
278
|
+
tools: mocks.tools,
|
|
279
|
+
close: mocks.close,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const toolkit = new AISDKToolkit({
|
|
283
|
+
toolkit: {
|
|
284
|
+
local: {
|
|
285
|
+
type: "mcp",
|
|
286
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
287
|
+
},
|
|
288
|
+
web_search: {
|
|
289
|
+
type: "provider",
|
|
290
|
+
providerId: "openai.web_search_preview",
|
|
291
|
+
args: { searchContextSize: "low" },
|
|
292
|
+
supportsDeferredResults: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await expect(toolkit.tools()).resolves.toMatchObject({
|
|
298
|
+
echo: { inputSchema: {} },
|
|
299
|
+
web_search: {
|
|
300
|
+
type: "provider",
|
|
301
|
+
id: "openai.web_search_preview",
|
|
302
|
+
args: { searchContextSize: "low" },
|
|
303
|
+
supportsDeferredResults: false,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("AISDKToolkit toModelOutput", () => {
|
|
310
|
+
const createWeatherTools = (toModelOutput?: any) =>
|
|
311
|
+
new AISDKToolkit({
|
|
312
|
+
toolkit: {
|
|
313
|
+
get_weather: {
|
|
314
|
+
...(toModelOutput && { toModelOutput }),
|
|
315
|
+
},
|
|
316
|
+
} as any,
|
|
317
|
+
}).tools();
|
|
318
|
+
|
|
319
|
+
it("adapts assistant-ui model content parts to the AI SDK tool output shape", async () => {
|
|
320
|
+
const tools = await createWeatherTools(({ output }: any) => [
|
|
321
|
+
{ type: "text", text: `Weather card displayed: ${output.location}` },
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
325
|
+
toolCallId: "tc-weather",
|
|
326
|
+
input: {},
|
|
327
|
+
output: { location: "San Francisco" },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(output).toEqual({
|
|
331
|
+
type: "content",
|
|
332
|
+
value: [{ type: "text", text: "Weather card displayed: San Francisco" }],
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("uses stored model content envelopes without re-running the custom projector", async () => {
|
|
337
|
+
let called = false;
|
|
338
|
+
const tools = await createWeatherTools(() => {
|
|
339
|
+
called = true;
|
|
340
|
+
return [{ type: "text", text: "recomputed" }];
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
344
|
+
toolCallId: "tc-weather",
|
|
345
|
+
input: {},
|
|
346
|
+
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
347
|
+
{ type: "text", text: "cached weather receipt" },
|
|
348
|
+
]),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(called).toBe(false);
|
|
352
|
+
expect(output).toEqual({
|
|
353
|
+
type: "content",
|
|
354
|
+
value: [{ type: "text", text: "cached weather receipt" }],
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("falls back to default model output when no custom projector is defined", async () => {
|
|
359
|
+
const tools = await createWeatherTools();
|
|
360
|
+
|
|
361
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
362
|
+
toolCallId: "tc-weather",
|
|
363
|
+
input: {},
|
|
364
|
+
output: { location: "San Francisco" },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(output).toEqual({
|
|
368
|
+
type: "json",
|
|
369
|
+
value: { location: "San Francisco" },
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("uses stored model content envelopes when no custom projector is defined", async () => {
|
|
374
|
+
const tools = await createWeatherTools();
|
|
375
|
+
|
|
376
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
377
|
+
toolCallId: "tc-weather",
|
|
378
|
+
input: {},
|
|
379
|
+
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
380
|
+
{ type: "text", text: "cached weather receipt" },
|
|
381
|
+
]),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(output).toEqual({
|
|
385
|
+
type: "content",
|
|
386
|
+
value: [{ type: "text", text: "cached weather receipt" }],
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|