@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/aiSDKFormatAdapter.js +17 -0
- package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.js +36 -0
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
- package/dist/aiSDKExtras.d.ts +13 -0
- package/dist/aiSDKExtras.d.ts.map +1 -0
- package/dist/aiSDKExtras.js +6 -0
- package/dist/aiSDKExtras.js.map +1 -0
- package/dist/converters/convertMessage.d.ts +44 -0
- package/dist/converters/convertMessage.d.ts.map +1 -0
- package/dist/converters/convertMessage.js +465 -0
- package/dist/converters/convertMessage.js.map +1 -0
- package/dist/converters/toCreateMessage.d.ts +4 -0
- package/dist/converters/toCreateMessage.d.ts.map +1 -0
- package/dist/converters/toCreateMessage.js +65 -0
- package/dist/converters/toCreateMessage.js.map +1 -0
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +16 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/model-context/injectInteractableContext.d.ts +37 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.js +54 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -0
- package/dist/model-context/injectQuoteContext.d.ts +25 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
- package/dist/model-context/injectQuoteContext.js +51 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -0
- package/dist/runtime/AISDKChat.d.ts +13 -0
- package/dist/runtime/AISDKChat.d.ts.map +1 -0
- package/dist/runtime/AISDKChat.js +34 -0
- package/dist/runtime/AISDKChat.js.map +1 -0
- package/dist/runtime/AISDKThreads.d.ts +24 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -0
- package/dist/runtime/AISDKThreads.js +92 -0
- package/dist/runtime/AISDKThreads.js.map +1 -0
- package/dist/runtime/DynamicChatTransport.d.ts +50 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +153 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +11 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +122 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +95 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
- package/dist/runtime/useAISDKRuntime.js +754 -0
- package/dist/runtime/useAISDKRuntime.js.map +1 -0
- package/dist/runtime/useChatRuntime.d.ts +17 -0
- package/dist/runtime/useChatRuntime.d.ts.map +1 -0
- package/dist/runtime/useChatRuntime.js +50 -0
- package/dist/runtime/useChatRuntime.js.map +1 -0
- package/dist/runtime/useChatSnapshot.d.ts +6 -0
- package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
- package/dist/runtime/useChatSnapshot.js +62 -0
- package/dist/runtime/useChatSnapshot.js.map +1 -0
- package/dist/runtime/useChatThread.d.ts +121 -0
- package/dist/runtime/useChatThread.d.ts.map +1 -0
- package/dist/runtime/useChatThread.js +255 -0
- package/dist/runtime/useChatThread.js.map +1 -0
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +26 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts +12 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -0
- package/dist/runtime/useExternalHistory.js +556 -0
- package/dist/runtime/useExternalHistory.js.map +1 -0
- package/dist/runtime/useMessageQueue.d.ts +14 -0
- package/dist/runtime/useMessageQueue.d.ts.map +1 -0
- package/dist/runtime/useMessageQueue.js +142 -0
- package/dist/runtime/useMessageQueue.js.map +1 -0
- package/dist/runtime/useResourceCleanup.d.ts +2 -0
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
- package/dist/runtime/useResourceCleanup.js +47 -0
- package/dist/runtime/useResourceCleanup.js.map +1 -0
- package/dist/runtime/useStreamingTiming.d.ts +12 -0
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
- package/dist/runtime/useStreamingTiming.js +36 -0
- package/dist/runtime/useStreamingTiming.js.map +1 -0
- package/dist/tools/generativeTools.d.ts +71 -0
- package/dist/tools/generativeTools.d.ts.map +1 -0
- package/dist/tools/generativeTools.js +258 -0
- package/dist/tools/generativeTools.js.map +1 -0
- package/dist/tools/mcp-stdio.node.d.ts +2 -0
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.node.js +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.js +10 -0
- package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
- package/dist/transport/AssistantChatTransport.d.ts +20 -0
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
- package/dist/transport/AssistantChatTransport.js +151 -0
- package/dist/transport/AssistantChatTransport.js.map +1 -0
- package/dist/transport/resumable.d.ts +36 -0
- package/dist/transport/resumable.d.ts.map +1 -0
- package/dist/transport/resumable.js +112 -0
- package/dist/transport/resumable.js.map +1 -0
- package/dist/usage.d.ts +24 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +121 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/getVercelAIMessages.d.ts +4 -0
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
- package/dist/utils/getVercelAIMessages.js +8 -0
- package/dist/utils/getVercelAIMessages.js.map +1 -0
- package/dist/utils/sliceMessagesUntil.d.ts +3 -0
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
- package/dist/utils/sliceMessagesUntil.js +10 -0
- package/dist/utils/sliceMessagesUntil.js.map +1 -0
- package/package.json +96 -3
- package/src/__tests__/standalone.e2e.ts +118 -0
- package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
- package/src/adapters/aiSDKFormatAdapter.ts +18 -0
- package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
- package/src/adapters/vercelAttachmentAdapter.ts +36 -0
- package/src/aiSDKExtras.ts +31 -0
- package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
- package/src/converters/convertMessage.test.ts +2428 -0
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +919 -0
- package/src/converters/toCreateMessage.test.ts +716 -0
- package/src/converters/toCreateMessage.ts +110 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/hooks.test.ts +168 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +29 -0
- package/src/model-context/injectInteractableContext.test.ts +167 -0
- package/src/model-context/injectInteractableContext.ts +64 -0
- package/src/model-context/injectQuoteContext.test.ts +33 -0
- package/src/model-context/injectQuoteContext.ts +63 -0
- package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
- package/src/runtime/AISDKChat.integration.test.tsx +148 -0
- package/src/runtime/AISDKChat.test.ts +241 -0
- package/src/runtime/AISDKChat.ts +43 -0
- package/src/runtime/AISDKThreads.test.ts +811 -0
- package/src/runtime/AISDKThreads.ts +194 -0
- package/src/runtime/DynamicChatTransport.test.ts +228 -0
- package/src/runtime/DynamicChatTransport.ts +281 -0
- package/src/runtime/__tests__/controlled-transport.ts +70 -0
- package/src/runtime/__tests__/tap.ts +80 -0
- package/src/runtime/__tests__/vue.ts +46 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +187 -0
- package/src/runtime/toolHistoryCodec.ts +245 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
- package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
- package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
- package/src/runtime/useAISDKRuntime.test.ts +2345 -0
- package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
- package/src/runtime/useAISDKRuntime.ts +1503 -0
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatOnTap.test.ts +60 -0
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
- package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
- package/src/runtime/useChatRuntime.native.test.ts +147 -0
- package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
- package/src/runtime/useChatRuntime.test.ts +619 -0
- package/src/runtime/useChatRuntime.ts +80 -0
- package/src/runtime/useChatSnapshot.test.ts +60 -0
- package/src/runtime/useChatSnapshot.ts +86 -0
- package/src/runtime/useChatThread.binding.test.tsx +140 -0
- package/src/runtime/useChatThread.test.ts +993 -0
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +533 -0
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +4187 -0
- package/src/runtime/useExternalHistory.ts +902 -0
- package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
- package/src/runtime/useMessageQueue.test.tsx +256 -0
- package/src/runtime/useMessageQueue.ts +189 -0
- package/src/runtime/useResourceCleanup.ts +58 -0
- package/src/runtime/useStreamingTiming.test.tsx +96 -0
- package/src/runtime/useStreamingTiming.ts +69 -0
- package/src/tools/generativeTools.test.ts +1058 -0
- package/src/tools/generativeTools.ts +491 -0
- package/src/tools/mcp-stdio.node.ts +1 -0
- package/src/tools/mcp-stdio.unsupported.ts +12 -0
- package/src/transport/AssistantChatTransport.test.ts +631 -0
- package/src/transport/AssistantChatTransport.ts +236 -0
- package/src/transport/resumable.test.ts +262 -0
- package/src/transport/resumable.ts +162 -0
- package/src/usage.test.ts +204 -0
- package/src/usage.ts +179 -0
- package/src/utils/getVercelAIMessages.ts +11 -0
- package/src/utils/sliceMessagesUntil.test.ts +51 -0
- package/src/utils/sliceMessagesUntil.ts +16 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getThreadMessageTokenUsage } from "./usage";
|
|
3
|
+
|
|
4
|
+
function msg(metadata: unknown): { role: "assistant"; metadata: unknown } {
|
|
5
|
+
return {
|
|
6
|
+
role: "assistant",
|
|
7
|
+
metadata,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("getThreadMessageTokenUsage", () => {
|
|
12
|
+
it("reads usage from custom.usage", () => {
|
|
13
|
+
const usage = getThreadMessageTokenUsage(
|
|
14
|
+
msg({
|
|
15
|
+
custom: {
|
|
16
|
+
usage: { inputTokens: 4, outputTokens: 6 },
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(usage).toEqual({
|
|
22
|
+
totalTokens: 10,
|
|
23
|
+
inputTokens: 4,
|
|
24
|
+
outputTokens: 6,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not double-count reasoning/cached in fallback totalTokens", () => {
|
|
29
|
+
const usage = getThreadMessageTokenUsage(
|
|
30
|
+
msg({
|
|
31
|
+
usage: {
|
|
32
|
+
inputTokens: 4,
|
|
33
|
+
outputTokens: 6,
|
|
34
|
+
reasoningTokens: 9,
|
|
35
|
+
cachedInputTokens: 3,
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// totalTokens = input + output only; reasoning/cached are detail fields
|
|
41
|
+
expect(usage).toEqual({
|
|
42
|
+
totalTokens: 10,
|
|
43
|
+
inputTokens: 4,
|
|
44
|
+
outputTokens: 6,
|
|
45
|
+
reasoningTokens: 9,
|
|
46
|
+
cachedInputTokens: 3,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reads v7 token detail objects for reasoning and cached tokens", () => {
|
|
51
|
+
const usage = getThreadMessageTokenUsage(
|
|
52
|
+
msg({
|
|
53
|
+
usage: {
|
|
54
|
+
inputTokens: 4,
|
|
55
|
+
outputTokens: 6,
|
|
56
|
+
inputTokenDetails: { cacheReadTokens: 3, cacheWriteTokens: 1 },
|
|
57
|
+
outputTokenDetails: { textTokens: 5, reasoningTokens: 9 },
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(usage).toEqual({
|
|
63
|
+
totalTokens: 10,
|
|
64
|
+
inputTokens: 4,
|
|
65
|
+
outputTokens: 6,
|
|
66
|
+
reasoningTokens: 9,
|
|
67
|
+
cachedInputTokens: 3,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not fabricate zero splits when only totalTokens is present", () => {
|
|
72
|
+
const usage = getThreadMessageTokenUsage(
|
|
73
|
+
msg({ usage: { totalTokens: 12 } }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(usage).toEqual({ totalTokens: 12 });
|
|
77
|
+
expect(usage).not.toHaveProperty("inputTokens");
|
|
78
|
+
expect(usage).not.toHaveProperty("outputTokens");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("retains partial usage when only inputTokens is present", () => {
|
|
82
|
+
const usage = getThreadMessageTokenUsage(
|
|
83
|
+
msg({ usage: { inputTokens: 10 } }),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(usage).toEqual({ inputTokens: 10 });
|
|
87
|
+
expect(usage).not.toHaveProperty("totalTokens");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("retains partial usage when only outputTokens is present", () => {
|
|
91
|
+
const usage = getThreadMessageTokenUsage(
|
|
92
|
+
msg({ usage: { outputTokens: 4 } }),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(usage).toEqual({ outputTokens: 4 });
|
|
96
|
+
expect(usage).not.toHaveProperty("totalTokens");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("retains detail-only usage when only reasoning/cached tokens are present", () => {
|
|
100
|
+
const usage = getThreadMessageTokenUsage(
|
|
101
|
+
msg({ usage: { reasoningTokens: 7, cachedInputTokens: 2 } }),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(usage).toEqual({ reasoningTokens: 7, cachedInputTokens: 2 });
|
|
105
|
+
expect(usage).not.toHaveProperty("totalTokens");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("aggregates multi-step usage without inflating totals", () => {
|
|
109
|
+
const usage = getThreadMessageTokenUsage(
|
|
110
|
+
msg({
|
|
111
|
+
steps: [
|
|
112
|
+
{ usage: { inputTokens: 3, outputTokens: 2, reasoningTokens: 11 } },
|
|
113
|
+
{ usage: { inputTokens: 4, outputTokens: 1, reasoningTokens: 13 } },
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(usage).toEqual({
|
|
119
|
+
totalTokens: 10,
|
|
120
|
+
inputTokens: 7,
|
|
121
|
+
outputTokens: 3,
|
|
122
|
+
reasoningTokens: 24,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("omits totalTokens if any step lacks a computable total", () => {
|
|
127
|
+
const usage = getThreadMessageTokenUsage(
|
|
128
|
+
msg({
|
|
129
|
+
steps: [
|
|
130
|
+
{ usage: { inputTokens: 5, outputTokens: 5 } }, // total = 10
|
|
131
|
+
{ usage: { reasoningTokens: 10 } }, // total = undefined
|
|
132
|
+
],
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Sums known partials but omits the invalid total
|
|
137
|
+
expect(usage).toEqual({
|
|
138
|
+
inputTokens: 5,
|
|
139
|
+
outputTokens: 5,
|
|
140
|
+
reasoningTokens: 10,
|
|
141
|
+
});
|
|
142
|
+
expect(usage).not.toHaveProperty("totalTokens");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("aggregates totalTokens if all steps have a computable total", () => {
|
|
146
|
+
const usage = getThreadMessageTokenUsage(
|
|
147
|
+
msg({
|
|
148
|
+
steps: [
|
|
149
|
+
{ usage: { inputTokens: 5, outputTokens: 5 } }, // implicit total = 10
|
|
150
|
+
{ usage: { totalTokens: 15 } }, // explicit total = 15
|
|
151
|
+
],
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(usage).toEqual({
|
|
156
|
+
totalTokens: 25,
|
|
157
|
+
inputTokens: 5,
|
|
158
|
+
outputTokens: 5,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("omits totalTokens when one step has only total and another has only input", () => {
|
|
163
|
+
const usage = getThreadMessageTokenUsage(
|
|
164
|
+
msg({
|
|
165
|
+
steps: [{ usage: { totalTokens: 10 } }, { usage: { inputTokens: 3 } }],
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(usage).toEqual({
|
|
170
|
+
inputTokens: 3,
|
|
171
|
+
});
|
|
172
|
+
expect(usage).not.toHaveProperty("totalTokens");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("getThreadMessageTokenUsage", () => {
|
|
177
|
+
it("returns token usage from an earlier assistant message", () => {
|
|
178
|
+
const messages = [
|
|
179
|
+
{ role: "assistant", metadata: { usage: { totalTokens: 100 } } },
|
|
180
|
+
{ role: "user", metadata: {} },
|
|
181
|
+
{ role: "assistant", metadata: {} },
|
|
182
|
+
];
|
|
183
|
+
const usage = getThreadMessageTokenUsage(messages[0]);
|
|
184
|
+
|
|
185
|
+
expect(usage).toEqual({ totalTokens: 100 });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returns token usage from the newest assistant message", () => {
|
|
189
|
+
const messages = [
|
|
190
|
+
{ role: "assistant", metadata: { usage: { totalTokens: 100 } } },
|
|
191
|
+
{
|
|
192
|
+
role: "assistant",
|
|
193
|
+
metadata: { usage: { inputTokens: 40, outputTokens: 2 } },
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
const usage = getThreadMessageTokenUsage(messages[1]);
|
|
197
|
+
|
|
198
|
+
expect(usage).toEqual({
|
|
199
|
+
totalTokens: 42,
|
|
200
|
+
inputTokens: 40,
|
|
201
|
+
outputTokens: 2,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/// <reference types="@ulearning/ai-core/vue" preserve="true" />
|
|
2
|
+
import { computed, type ComputedRef } from "vue";
|
|
3
|
+
import { useAuiState } from "@ulearning/ai-store";
|
|
4
|
+
|
|
5
|
+
export type ThreadTokenUsage = {
|
|
6
|
+
totalTokens?: number;
|
|
7
|
+
inputTokens?: number;
|
|
8
|
+
outputTokens?: number;
|
|
9
|
+
reasoningTokens?: number;
|
|
10
|
+
cachedInputTokens?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface TokenUsageExtractableMessage {
|
|
14
|
+
role?: string;
|
|
15
|
+
metadata?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type UsageRecord = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
const USAGE_KEYS = [
|
|
21
|
+
"inputTokens",
|
|
22
|
+
"outputTokens",
|
|
23
|
+
"reasoningTokens",
|
|
24
|
+
"cachedInputTokens",
|
|
25
|
+
"totalTokens",
|
|
26
|
+
] as const satisfies (keyof ThreadTokenUsage)[];
|
|
27
|
+
|
|
28
|
+
function asRecord(value: unknown): UsageRecord | undefined {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return undefined;
|
|
31
|
+
return value as UsageRecord;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asPositiveTokenCount(value: unknown): number | undefined {
|
|
35
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function computeTotalTokens(usage: ThreadTokenUsage): number | undefined {
|
|
42
|
+
if (usage.totalTokens !== undefined) return usage.totalTokens;
|
|
43
|
+
if (usage.inputTokens !== undefined && usage.outputTokens !== undefined) {
|
|
44
|
+
return usage.inputTokens + usage.outputTokens;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeUsage(value: unknown): ThreadTokenUsage | undefined {
|
|
50
|
+
const record = asRecord(value);
|
|
51
|
+
if (!record) return undefined;
|
|
52
|
+
|
|
53
|
+
const result: ThreadTokenUsage = {};
|
|
54
|
+
let hasFields = false;
|
|
55
|
+
for (const key of USAGE_KEYS) {
|
|
56
|
+
const count = asPositiveTokenCount(record[key]);
|
|
57
|
+
if (count !== undefined) {
|
|
58
|
+
result[key] = count;
|
|
59
|
+
hasFields = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// AI SDK v7 moved these under token detail objects; v6 kept them top-level.
|
|
63
|
+
if (result.reasoningTokens === undefined) {
|
|
64
|
+
const count = asPositiveTokenCount(
|
|
65
|
+
asRecord(record.outputTokenDetails)?.reasoningTokens,
|
|
66
|
+
);
|
|
67
|
+
if (count !== undefined) {
|
|
68
|
+
result.reasoningTokens = count;
|
|
69
|
+
hasFields = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (result.cachedInputTokens === undefined) {
|
|
73
|
+
const count = asPositiveTokenCount(
|
|
74
|
+
asRecord(record.inputTokenDetails)?.cacheReadTokens,
|
|
75
|
+
);
|
|
76
|
+
if (count !== undefined) {
|
|
77
|
+
result.cachedInputTokens = count;
|
|
78
|
+
hasFields = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return hasFields ? result : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function withComputedTotal(
|
|
85
|
+
usage: ThreadTokenUsage,
|
|
86
|
+
): ThreadTokenUsage | undefined {
|
|
87
|
+
const totalTokens = computeTotalTokens(usage);
|
|
88
|
+
return { ...usage, ...(totalTokens !== undefined && { totalTokens }) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usageFromSteps(value: unknown): ThreadTokenUsage | undefined {
|
|
92
|
+
const steps = Array.isArray(value) ? value : [];
|
|
93
|
+
|
|
94
|
+
const sums: Record<string, number> = {};
|
|
95
|
+
const present: Record<string, boolean> = {};
|
|
96
|
+
let stepsWithUsage = 0;
|
|
97
|
+
let stepsWithComputableTotal = 0;
|
|
98
|
+
|
|
99
|
+
for (const step of steps) {
|
|
100
|
+
const usage = normalizeUsage(asRecord(step)?.usage);
|
|
101
|
+
if (!usage) continue;
|
|
102
|
+
stepsWithUsage++;
|
|
103
|
+
|
|
104
|
+
const stepTotal = computeTotalTokens(usage);
|
|
105
|
+
if (stepTotal !== undefined) {
|
|
106
|
+
sums.totalTokens = (sums.totalTokens ?? 0) + stepTotal;
|
|
107
|
+
stepsWithComputableTotal++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const key of USAGE_KEYS) {
|
|
111
|
+
if (key === "totalTokens") continue;
|
|
112
|
+
if (usage[key] !== undefined) {
|
|
113
|
+
sums[key] = (sums[key] ?? 0) + usage[key];
|
|
114
|
+
present[key] = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stepsWithUsage === 0) return undefined;
|
|
120
|
+
|
|
121
|
+
const result: ThreadTokenUsage = {};
|
|
122
|
+
if (stepsWithComputableTotal === stepsWithUsage) {
|
|
123
|
+
result.totalTokens = sums.totalTokens!;
|
|
124
|
+
}
|
|
125
|
+
for (const key of USAGE_KEYS) {
|
|
126
|
+
if (key === "totalTokens") continue;
|
|
127
|
+
if (present[key]) {
|
|
128
|
+
result[key] = sums[key]!;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getThreadMessageTokenUsage(
|
|
135
|
+
message: TokenUsageExtractableMessage | undefined,
|
|
136
|
+
): ThreadTokenUsage | undefined {
|
|
137
|
+
if (!message || message.role !== "assistant") return undefined;
|
|
138
|
+
|
|
139
|
+
const metadata = asRecord(message.metadata);
|
|
140
|
+
if (!metadata) return undefined;
|
|
141
|
+
|
|
142
|
+
const topLevelUsage = normalizeUsage(metadata.usage);
|
|
143
|
+
if (topLevelUsage) return withComputedTotal(topLevelUsage);
|
|
144
|
+
|
|
145
|
+
const customUsage = normalizeUsage(asRecord(metadata.custom)?.usage);
|
|
146
|
+
if (customUsage) return withComputedTotal(customUsage);
|
|
147
|
+
|
|
148
|
+
return usageFromSteps(metadata.steps);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findLatestMessageWithUsage(
|
|
152
|
+
messages: readonly TokenUsageExtractableMessage[] | undefined,
|
|
153
|
+
): TokenUsageExtractableMessage | undefined {
|
|
154
|
+
if (!messages) return undefined;
|
|
155
|
+
|
|
156
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
157
|
+
const message = messages[idx];
|
|
158
|
+
if (getThreadMessageTokenUsage(message)) {
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Reads token usage from the newest assistant message that reports any.
|
|
168
|
+
*
|
|
169
|
+
* A route attaches usage through the AI SDK's `messageMetadata` option. Because
|
|
170
|
+
* a thread message carries a fixed metadata shape, the converter moves every
|
|
171
|
+
* other key the route returns into `metadata.custom`, which is where this hook
|
|
172
|
+
* looks.
|
|
173
|
+
*/
|
|
174
|
+
export function useThreadTokenUsage(): ComputedRef<
|
|
175
|
+
ThreadTokenUsage | undefined
|
|
176
|
+
> {
|
|
177
|
+
const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));
|
|
178
|
+
return computed(() => getThreadMessageTokenUsage(msg.value));
|
|
179
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getExternalStoreMessages,
|
|
3
|
+
type ThreadMessage,
|
|
4
|
+
} from "@ulearning/ai-core";
|
|
5
|
+
import type { UIMessage } from "ai";
|
|
6
|
+
|
|
7
|
+
export const getVercelAIMessages = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
8
|
+
message: ThreadMessage,
|
|
9
|
+
) => {
|
|
10
|
+
return getExternalStoreMessages(message) as UI_MESSAGE[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { UIMessage } from "ai";
|
|
3
|
+
import { sliceMessagesUntil } from "./sliceMessagesUntil";
|
|
4
|
+
|
|
5
|
+
const msg = (id: string, role: UIMessage["role"]): UIMessage =>
|
|
6
|
+
({ id, role, parts: [] }) as any;
|
|
7
|
+
|
|
8
|
+
describe("sliceMessagesUntil", () => {
|
|
9
|
+
it("returns an empty array when the message id is null", () => {
|
|
10
|
+
expect(
|
|
11
|
+
sliceMessagesUntil([msg("u1", "user"), msg("a1", "assistant")], null),
|
|
12
|
+
).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("throws when the message id is not found", () => {
|
|
16
|
+
expect(() => sliceMessagesUntil([msg("u1", "user")], "missing")).toThrow(
|
|
17
|
+
'sliceMessagesUntil: Message "missing" not found in AI SDK messages. This is likely an internal bug in assistant-ui.',
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("slices up to and including the target when no assistant follows", () => {
|
|
22
|
+
const messages = [
|
|
23
|
+
msg("u1", "user"),
|
|
24
|
+
msg("a1", "assistant"),
|
|
25
|
+
msg("u2", "user"),
|
|
26
|
+
];
|
|
27
|
+
expect(sliceMessagesUntil(messages, "u2").map((m) => m.id)).toEqual([
|
|
28
|
+
"u1",
|
|
29
|
+
"a1",
|
|
30
|
+
"u2",
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("stops at the target even when assistant messages follow it", () => {
|
|
35
|
+
const messages = [
|
|
36
|
+
msg("u1", "user"),
|
|
37
|
+
msg("a1", "assistant"),
|
|
38
|
+
msg("a2", "assistant"),
|
|
39
|
+
msg("u2", "user"),
|
|
40
|
+
];
|
|
41
|
+
expect(sliceMessagesUntil(messages, "u1").map((m) => m.id)).toEqual(["u1"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns the whole array when the target is the last message", () => {
|
|
45
|
+
const messages = [msg("u1", "user"), msg("a1", "assistant")];
|
|
46
|
+
expect(sliceMessagesUntil(messages, "a1").map((m) => m.id)).toEqual([
|
|
47
|
+
"u1",
|
|
48
|
+
"a1",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
export const sliceMessagesUntil = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
4
|
+
messages: UI_MESSAGE[],
|
|
5
|
+
messageId: string | null,
|
|
6
|
+
) => {
|
|
7
|
+
if (messageId == null) return [];
|
|
8
|
+
|
|
9
|
+
const messageIdx = messages.findIndex((m) => m.id === messageId);
|
|
10
|
+
if (messageIdx === -1)
|
|
11
|
+
throw new Error(
|
|
12
|
+
`sliceMessagesUntil: Message "${messageId}" not found in AI SDK messages. This is likely an internal bug in assistant-ui.`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return messages.slice(0, messageIdx + 1);
|
|
16
|
+
};
|