@@ -76,12 +76,45 @@ export interface RouteResponse {
7676/** Valid message roles accepted by the AI routes. */
7777const VALID_ROLES = new Set < string > ( [ 'system' , 'user' , 'assistant' , 'tool' ] ) ;
7878
79+ /**
80+ * Normalize a Vercel AI SDK v6 message (which may use `parts` instead of
81+ * `content`) into a plain `{ role, content }` ModelMessage.
82+ */
83+ function normalizeMessage ( raw : Record < string , unknown > ) : ModelMessage {
84+ const role = raw . role as string ;
85+
86+ // If content is already a string, use it directly
87+ if ( typeof raw . content === 'string' ) {
88+ return { role, content : raw . content } as unknown as ModelMessage ;
89+ }
90+
91+ // If content is an array (multi-part), pass through
92+ if ( Array . isArray ( raw . content ) ) {
93+ return { role, content : raw . content } as unknown as ModelMessage ;
94+ }
95+
96+ // Vercel AI SDK v6: extract text from `parts` array
97+ if ( Array . isArray ( raw . parts ) ) {
98+ const textParts = ( raw . parts as Array < Record < string , unknown > > )
99+ . filter ( p => p . type === 'text' && typeof p . text === 'string' )
100+ . map ( p => p . text as string ) ;
101+ if ( textParts . length > 0 ) {
102+ return { role, content : textParts . join ( '' ) } as unknown as ModelMessage ;
103+ }
104+ }
105+
106+ // Fallback: empty content (e.g. tool-only assistant messages)
107+ return { role, content : '' } as unknown as ModelMessage ;
108+ }
109+
79110/**
80111 * Validate that `raw` is a well-formed message.
81112 * Returns null on success, or an error string on failure.
82113 *
83- * Accepts both simple string content (legacy) and Vercel AI SDK array content
84- * (e.g. `[{ type: 'text', text: '...' }]`).
114+ * Accepts:
115+ * - Simple string `content` (legacy)
116+ * - Array `content` (e.g. `[{ type: 'text', text: '...' }]`)
117+ * - Vercel AI SDK v6 `parts` format (content may be absent/null)
85118 */
86119function validateMessage ( raw : unknown ) : string | null {
87120 if ( typeof raw !== 'object' || raw === null ) {
@@ -92,12 +125,21 @@ function validateMessage(raw: unknown): string | null {
92125 return `message.role must be one of ${ [ ...VALID_ROLES ] . map ( r => `"${ r } "` ) . join ( ', ' ) } ` ;
93126 }
94127 const content = msg . content ;
128+
129+ // Vercel AI SDK v6 sends `parts` instead of (or alongside) `content`.
130+ // Accept any message that carries a `parts` array, even when `content` is absent.
131+ if ( Array . isArray ( msg . parts ) ) {
132+ return null ;
133+ }
134+
135+ // content is a plain string — OK
95136 if ( typeof content === 'string' ) {
96137 return null ;
97138 }
139+
140+ // content is an array of typed parts (legacy multi-part format)
98141 if ( Array . isArray ( content ) ) {
99- const parts = content as unknown [ ] ;
100- for ( const part of parts ) {
142+ for ( const part of content as unknown [ ] ) {
101143 if ( typeof part !== 'object' || part === null ) {
102144 return 'message.content array elements must be non-null objects' ;
103145 }
@@ -111,7 +153,15 @@ function validateMessage(raw: unknown): string | null {
111153 }
112154 return null ;
113155 }
114- return 'message.content must be a string or an array' ;
156+
157+ // Assistant / tool messages may legitimately have null or missing content
158+ if ( content === null || content === undefined ) {
159+ if ( msg . role === 'assistant' || msg . role === 'tool' ) {
160+ return null ;
161+ }
162+ }
163+
164+ return 'message.content must be a string, an array, or include parts' ;
115165}
116166
117167/**
@@ -192,7 +242,7 @@ export function buildAIRoutes(
192242 ...( systemPrompt
193243 ? [ { role : 'system' as const , content : systemPrompt } ]
194244 : [ ] ) ,
195- ...( messages as ModelMessage [ ] ) ,
245+ ...messages . map ( m => normalizeMessage ( m as Record < string , unknown > ) ) ,
196246 ] ;
197247
198248 // ── Choose response mode ─────────────────────────────
@@ -249,7 +299,7 @@ export function buildAIRoutes(
249299 if ( ! aiService . streamChat ) {
250300 return { status : 501 , body : { error : 'Streaming is not supported by the configured AI service' } } ;
251301 }
252- const events = aiService . streamChat ( messages as ModelMessage [ ] , options as any ) ;
302+ const events = aiService . streamChat ( messages . map ( m => normalizeMessage ( m as Record < string , unknown > ) ) , options as any ) ;
253303 return { status : 200 , stream : true , events } ;
254304 } catch ( err ) {
255305 logger . error ( '[AI Route] /chat/stream error' , err instanceof Error ? err : undefined ) ;
0 commit comments