@gram-ai/elements 1.33.2 → 1.34.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/dist/components/ui/time-range-picker.d.ts +17 -1
- package/dist/components/ui/time-range-picker.test.d.ts +1 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.js +19 -16
- package/dist/hooks/useGramThreadListAdapter.d.ts +13 -0
- package/dist/{index-CGoLfO5p.js → index-B5lZrrO2.js} +52 -37
- package/dist/index-B5lZrrO2.js.map +1 -0
- package/dist/{index-reVrRxP1.js → index-BFU6NvbL.js} +6854 -6807
- package/dist/index-BFU6NvbL.js.map +1 -0
- package/dist/{index-DAWGW1Nj.cjs → index-C08dvTEo.cjs} +43 -43
- package/dist/index-C08dvTEo.cjs.map +1 -0
- package/dist/index-DzZ1-jQY.cjs +194 -0
- package/dist/index-DzZ1-jQY.cjs.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/lib/messageConverter.d.ts +58 -8
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/utils.d.ts +2 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/dist/{profiler-B3tfiOx4.cjs → profiler-BRnyr1GA.cjs} +2 -2
- package/dist/{profiler-B3tfiOx4.cjs.map → profiler-BRnyr1GA.cjs.map} +1 -1
- package/dist/{profiler-noho3NG9.js → profiler-KLSTpp6I.js} +2 -2
- package/dist/{profiler-noho3NG9.js.map → profiler-KLSTpp6I.js.map} +1 -1
- package/dist/{startRecording-mkmig-2n.js → startRecording-BfxB1xxR.js} +2 -2
- package/dist/{startRecording-mkmig-2n.js.map → startRecording-BfxB1xxR.js.map} +1 -1
- package/dist/{startRecording-7Oy6wM18.cjs → startRecording-CKx-YWbq.cjs} +2 -2
- package/dist/{startRecording-7Oy6wM18.cjs.map → startRecording-CKx-YWbq.cjs.map} +1 -1
- package/dist/types/index.d.ts +64 -1
- package/package.json +4 -2
- package/src/components/assistant-ui/thread.tsx +7 -3
- package/src/components/ui/time-range-picker.test.ts +57 -0
- package/src/components/ui/time-range-picker.tsx +29 -4
- package/src/contexts/ElementsProvider.tsx +57 -8
- package/src/hooks/useGramThreadListAdapter.tsx +68 -6
- package/src/index.ts +16 -0
- package/src/lib/cassette.ts +1 -19
- package/src/lib/contextCompaction.test.ts +2 -2
- package/src/lib/contextCompaction.ts +20 -8
- package/src/lib/messageConverter.ts +94 -56
- package/src/lib/models.ts +19 -7
- package/src/lib/utils.ts +20 -0
- package/src/types/index.ts +73 -1
- package/dist/index-BCV7Zf9E.cjs +0 -194
- package/dist/index-BCV7Zf9E.cjs.map +0 -1
- package/dist/index-CGoLfO5p.js.map +0 -1
- package/dist/index-DAWGW1Nj.cjs.map +0 -1
- package/dist/index-reVrRxP1.js.map +0 -1
|
@@ -67,6 +67,19 @@ import { ElementsContext } from "./contexts";
|
|
|
67
67
|
import { ToolApprovalProvider } from "./ToolApprovalContext";
|
|
68
68
|
import { ToolExecutionProvider } from "./ToolExecutionContext";
|
|
69
69
|
|
|
70
|
+
// Reads the active local thread id from the runtime's threads store. Reaches
|
|
71
|
+
// into an assistant-ui internal that isn't part of the public type, so it's
|
|
72
|
+
// isolated here as the single point of breakage if the API moves.
|
|
73
|
+
function getActiveLocalThreadId(
|
|
74
|
+
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const threadsState = (runtimeRef.current as any)?.threads?.getState?.();
|
|
78
|
+
return (threadsState?.mainThreadId ?? threadsState?.threadIds?.[0]) as
|
|
79
|
+
| string
|
|
80
|
+
| undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* Extracts executable tools from frontend tool definitions.
|
|
72
85
|
* Frontend tools created via defineFrontendTool have an unstable_tool property
|
|
@@ -276,8 +289,10 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
276
289
|
// in a way that's accessible from the transport's sendMessages function.
|
|
277
290
|
const currentRemoteIdRef = useRef<string | null>(null);
|
|
278
291
|
|
|
279
|
-
// Create chat transport configuration
|
|
280
|
-
|
|
292
|
+
// Create chat transport configuration. This is the built-in client-side
|
|
293
|
+
// streaming transport; a consumer can override it via config.transport (see
|
|
294
|
+
// below) to route the conversation through a server-side assistant instead.
|
|
295
|
+
const defaultTransport = useMemo<ChatTransport<UIMessage>>(
|
|
281
296
|
() => ({
|
|
282
297
|
sendMessages: async ({ messages, abortSignal }) => {
|
|
283
298
|
const usingCustomModel = !!config.languageModel;
|
|
@@ -298,12 +313,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
298
313
|
// chatId is already set correctly from the synced ref
|
|
299
314
|
} else if (isLocalThreadId(chatId) || !chatId) {
|
|
300
315
|
// For local thread IDs or no ID, check/generate UUID mapping
|
|
301
|
-
|
|
302
|
-
const runtimeAny = runtimeRef.current as any;
|
|
303
|
-
const threadsState = runtimeAny?.threads?.getState?.();
|
|
304
|
-
const localThreadId = (threadsState?.mainThreadId ??
|
|
305
|
-
threadsState?.threadIds?.[0]) as string | undefined;
|
|
306
|
-
|
|
316
|
+
const localThreadId = getActiveLocalThreadId(runtimeRef);
|
|
307
317
|
const lookupKey = chatId ?? localThreadId;
|
|
308
318
|
if (lookupKey) {
|
|
309
319
|
const existingUuid = localIdToUuidMapRef.current.get(lookupKey);
|
|
@@ -521,6 +531,43 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
521
531
|
],
|
|
522
532
|
);
|
|
523
533
|
|
|
534
|
+
// A consumer-supplied transport (e.g. a server-side assistant transport) takes
|
|
535
|
+
// precedence over the built-in client-side one. It may be a ChatTransport or a
|
|
536
|
+
// factory: a factory is invoked here, inside the provider, with a getChatId()
|
|
537
|
+
// sourced from the synced thread state, so the transport can read the active
|
|
538
|
+
// chat id at send time without reaching into Elements internals. Local
|
|
539
|
+
// (unpersisted) thread ids read as null so the transport can treat them as a
|
|
540
|
+
// brand-new conversation.
|
|
541
|
+
const getChatId = useCallback(() => {
|
|
542
|
+
const id = currentRemoteIdRef.current;
|
|
543
|
+
return id && !isLocalThreadId(id) ? id : null;
|
|
544
|
+
}, []);
|
|
545
|
+
// Capture the active local thread identity now and return a bind function
|
|
546
|
+
// closing over it. Consumer transports call this at the start of
|
|
547
|
+
// `sendMessages`; once a server-minted chat id is known, invoking the
|
|
548
|
+
// returned function reconciles the captured thread to it — the same
|
|
549
|
+
// reconciliation the built-in transport does inline when it generates an id.
|
|
550
|
+
// Closing over the captured id (instead of re-reading active state at bind
|
|
551
|
+
// time) is what makes a thread switch or a parallel send on another thread
|
|
552
|
+
// during the round-trip safe.
|
|
553
|
+
const adoptChatId = useCallback(() => {
|
|
554
|
+
const capturedLocalThreadId = getActiveLocalThreadId(runtimeRef);
|
|
555
|
+
return (chatId: string) => {
|
|
556
|
+
if (capturedLocalThreadId) {
|
|
557
|
+
localIdToUuidMapRef.current.set(capturedLocalThreadId, chatId);
|
|
558
|
+
}
|
|
559
|
+
currentRemoteIdRef.current = chatId;
|
|
560
|
+
mcpHeaders["Gram-Chat-ID"] = chatId;
|
|
561
|
+
setCurrentChatId(chatId);
|
|
562
|
+
};
|
|
563
|
+
}, [mcpHeaders, setCurrentChatId]);
|
|
564
|
+
const transport = useMemo<ChatTransport<UIMessage>>(() => {
|
|
565
|
+
if (typeof config.transport === "function") {
|
|
566
|
+
return config.transport({ getChatId, adoptChatId });
|
|
567
|
+
}
|
|
568
|
+
return config.transport ?? defaultTransport;
|
|
569
|
+
}, [config.transport, defaultTransport, getChatId, adoptChatId]);
|
|
570
|
+
|
|
524
571
|
const historyEnabled = config.history?.enabled ?? false;
|
|
525
572
|
|
|
526
573
|
// Shared context value for ElementsContext
|
|
@@ -658,6 +705,8 @@ const ElementsProviderWithHistory = ({
|
|
|
658
705
|
apiUrl,
|
|
659
706
|
headers,
|
|
660
707
|
localIdToUuidMap,
|
|
708
|
+
threadListFilters: contextValue?.config.history?.threadListFilters,
|
|
709
|
+
deferThreadIdMinting: contextValue?.config.history?.deferThreadIdMinting,
|
|
661
710
|
});
|
|
662
711
|
const initialThreadId = contextValue?.config.history?.initialThreadId;
|
|
663
712
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
convertGramMessagesToExported,
|
|
14
14
|
convertGramMessagesToUIMessages,
|
|
15
15
|
} from "@/lib/messageConverter";
|
|
16
|
+
import { sleep } from "@/lib/utils";
|
|
16
17
|
import {
|
|
17
18
|
useCallback,
|
|
18
19
|
useEffect,
|
|
@@ -37,11 +38,46 @@ export function isLocalThreadId(threadId: string | null | undefined): boolean {
|
|
|
37
38
|
return !!threadId?.startsWith(LOCAL_THREAD_ID_PREFIX);
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Polls the shared local→remote id map until the transport assigns an id for
|
|
43
|
+
* `threadId`, or a deadline passes. Used in deferred-minting mode so a new
|
|
44
|
+
* thread adopts the backend-minted chat id instead of a client-generated one.
|
|
45
|
+
* The timeout is generous to tolerate cold serverless boots on the first send.
|
|
46
|
+
*/
|
|
47
|
+
async function waitForMappedId(
|
|
48
|
+
map: Map<string, string> | undefined,
|
|
49
|
+
threadId: string,
|
|
50
|
+
timeoutMs = 30_000,
|
|
51
|
+
intervalMs = 50,
|
|
52
|
+
): Promise<string | undefined> {
|
|
53
|
+
if (!map) return undefined;
|
|
54
|
+
const deadline = Date.now() + timeoutMs;
|
|
55
|
+
for (;;) {
|
|
56
|
+
const id = map.get(threadId);
|
|
57
|
+
if (id) return id;
|
|
58
|
+
if (Date.now() >= deadline) return undefined;
|
|
59
|
+
await sleep(intervalMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
export interface ThreadListAdapterOptions {
|
|
41
64
|
apiUrl: string;
|
|
42
65
|
headers: Record<string, string>;
|
|
43
66
|
/** Map to translate local thread IDs to UUIDs (shared with transport) */
|
|
44
67
|
localIdToUuidMap?: Map<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* Extra query parameters forwarded to `chat.list` to filter which threads are
|
|
70
|
+
* listed. Opaque to the adapter — the consumer chooses the keys.
|
|
71
|
+
*/
|
|
72
|
+
threadListFilters?: Record<string, string>;
|
|
73
|
+
/**
|
|
74
|
+
* Don't client-mint a chat id for a brand-new thread. When true, `initialize`
|
|
75
|
+
* waits for the transport to assign the id (via the shared `localIdToUuidMap`,
|
|
76
|
+
* e.g. a server-minted id reported through the transport context's
|
|
77
|
+
* `adoptChatId` bind closure) instead of generating one with
|
|
78
|
+
* `crypto.randomUUID()`. Use this when the backend owns chat-id creation.
|
|
79
|
+
*/
|
|
80
|
+
deferThreadIdMinting?: boolean;
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
interface ListChatsResponse {
|
|
@@ -213,12 +249,14 @@ export function useGramThreadListAdapter(
|
|
|
213
249
|
|
|
214
250
|
async list() {
|
|
215
251
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
252
|
+
const { apiUrl, headers, threadListFilters } = optionsRef.current;
|
|
253
|
+
const qs = threadListFilters
|
|
254
|
+
? new URLSearchParams(threadListFilters).toString()
|
|
255
|
+
: "";
|
|
256
|
+
const listUrl = qs
|
|
257
|
+
? `${apiUrl}/rpc/chat.list?${qs}`
|
|
258
|
+
: `${apiUrl}/rpc/chat.list`;
|
|
259
|
+
const response = await fetch(listUrl, { headers });
|
|
222
260
|
|
|
223
261
|
if (!response.ok) {
|
|
224
262
|
console.error("Failed to list chats:", response.status);
|
|
@@ -252,6 +290,30 @@ export function useGramThreadListAdapter(
|
|
|
252
290
|
externalId: existingUuid,
|
|
253
291
|
};
|
|
254
292
|
}
|
|
293
|
+
// When the backend owns chat-id creation, don't client-mint: wait for
|
|
294
|
+
// the transport to report the server-minted id (it assigns it during
|
|
295
|
+
// the first send via the adoptChatId bind closure, populating the
|
|
296
|
+
// shared map — the same path the built-in transport uses).
|
|
297
|
+
if (optionsRef.current.deferThreadIdMinting) {
|
|
298
|
+
const mappedUuid = await waitForMappedId(
|
|
299
|
+
optionsRef.current.localIdToUuidMap,
|
|
300
|
+
threadId,
|
|
301
|
+
);
|
|
302
|
+
if (mappedUuid) {
|
|
303
|
+
return {
|
|
304
|
+
remoteId: mappedUuid,
|
|
305
|
+
externalId: mappedUuid,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// Falling through to crypto.randomUUID() here would defeat deferred
|
|
309
|
+
// minting: the client id would race the server-minted one reported
|
|
310
|
+
// later via the adoptChatId bind closure, leaving runtime state
|
|
311
|
+
// and the local→remote map disagreeing. Surface the failure to
|
|
312
|
+
// the user instead.
|
|
313
|
+
throw new Error(
|
|
314
|
+
"Backend did not mint a chat id in time — first send may have failed or is still in flight. Retry the send.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
255
317
|
// Otherwise generate a new one and store it
|
|
256
318
|
const uuid = crypto.randomUUID();
|
|
257
319
|
optionsRef.current.localIdToUuidMap?.set(threadId, uuid);
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type {
|
|
|
56
56
|
Dimension,
|
|
57
57
|
Dimensions,
|
|
58
58
|
ElementsConfig,
|
|
59
|
+
ElementsTransportContext,
|
|
60
|
+
ElementsTransportFactory,
|
|
59
61
|
ErrorTrackingConfigOption,
|
|
60
62
|
GetSessionFn,
|
|
61
63
|
HistoryConfig,
|
|
@@ -80,6 +82,20 @@ export type {
|
|
|
80
82
|
|
|
81
83
|
export { MODELS } from "./lib/models";
|
|
82
84
|
|
|
85
|
+
// Chat-message conversion — for consumers building a custom transport against
|
|
86
|
+
// the Gram chat service (e.g. the dashboard's server-assistant transport).
|
|
87
|
+
export {
|
|
88
|
+
convertGramMessagesToUIMessages,
|
|
89
|
+
convertGramMessagesToExported,
|
|
90
|
+
} from "@/lib/messageConverter";
|
|
91
|
+
|
|
92
|
+
export { sleep } from "@/lib/utils";
|
|
93
|
+
export type {
|
|
94
|
+
GramChat,
|
|
95
|
+
GramChatMessage,
|
|
96
|
+
GramChatOverview,
|
|
97
|
+
} from "@/lib/messageConverter";
|
|
98
|
+
|
|
83
99
|
export type { Plugin } from "./types/plugins";
|
|
84
100
|
|
|
85
101
|
// Time Range Picker
|
package/src/lib/cassette.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { createUIMessageStream, type ChatTransport, type UIMessage } from "ai";
|
|
14
14
|
import type { ThreadMessage } from "@assistant-ui/react";
|
|
15
|
+
import { sleep } from "@/lib/utils";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Cassette types
|
|
@@ -112,25 +113,6 @@ export function recordCassette(messages: readonly ThreadMessage[]): Cassette {
|
|
|
112
113
|
// Playback: Cassette → ChatTransport
|
|
113
114
|
// ---------------------------------------------------------------------------
|
|
114
115
|
|
|
115
|
-
/** Sleep that respects AbortSignal for clean cancellation. */
|
|
116
|
-
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
if (signal?.aborted) {
|
|
119
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const timeout = setTimeout(resolve, ms);
|
|
123
|
-
signal?.addEventListener(
|
|
124
|
-
"abort",
|
|
125
|
-
() => {
|
|
126
|
-
clearTimeout(timeout);
|
|
127
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
128
|
-
},
|
|
129
|
-
{ once: true },
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
116
|
/**
|
|
135
117
|
* Creates a ChatTransport that replays pre-recorded assistant messages
|
|
136
118
|
* from a cassette. Each call to `sendMessages` (triggered by a user message
|
|
@@ -38,8 +38,8 @@ describe("getModelContextLimit", () => {
|
|
|
38
38
|
expect(getModelContextLimit("anthropic/claude-sonnet-4.6")).toBe(1_000_000);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("returns known mapping for Claude 4
|
|
42
|
-
expect(getModelContextLimit("anthropic/claude-sonnet-4")).toBe(
|
|
41
|
+
it("returns known mapping for Claude Sonnet 4", () => {
|
|
42
|
+
expect(getModelContextLimit("anthropic/claude-sonnet-4")).toBe(1_000_000);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it("returns DEFAULT_CONTEXT_LIMIT for unknown models", () => {
|
|
@@ -32,39 +32,51 @@ export const DEFAULT_CONTEXT_LIMIT = 200_000;
|
|
|
32
32
|
*/
|
|
33
33
|
const MODEL_CONTEXT_LIMITS: Partial<Record<KnownModelId, number>> = {
|
|
34
34
|
// Anthropic (1M tier where available, else 200K)
|
|
35
|
+
"anthropic/claude-opus-4.8": 1_000_000,
|
|
36
|
+
"anthropic/claude-opus-4.7": 1_000_000,
|
|
35
37
|
"anthropic/claude-opus-4.6": 1_000_000,
|
|
36
|
-
"anthropic/claude-opus-4.5":
|
|
37
|
-
"anthropic/claude-opus-4.1": 200_000,
|
|
38
|
+
"anthropic/claude-opus-4.5": 200_000,
|
|
38
39
|
"anthropic/claude-sonnet-4.6": 1_000_000,
|
|
39
40
|
"anthropic/claude-sonnet-4.5": 1_000_000,
|
|
40
|
-
"anthropic/claude-sonnet-4":
|
|
41
|
+
"anthropic/claude-sonnet-4": 1_000_000,
|
|
41
42
|
"anthropic/claude-haiku-4.5": 200_000,
|
|
42
43
|
|
|
43
44
|
// OpenAI
|
|
44
|
-
"openai/gpt-5.
|
|
45
|
+
"openai/gpt-5.5": 1_000_000,
|
|
46
|
+
"openai/gpt-5.5-pro": 1_000_000,
|
|
47
|
+
"openai/gpt-5.4": 1_000_000,
|
|
45
48
|
"openai/gpt-5.4-mini": 400_000,
|
|
49
|
+
"openai/gpt-5.4-nano": 400_000,
|
|
50
|
+
"openai/gpt-5.3-codex": 400_000,
|
|
46
51
|
"openai/gpt-5.1": 400_000,
|
|
47
|
-
"openai/gpt-5.1-codex": 400_000,
|
|
48
52
|
"openai/gpt-5": 400_000,
|
|
49
53
|
"openai/gpt-4.1": 1_000_000,
|
|
50
54
|
"openai/o4-mini": 200_000,
|
|
51
55
|
"openai/o3": 200_000,
|
|
52
56
|
|
|
53
57
|
// Google
|
|
58
|
+
"google/gemini-3.5-flash": 1_000_000,
|
|
54
59
|
"google/gemini-3.1-pro-preview": 1_000_000,
|
|
60
|
+
"google/gemini-3.1-flash-lite": 1_000_000,
|
|
55
61
|
"google/gemini-2.5-pro": 1_000_000,
|
|
56
62
|
"google/gemini-2.5-flash": 1_000_000,
|
|
57
63
|
|
|
58
64
|
// Others
|
|
65
|
+
"deepseek/deepseek-v4-pro": 1_000_000,
|
|
66
|
+
"deepseek/deepseek-v4-flash": 1_000_000,
|
|
59
67
|
"deepseek/deepseek-r1": 128_000,
|
|
60
68
|
"deepseek/deepseek-v3.2": 128_000,
|
|
61
69
|
"meta-llama/llama-4-maverick": 1_000_000,
|
|
62
|
-
"x-ai/grok-4":
|
|
70
|
+
"x-ai/grok-4.3": 1_000_000,
|
|
71
|
+
"x-ai/grok-4.20": 2_000_000,
|
|
72
|
+
"qwen/qwen3.7-max": 1_000_000,
|
|
63
73
|
"qwen/qwen3-coder": 256_000,
|
|
64
|
-
"moonshotai/kimi-k2.
|
|
74
|
+
"moonshotai/kimi-k2.6": 256_000,
|
|
75
|
+
"moonshotai/kimi-k2.5": 256_000,
|
|
76
|
+
"mistralai/mistral-medium-3-5": 256_000,
|
|
65
77
|
"mistralai/mistral-medium-3.1": 128_000,
|
|
66
78
|
"mistralai/codestral-2508": 256_000,
|
|
67
|
-
"mistralai/devstral-
|
|
79
|
+
"mistralai/devstral-2512": 256_000,
|
|
68
80
|
};
|
|
69
81
|
|
|
70
82
|
/**
|
|
@@ -19,23 +19,69 @@ import type {
|
|
|
19
19
|
ThreadAssistantMessagePart,
|
|
20
20
|
TextMessagePart,
|
|
21
21
|
} from "@assistant-ui/react";
|
|
22
|
-
import type {
|
|
23
|
-
Message,
|
|
24
|
-
UserMessage,
|
|
25
|
-
AssistantMessage,
|
|
26
|
-
ToolResponseMessage,
|
|
27
|
-
} from "@openrouter/sdk/models";
|
|
28
22
|
import { UIMessage } from "ai";
|
|
29
23
|
|
|
30
24
|
/**
|
|
31
|
-
*
|
|
32
|
-
|
|
25
|
+
* A single text part of a multi-modal chat message.
|
|
26
|
+
*/
|
|
27
|
+
export interface GramChatTextPart {
|
|
28
|
+
type: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A single image part of a multi-modal chat message. The wire shape mirrors the
|
|
34
|
+
* upstream OpenAI/OpenRouter chat schema (`image_url.url`) so the converter can
|
|
35
|
+
* read it without normalisation.
|
|
36
|
+
*/
|
|
37
|
+
export interface GramChatImagePart {
|
|
38
|
+
type: "image_url";
|
|
39
|
+
image_url?: { url?: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single audio part of a multi-modal chat message. Mirrors the OpenAI/OpenRouter
|
|
44
|
+
* `input_audio.{data, format}` shape on the wire.
|
|
45
|
+
*/
|
|
46
|
+
export interface GramChatAudioPart {
|
|
47
|
+
type: "input_audio";
|
|
48
|
+
input_audio?: { data?: string; format?: string };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Content part of a multi-modal chat message.
|
|
53
|
+
*/
|
|
54
|
+
export type GramChatContentPart =
|
|
55
|
+
| GramChatTextPart
|
|
56
|
+
| GramChatImagePart
|
|
57
|
+
| GramChatAudioPart;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Content of a chat message — either a plain string or an array of parts for
|
|
61
|
+
* multi-modal messages.
|
|
62
|
+
*/
|
|
63
|
+
export type GramChatContent = string | GramChatContentPart[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Represents a chat message from the Gram API. Only fields actually surfaced
|
|
67
|
+
* through Elements' public converters are modelled; provider-specific extras
|
|
68
|
+
* remain on the wire shape but are intentionally not part of the contract.
|
|
69
|
+
*
|
|
70
|
+
* `tool_calls` is the JSON-encoded string the Gram chat service stores on
|
|
71
|
+
* assistant rows; `tool_call_id` is the id the corresponding tool-response row
|
|
72
|
+
* carries when `role === "tool"`.
|
|
33
73
|
*/
|
|
34
|
-
export
|
|
74
|
+
export interface GramChatMessage {
|
|
35
75
|
id: string;
|
|
36
76
|
model: string;
|
|
37
77
|
created_at: Date | string;
|
|
38
|
-
|
|
78
|
+
role: "system" | "developer" | "user" | "assistant" | "tool";
|
|
79
|
+
content?: GramChatContent | null;
|
|
80
|
+
name?: string;
|
|
81
|
+
tool_calls?: string;
|
|
82
|
+
tool_call_id?: string;
|
|
83
|
+
reasoning?: string | null;
|
|
84
|
+
}
|
|
39
85
|
|
|
40
86
|
/**
|
|
41
87
|
* Represents a chat from the Gram API.
|
|
@@ -96,29 +142,23 @@ function buildUserContentParts(msg: GramChatMessage): ThreadUserMessagePart[] {
|
|
|
96
142
|
text: item.text,
|
|
97
143
|
});
|
|
98
144
|
break;
|
|
99
|
-
case "image_url":
|
|
145
|
+
case "image_url": {
|
|
146
|
+
const url = item.image_url?.url ?? "";
|
|
100
147
|
parts.push({
|
|
101
148
|
type: "image",
|
|
102
|
-
image:
|
|
103
|
-
string,
|
|
104
|
-
"Fixed by switching to Gram TS SDK."
|
|
105
|
-
>,
|
|
149
|
+
image: url,
|
|
106
150
|
});
|
|
107
151
|
break;
|
|
152
|
+
}
|
|
108
153
|
case "input_audio": {
|
|
109
|
-
const format =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
>;
|
|
113
|
-
if (format === "mp3" || format === "wav") {
|
|
154
|
+
const format = item.input_audio?.format;
|
|
155
|
+
const data = item.input_audio?.data;
|
|
156
|
+
if ((format === "mp3" || format === "wav") && data) {
|
|
114
157
|
parts.push({
|
|
115
158
|
type: "audio",
|
|
116
159
|
audio: {
|
|
117
|
-
data
|
|
118
|
-
|
|
119
|
-
"Fixed by switching to Gram TS SDK."
|
|
120
|
-
>,
|
|
121
|
-
format: format,
|
|
160
|
+
data,
|
|
161
|
+
format,
|
|
122
162
|
},
|
|
123
163
|
});
|
|
124
164
|
}
|
|
@@ -155,12 +195,7 @@ function buildAssistantContentParts(
|
|
|
155
195
|
});
|
|
156
196
|
}
|
|
157
197
|
|
|
158
|
-
|
|
159
|
-
string | undefined,
|
|
160
|
-
"Fixed by switching to Gram TS SDK."
|
|
161
|
-
>;
|
|
162
|
-
|
|
163
|
-
let toolCalls = tryParseJSON(toolCallsJSON || "[]");
|
|
198
|
+
let toolCalls = tryParseJSON(msg.tool_calls || "[]");
|
|
164
199
|
if (!Array.isArray(toolCalls)) {
|
|
165
200
|
console.warn("Invalid tool_calls format, expected an array.");
|
|
166
201
|
toolCalls = [];
|
|
@@ -280,8 +315,10 @@ function convertGramMessageToThreadMessage(
|
|
|
280
315
|
* Converts an array of Gram ChatMessages to an ExportedMessageRepository.
|
|
281
316
|
* Creates parent-child relationships based on message order.
|
|
282
317
|
*
|
|
283
|
-
* Note:
|
|
284
|
-
*
|
|
318
|
+
* Note: system, developer, and tool messages are filtered out. assistant-ui's
|
|
319
|
+
* exported format only models user/assistant turns; system/developer rows are
|
|
320
|
+
* pre-prompt instructions the UI doesn't render, and tool rows are folded into
|
|
321
|
+
* the preceding assistant message as `tool-call` parts via `tool_calls`.
|
|
285
322
|
*/
|
|
286
323
|
export function convertGramMessagesToExported(
|
|
287
324
|
messages: GramChatMessage[],
|
|
@@ -294,8 +331,11 @@ export function convertGramMessagesToExported(
|
|
|
294
331
|
let prevId: string | null = null;
|
|
295
332
|
|
|
296
333
|
for (const msg of messages) {
|
|
297
|
-
|
|
298
|
-
|
|
334
|
+
if (
|
|
335
|
+
msg.role === "system" ||
|
|
336
|
+
msg.role === "developer" ||
|
|
337
|
+
msg.role === "tool"
|
|
338
|
+
) {
|
|
299
339
|
continue;
|
|
300
340
|
}
|
|
301
341
|
|
|
@@ -322,17 +362,17 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
322
362
|
return { messages: [], headId: null };
|
|
323
363
|
}
|
|
324
364
|
|
|
325
|
-
const toolCallResults = new Map<string,
|
|
365
|
+
const toolCallResults = new Map<string, GramChatMessage>();
|
|
326
366
|
for (const msg of messages) {
|
|
327
367
|
if (msg.role !== "tool") {
|
|
328
368
|
continue;
|
|
329
369
|
}
|
|
330
|
-
const id =
|
|
370
|
+
const id = msg.tool_call_id;
|
|
331
371
|
if (typeof id !== "string") {
|
|
332
372
|
continue;
|
|
333
373
|
}
|
|
334
374
|
|
|
335
|
-
toolCallResults.set(id, msg
|
|
375
|
+
toolCallResults.set(id, msg);
|
|
336
376
|
}
|
|
337
377
|
|
|
338
378
|
const uiMessages: { parentId: string | null; message: UIMessage }[] = [];
|
|
@@ -415,9 +455,19 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
415
455
|
};
|
|
416
456
|
}
|
|
417
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Parsed shape of a single entry inside an assistant message's `tool_calls`
|
|
460
|
+
* JSON string. Mirrors the OpenAI/OpenRouter tool-call wire format.
|
|
461
|
+
*/
|
|
462
|
+
interface GramToolCall {
|
|
463
|
+
id: string;
|
|
464
|
+
type?: "function";
|
|
465
|
+
function?: { name?: string; arguments?: string | Record<string, unknown> };
|
|
466
|
+
}
|
|
467
|
+
|
|
418
468
|
export function convertGramMessagePartsToUIMessageParts(
|
|
419
|
-
msg:
|
|
420
|
-
toolResults: Map<string,
|
|
469
|
+
msg: GramChatMessage,
|
|
470
|
+
toolResults: Map<string, GramChatMessage>,
|
|
421
471
|
seenToolCallIds?: Set<string>,
|
|
422
472
|
): UIMessage["parts"] {
|
|
423
473
|
const uiparts: UIMessage["parts"] = [];
|
|
@@ -440,10 +490,7 @@ export function convertGramMessagePartsToUIMessageParts(
|
|
|
440
490
|
break;
|
|
441
491
|
}
|
|
442
492
|
case "image_url": {
|
|
443
|
-
const url =
|
|
444
|
-
string | undefined,
|
|
445
|
-
"Fixed by switching to Gram TS SDK."
|
|
446
|
-
>;
|
|
493
|
+
const url = p.image_url?.url;
|
|
447
494
|
if (!url) {
|
|
448
495
|
break;
|
|
449
496
|
}
|
|
@@ -456,10 +503,7 @@ export function convertGramMessagePartsToUIMessageParts(
|
|
|
456
503
|
break;
|
|
457
504
|
}
|
|
458
505
|
case "input_audio": {
|
|
459
|
-
const url =
|
|
460
|
-
string | undefined,
|
|
461
|
-
"Fixed by switching to Gram TS SDK."
|
|
462
|
-
>;
|
|
506
|
+
const url = p.input_audio?.data;
|
|
463
507
|
if (!url) {
|
|
464
508
|
break;
|
|
465
509
|
}
|
|
@@ -481,14 +525,8 @@ export function convertGramMessagePartsToUIMessageParts(
|
|
|
481
525
|
});
|
|
482
526
|
}
|
|
483
527
|
|
|
484
|
-
if (msg.role === "assistant" &&
|
|
485
|
-
|
|
486
|
-
string,
|
|
487
|
-
"Fixed by switching to Gram TS SDK."
|
|
488
|
-
>;
|
|
489
|
-
let toolCalls = tryParseJSON<AssistantMessage["toolCalls"]>(
|
|
490
|
-
toolCallsJSON || "[]",
|
|
491
|
-
);
|
|
528
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
529
|
+
let toolCalls = tryParseJSON<GramToolCall[]>(msg.tool_calls || "[]");
|
|
492
530
|
if (!Array.isArray(toolCalls)) {
|
|
493
531
|
console.warn("Invalid tool_calls format, expected an array.");
|
|
494
532
|
toolCalls = [];
|
package/src/lib/models.ts
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
// List of openrouter models available to the user
|
|
2
2
|
// This list should be updated to match the model whitelist on the backend side.
|
|
3
3
|
export const MODELS = [
|
|
4
|
-
"anthropic/claude-opus-4.
|
|
4
|
+
"anthropic/claude-opus-4.8",
|
|
5
|
+
"anthropic/claude-opus-4.7",
|
|
5
6
|
"anthropic/claude-sonnet-4.6",
|
|
6
7
|
"anthropic/claude-sonnet-4.5",
|
|
8
|
+
"anthropic/claude-opus-4.6",
|
|
7
9
|
"anthropic/claude-opus-4.5",
|
|
8
10
|
"anthropic/claude-haiku-4.5",
|
|
9
|
-
"anthropic/claude-opus-4.1",
|
|
10
11
|
"anthropic/claude-sonnet-4",
|
|
12
|
+
"openai/gpt-5.5",
|
|
13
|
+
"openai/gpt-5.5-pro",
|
|
11
14
|
"openai/gpt-5.4",
|
|
12
15
|
"openai/gpt-5.4-mini",
|
|
16
|
+
"openai/gpt-5.4-nano",
|
|
17
|
+
"openai/gpt-5.3-codex",
|
|
13
18
|
"openai/gpt-5.1",
|
|
14
|
-
"openai/gpt-5.1-codex",
|
|
15
19
|
"openai/gpt-5",
|
|
16
20
|
"openai/gpt-4.1",
|
|
17
21
|
"openai/o4-mini",
|
|
18
22
|
"openai/o3",
|
|
23
|
+
"google/gemini-3.5-flash",
|
|
19
24
|
"google/gemini-3.1-pro-preview",
|
|
25
|
+
"google/gemini-3.1-flash-lite",
|
|
20
26
|
"google/gemini-2.5-pro",
|
|
21
27
|
"google/gemini-2.5-flash",
|
|
22
|
-
"deepseek/deepseek-
|
|
28
|
+
"deepseek/deepseek-v4-pro",
|
|
29
|
+
"deepseek/deepseek-v4-flash",
|
|
23
30
|
"deepseek/deepseek-v3.2",
|
|
31
|
+
"deepseek/deepseek-r1",
|
|
24
32
|
"meta-llama/llama-4-maverick",
|
|
25
|
-
"x-ai/grok-4",
|
|
33
|
+
"x-ai/grok-4.3",
|
|
34
|
+
"x-ai/grok-4.20",
|
|
35
|
+
"qwen/qwen3.7-max",
|
|
26
36
|
"qwen/qwen3-coder",
|
|
37
|
+
"moonshotai/kimi-k2.6",
|
|
27
38
|
"moonshotai/kimi-k2.5",
|
|
28
|
-
"mistralai/mistral-medium-3
|
|
39
|
+
"mistralai/mistral-medium-3-5",
|
|
29
40
|
"mistralai/codestral-2508",
|
|
30
|
-
"mistralai/devstral-
|
|
41
|
+
"mistralai/devstral-2512",
|
|
42
|
+
"mistralai/mistral-medium-3.1",
|
|
31
43
|
] as const;
|
package/src/lib/utils.ts
CHANGED
|
@@ -14,3 +14,23 @@ export function assert(condition: unknown, message: string): asserts condition {
|
|
|
14
14
|
throw new Error(message);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/** Sleep that respects AbortSignal for clean cancellation. */
|
|
19
|
+
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
if (signal?.aborted) {
|
|
22
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const onAbort = () => {
|
|
26
|
+
clearTimeout(timeout);
|
|
27
|
+
signal?.removeEventListener("abort", onAbort);
|
|
28
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
29
|
+
};
|
|
30
|
+
const timeout = setTimeout(() => {
|
|
31
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32
|
+
resolve();
|
|
33
|
+
}, ms);
|
|
34
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
35
|
+
});
|
|
36
|
+
}
|