@gram-ai/elements 1.18.6 → 1.18.8

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.
@@ -1,4 +1,4 @@
1
- import { AuthConfig } from '../types';
1
+ import { ApiConfig } from '../types';
2
2
  export type Auth = {
3
3
  headers: Record<string, string>;
4
4
  isLoading: false;
@@ -11,6 +11,6 @@ export type Auth = {
11
11
  * @returns The session token string or null
12
12
  */
13
13
  export declare const useAuth: ({ projectSlug, auth, }: {
14
- auth?: AuthConfig;
14
+ auth?: ApiConfig;
15
15
  projectSlug: string;
16
16
  }) => Auth;
@@ -1,5 +1,6 @@
1
- import { ApiKeyAuthConfig, AuthConfig } from '../types';
1
+ import { ApiKeyAuthConfig, ApiConfig, SessionAuthConfig } from '../types';
2
2
  /**
3
3
  * Checks if the auth config is an API key auth config
4
4
  */
5
- export declare function isApiKeyAuth(auth: AuthConfig | undefined): auth is ApiKeyAuthConfig;
5
+ export declare function isApiKeyAuth(auth: ApiConfig | undefined): auth is ApiKeyAuthConfig;
6
+ export declare function hasExplicitSessionAuth(auth: ApiConfig | undefined): auth is SessionAuthConfig;
@@ -231,18 +231,36 @@ export interface ElementsConfig {
231
231
  * }
232
232
  */
233
233
  tools?: ToolsConfig;
234
- api?: {
235
- /**
236
- * The Gram API URL to use for the Elements library.
237
- *
238
- * @example
239
- * const config: ElementsConfig = {
240
- * apiURL: 'https://api.getgram.ai',
241
- * }
242
- */
243
- url?: string;
244
- } & AuthConfig;
234
+ /**
235
+ * The API configuration to use for the Elements library.
236
+ *
237
+ * Use this to override the default API URL, or add explicit auth configuration
238
+ *
239
+ * @example
240
+ * const config: ElementsConfig = {
241
+ * api: {
242
+ * url: 'https://api.getgram.ai',
243
+ * },
244
+ * }
245
+ */
246
+ api?: ApiConfig;
245
247
  }
248
+ /**
249
+ * Base API configuration with the server URL.
250
+ */
251
+ export type BaseApiConfig = {
252
+ /**
253
+ * The Gram API URL to use for the Elements library.
254
+ *
255
+ * @example
256
+ * const config: ElementsConfig = {
257
+ * api: {
258
+ * url: 'https://api.getgram.ai',
259
+ * },
260
+ * }
261
+ */
262
+ url?: string;
263
+ };
246
264
  export type SessionAuthConfig = {
247
265
  /**
248
266
  * The function to use to retrieve the session token from the backend endpoint.
@@ -287,7 +305,10 @@ export type ApiKeyAuthConfig = {
287
305
  */
288
306
  UNSAFE_apiKey: string;
289
307
  };
290
- export type AuthConfig = SessionAuthConfig | ApiKeyAuthConfig | undefined;
308
+ /**
309
+ * API configuration - can be just the URL, or URL with session auth, or URL with API key auth.
310
+ */
311
+ export type ApiConfig = BaseApiConfig | (BaseApiConfig & SessionAuthConfig) | (BaseApiConfig & ApiKeyAuthConfig);
291
312
  /**
292
313
  * The LLM model to use for the Elements library.
293
314
  *
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@gram-ai/elements",
3
3
  "description": "Gram Elements is a library of UI primitives for building chat-like experiences for MCP Servers.",
4
4
  "type": "module",
5
- "version": "1.18.6",
5
+ "version": "1.18.8",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
8
8
  ".": {
@@ -1,12 +1,10 @@
1
- import React from 'react'
2
1
  import { Chat } from '..'
3
2
  import type { Meta, StoryFn } from '@storybook/react-vite'
4
- import { useAssistantState } from '@assistant-ui/react'
5
3
  import { google } from '@ai-sdk/google'
6
- import { ComponentOverrides } from '../../../types'
4
+ import { GetSessionFn } from '@/types'
7
5
 
8
6
  const meta: Meta<typeof Chat> = {
9
- title: 'Chat/Customization',
7
+ title: 'Chat/Connection Configuration',
10
8
  component: Chat,
11
9
  parameters: {
12
10
  layout: 'fullscreen',
@@ -35,26 +33,47 @@ SystemPrompt.parameters = {
35
33
  },
36
34
  }
37
35
 
38
- const customComponents: ComponentOverrides = {
39
- Text: () => {
40
- const message = useAssistantState(({ message }) => message)
41
- return (
42
- <div className="text-red-500">
43
- {message.parts
44
- .map((part) => (part.type === 'text' ? part.text : ''))
45
- .join('')}
46
- </div>
47
- )
36
+ export const WithImplicitSessionAuth: Story = () => <Chat />
37
+ WithImplicitSessionAuth.storyName = 'With Implicit Session Auth'
38
+ WithImplicitSessionAuth.parameters = {
39
+ elements: {
40
+ config: {
41
+ // This story tests the case where no API config is provided, which should default to implicit session auth
42
+ api: undefined,
43
+ },
44
+ },
45
+ }
46
+
47
+ const sessionFn: GetSessionFn = async () => {
48
+ const response = await fetch('/chat/session', {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Gram-Project':
52
+ import.meta.env.VITE_GRAM_ELEMENTS_STORYBOOK_PROJECT_SLUG ?? '',
53
+ },
54
+ })
55
+ const data = await response.json()
56
+ return data.client_token
57
+ }
58
+
59
+ export const WithExplicitSessionAuth: Story = () => <Chat />
60
+ WithExplicitSessionAuth.storyName = 'With Explicit Session Auth'
61
+ WithExplicitSessionAuth.parameters = {
62
+ elements: {
63
+ config: {
64
+ api: { sessionFn },
65
+ },
48
66
  },
49
67
  }
50
68
 
51
- export const ComponentOverridesStory: Story = () => <Chat />
52
- ComponentOverridesStory.storyName = 'Component Overrides'
53
- ComponentOverridesStory.parameters = {
69
+ // NOTE: api key auth is currently non functional due to concerns with security
70
+ // Update this story when api key auth is secure
71
+ export const WithExplicitAPIKeyAuth: Story = () => <Chat />
72
+ WithExplicitAPIKeyAuth.storyName = 'With Explicit API Key Auth'
73
+ WithExplicitAPIKeyAuth.parameters = {
54
74
  elements: {
55
75
  config: {
56
- variant: 'standalone',
57
- components: customComponents,
76
+ api: { UNSAFE_apiKey: 'test' },
58
77
  },
59
78
  },
60
79
  }
@@ -0,0 +1,46 @@
1
+ import { Chat, ComponentOverrides } from '@/index'
2
+ import { useAssistantState } from '@assistant-ui/react'
3
+ import { Meta, StoryFn } from '@storybook/react-vite'
4
+
5
+ const meta: Meta<typeof Chat> = {
6
+ title: 'Chat/Customization',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ } satisfies Meta<typeof Chat>
19
+
20
+ export default meta
21
+
22
+ type Story = StoryFn<typeof Chat>
23
+
24
+ const customComponents: ComponentOverrides = {
25
+ Text: () => {
26
+ const message = useAssistantState(({ message }) => message)
27
+ return (
28
+ <div className="text-red-500">
29
+ {message.parts
30
+ .map((part) => (part.type === 'text' ? part.text : ''))
31
+ .join('')}
32
+ </div>
33
+ )
34
+ },
35
+ }
36
+
37
+ export const ComponentOverridesStory: Story = () => <Chat />
38
+ ComponentOverridesStory.storyName = 'Component Overrides'
39
+ ComponentOverridesStory.parameters = {
40
+ elements: {
41
+ config: {
42
+ variant: 'standalone',
43
+ components: customComponents,
44
+ },
45
+ },
46
+ }
@@ -11,10 +11,10 @@ import {
11
11
  } from 'react'
12
12
 
13
13
  import {
14
- useScrollLock,
15
14
  useAssistantState,
16
- type ReasoningMessagePartComponent,
15
+ useScrollLock,
17
16
  type ReasoningGroupComponent,
17
+ type ReasoningMessagePartComponent,
18
18
  } from '@assistant-ui/react'
19
19
 
20
20
  import { MarkdownText } from '@/components/assistant-ui/markdown-text'
@@ -1,6 +1,7 @@
1
1
  import { FrontendTools } from '@/components/FrontendTools'
2
2
  import { useMCPTools } from '@/hooks/useMCPTools'
3
3
  import { useToolApproval } from '@/hooks/useToolApproval'
4
+ import { getApiUrl } from '@/lib/api'
4
5
  import { MODELS } from '@/lib/models'
5
6
  import {
6
7
  clearFrontendToolApprovalConfig,
@@ -40,7 +41,6 @@ import {
40
41
  import { useAuth } from '../hooks/useAuth'
41
42
  import { ElementsContext } from './contexts'
42
43
  import { ToolApprovalProvider } from './ToolApprovalContext'
43
- import { getApiUrl } from '@/lib/api'
44
44
 
45
45
  export interface ElementsProviderProps {
46
46
  children: ReactNode
@@ -63,6 +63,33 @@ function mergeInternalSystemPromptWith(
63
63
  ${plugins.map((plugin) => `- ${plugin.language}: ${plugin.prompt}`).join('\n')}`
64
64
  }
65
65
 
66
+ /**
67
+ * Cleans messages before sending to the model to work around an AI SDK bug.
68
+ * Strips callProviderMetadata from all parts (AI SDK bug #9731)
69
+ */
70
+ function cleanMessagesForModel(messages: UIMessage[]): UIMessage[] {
71
+ return messages.map((message) => {
72
+ const partsArray = message.parts
73
+ if (!Array.isArray(partsArray)) {
74
+ return message
75
+ }
76
+
77
+ // Process each part: strip providerOptions/providerMetadata and filter reasoning
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const cleanedParts = partsArray.map((part: any) => {
80
+ // Strip providerOptions and providerMetadata from all remaining parts
81
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
82
+ const { callProviderMetadata, ...cleanPart } = part
83
+ return cleanPart
84
+ })
85
+
86
+ return {
87
+ ...message,
88
+ parts: cleanedParts,
89
+ }
90
+ })
91
+ }
92
+
66
93
  const ElementsProviderWithApproval = ({
67
94
  children,
68
95
  config,
@@ -180,10 +207,14 @@ const ElementsProviderWithApproval = ({
180
207
  : openRouterModel!.chat(model)
181
208
 
182
209
  try {
210
+ // This works around AI SDK bug where these fields cause validation failures
211
+ const cleanedMessages = cleanMessagesForModel(messages)
212
+ const modelMessages = convertToModelMessages(cleanedMessages)
213
+
183
214
  const result = streamText({
184
215
  system: systemPrompt,
185
216
  model: modelToUse,
186
- messages: convertToModelMessages(messages),
217
+ messages: modelMessages,
187
218
  tools,
188
219
  stopWhen: stepCountIs(10),
189
220
  experimental_transform: smoothStream({ delayInMs: 15 }),
@@ -1,6 +1,7 @@
1
- import { isApiKeyAuth } from '@/lib/auth'
2
- import { AuthConfig } from '../types'
1
+ import { hasExplicitSessionAuth, isApiKeyAuth } from '@/lib/auth'
2
+ import { ApiConfig } from '../types'
3
3
  import { useSession } from './useSession'
4
+ import { useMemo } from 'react'
4
5
 
5
6
  export type Auth =
6
7
  | {
@@ -33,17 +34,24 @@ export const useAuth = ({
33
34
  projectSlug,
34
35
  auth,
35
36
  }: {
36
- auth?: AuthConfig
37
+ auth?: ApiConfig
37
38
  projectSlug: string
38
39
  }): Auth => {
40
+ const getSession = useMemo(() => {
41
+ if (isApiKeyAuth(auth)) {
42
+ return null
43
+ }
44
+ return !isApiKeyAuth(auth) && hasExplicitSessionAuth(auth)
45
+ ? auth.sessionFn
46
+ : defaultGetSession
47
+ }, [auth])
39
48
  // The session request is only neccessary if we are not using an API key auth
40
49
  // configuration. If a custom session fetcher is provided, we use it,
41
50
  // otherwise we fallback to the default session fetcher
42
51
  const session = useSession({
52
+ // We want to check it's NOT API key auth, as the default auth scheme is session auth (if the user hasn't provided an explicit API config, we have a session auth config by default)
43
53
  enabled: !isApiKeyAuth(auth),
44
- getSession: !isApiKeyAuth(auth)
45
- ? (auth?.sessionFn ?? defaultGetSession)
46
- : null,
54
+ getSession,
47
55
  projectSlug,
48
56
  })
49
57
 
package/src/lib/auth.ts CHANGED
@@ -1,10 +1,17 @@
1
- import { ApiKeyAuthConfig, AuthConfig } from '@/types'
1
+ import { ApiKeyAuthConfig, ApiConfig, SessionAuthConfig } from '@/types'
2
2
 
3
3
  /**
4
4
  * Checks if the auth config is an API key auth config
5
5
  */
6
6
  export function isApiKeyAuth(
7
- auth: AuthConfig | undefined
7
+ auth: ApiConfig | undefined
8
8
  ): auth is ApiKeyAuthConfig {
9
9
  return !!auth && 'UNSAFE_apiKey' in auth
10
10
  }
11
+
12
+ export function hasExplicitSessionAuth(
13
+ auth: ApiConfig | undefined
14
+ ): auth is SessionAuthConfig {
15
+ if (!auth) return false
16
+ return 'sessionFn' in auth
17
+ }
@@ -261,17 +261,36 @@ export interface ElementsConfig {
261
261
  */
262
262
  tools?: ToolsConfig
263
263
 
264
- api?: {
265
- /**
266
- * The Gram API URL to use for the Elements library.
267
- *
268
- * @example
269
- * const config: ElementsConfig = {
270
- * apiURL: 'https://api.getgram.ai',
271
- * }
272
- */
273
- url?: string
274
- } & AuthConfig
264
+ /**
265
+ * The API configuration to use for the Elements library.
266
+ *
267
+ * Use this to override the default API URL, or add explicit auth configuration
268
+ *
269
+ * @example
270
+ * const config: ElementsConfig = {
271
+ * api: {
272
+ * url: 'https://api.getgram.ai',
273
+ * },
274
+ * }
275
+ */
276
+ api?: ApiConfig
277
+ }
278
+
279
+ /**
280
+ * Base API configuration with the server URL.
281
+ */
282
+ export type BaseApiConfig = {
283
+ /**
284
+ * The Gram API URL to use for the Elements library.
285
+ *
286
+ * @example
287
+ * const config: ElementsConfig = {
288
+ * api: {
289
+ * url: 'https://api.getgram.ai',
290
+ * },
291
+ * }
292
+ */
293
+ url?: string
275
294
  }
276
295
 
277
296
  export type SessionAuthConfig = {
@@ -320,7 +339,13 @@ export type ApiKeyAuthConfig = {
320
339
  UNSAFE_apiKey: string
321
340
  }
322
341
 
323
- export type AuthConfig = SessionAuthConfig | ApiKeyAuthConfig | undefined
342
+ /**
343
+ * API configuration - can be just the URL, or URL with session auth, or URL with API key auth.
344
+ */
345
+ export type ApiConfig =
346
+ | BaseApiConfig
347
+ | (BaseApiConfig & SessionAuthConfig)
348
+ | (BaseApiConfig & ApiKeyAuthConfig)
324
349
 
325
350
  /**
326
351
  * The LLM model to use for the Elements library.