11import { createFileRoute } from '@tanstack/react-router'
22import { convertMessagesToModelMessages } from '@tanstack/ai'
3- import type { ModelMessage , UIMessage } from '@tanstack/ai'
3+ import { z } from 'zod'
4+ import type { UIMessage } from '@tanstack/ai'
5+
6+ const sourceSchema = z . discriminatedUnion ( 'type' , [
7+ z . object ( {
8+ type : z . literal ( 'data' ) ,
9+ value : z . string ( ) ,
10+ mimeType : z . string ( ) ,
11+ } ) ,
12+ z . object ( {
13+ type : z . literal ( 'url' ) ,
14+ value : z . string ( ) ,
15+ mimeType : z . string ( ) . optional ( ) ,
16+ } ) ,
17+ ] )
18+
19+ const contentPartSchema = z . discriminatedUnion ( 'type' , [
20+ z . object ( {
21+ type : z . literal ( 'text' ) ,
22+ content : z . string ( ) ,
23+ metadata : z . unknown ( ) . optional ( ) ,
24+ } ) ,
25+ ...( [ 'image' , 'audio' , 'video' , 'document' ] as const ) . map ( ( type ) =>
26+ z . object ( {
27+ type : z . literal ( type ) ,
28+ source : sourceSchema ,
29+ metadata : z . unknown ( ) . optional ( ) ,
30+ } ) ,
31+ ) ,
32+ ] )
33+
34+ const messagePartSchema = z . discriminatedUnion ( 'type' , [
35+ ...contentPartSchema . options ,
36+ z . object ( {
37+ type : z . literal ( 'tool-call' ) ,
38+ id : z . string ( ) ,
39+ name : z . string ( ) ,
40+ arguments : z . string ( ) ,
41+ input : z . unknown ( ) . optional ( ) ,
42+ state : z . enum ( [
43+ 'awaiting-input' ,
44+ 'input-streaming' ,
45+ 'input-complete' ,
46+ 'approval-requested' ,
47+ 'approval-responded' ,
48+ 'complete' ,
49+ 'error' ,
50+ ] ) ,
51+ approval : z
52+ . object ( {
53+ id : z . string ( ) ,
54+ needsApproval : z . boolean ( ) ,
55+ approved : z . boolean ( ) . optional ( ) ,
56+ } )
57+ . optional ( ) ,
58+ output : z . unknown ( ) . optional ( ) ,
59+ metadata : z . unknown ( ) . optional ( ) ,
60+ } ) ,
61+ z . object ( {
62+ type : z . literal ( 'tool-result' ) ,
63+ toolCallId : z . string ( ) ,
64+ content : z . union ( [ z . string ( ) , z . array ( contentPartSchema ) ] ) ,
65+ state : z . enum ( [ 'streaming' , 'complete' , 'error' ] ) ,
66+ error : z . string ( ) . optional ( ) ,
67+ } ) ,
68+ z . object ( {
69+ type : z . literal ( 'thinking' ) ,
70+ content : z . string ( ) ,
71+ stepId : z . string ( ) . optional ( ) ,
72+ signature : z . string ( ) . optional ( ) ,
73+ } ) ,
74+ z . object ( {
75+ type : z . literal ( 'structured-output' ) ,
76+ status : z . enum ( [ 'streaming' , 'complete' , 'error' ] ) ,
77+ partial : z . unknown ( ) . optional ( ) ,
78+ data : z . unknown ( ) . optional ( ) ,
79+ raw : z . string ( ) ,
80+ reasoning : z . string ( ) . optional ( ) ,
81+ errorMessage : z . string ( ) . optional ( ) ,
82+ } ) ,
83+ z . object ( {
84+ type : z . literal ( 'ui-resource' ) ,
85+ resource : z . object ( {
86+ uri : z . string ( ) ,
87+ mimeType : z . string ( ) ,
88+ text : z . string ( ) . optional ( ) ,
89+ blob : z . string ( ) . optional ( ) ,
90+ } ) ,
91+ serverId : z . string ( ) . optional ( ) ,
92+ toolCallId : z . string ( ) ,
93+ toolName : z . string ( ) ,
94+ meta : z . record ( z . string ( ) , z . unknown ( ) ) . optional ( ) ,
95+ } ) ,
96+ ] )
97+
98+ const requestBodySchema : z . ZodType < { messages : Array < UIMessage > } > = z . object ( {
99+ messages : z . array (
100+ z . object ( {
101+ id : z . string ( ) ,
102+ role : z . enum ( [ 'system' , 'user' , 'assistant' ] ) ,
103+ parts : z . array ( messagePartSchema ) ,
104+ createdAt : z . coerce . date ( ) . optional ( ) ,
105+ } ) ,
106+ ) ,
107+ } )
4108
5109/**
6110 * Provider-free harness for the UIMessage -> ModelMessage identity contract.
@@ -11,11 +115,21 @@ export const Route = createFileRoute('/api/message-ids')({
11115 server : {
12116 handlers : {
13117 POST : async ( { request } ) => {
14- const body = ( await request . json ( ) ) as {
15- messages : Array < UIMessage | ModelMessage >
118+ let body : unknown
119+ try {
120+ body = await request . json ( )
121+ } catch {
122+ return new Response ( 'Invalid JSON request body' , { status : 400 } )
123+ }
124+
125+ const parsed = requestBodySchema . safeParse ( body )
126+ if ( ! parsed . success ) {
127+ return new Response ( 'Invalid message data' , { status : 400 } )
16128 }
17129
18- return Response . json ( convertMessagesToModelMessages ( body . messages ) )
130+ return Response . json (
131+ convertMessagesToModelMessages ( parsed . data . messages ) ,
132+ )
19133 } ,
20134 } ,
21135 } ,
0 commit comments