import { parsePartialJson } from '@ai-sdk/ui-utils' import type { TextStreamPart } from 'ai' import { z } from 'zod' export type DeepPartial = T extends object ? T extends Array ? T : { [P in keyof T]?: DeepPartial } : T export type ParseStreamingJsonEvent = | { type: 'object'; value: DeepPartial } | { type: 'reasoning'; delta: string } | { type: 'error'; message: string } /** The call finished with invalid content that can't be parsed as JSON */ | { type: 'bad-end'; rawText: string } export function removeJsonMarkdown(text: string) { text = text.trim() if (text.startsWith('```json')) { text = text.slice(7) } else if (text.startsWith('json')) { text = text.slice(4) } else if (text.startsWith('```')) { text = text.slice(3) } if (text.endsWith('```')) { text = text.slice(0, -3) } return text.trim() } /** * Parse streaming JSON text * @param fullStream Returned by AI SDK * @param _schema zod schema for type definition * @param isValid Custom validation function to check if the parsed JSON is valid */ export async function* parseStreamingJson( fullStream: AsyncIterable>, _schema: T, isValid: (value: DeepPartial>) => boolean, ): AsyncGenerator>> { let rawText = '' let isParseSuccessful = false for await (const chunk of fullStream) { if (chunk.type === 'reasoning') { yield { type: 'reasoning', delta: chunk.textDelta } continue } if (chunk.type === 'error') { yield { type: 'error', message: chunk.error instanceof Error ? chunk.error.message : String(chunk.error), } continue } if (chunk.type === 'text-delta') { rawText += chunk.textDelta const parsed = parsePartialJson(removeJsonMarkdown(rawText)) isParseSuccessful = parsed.state === 'repaired-parse' || parsed.state === 'successful-parse' if (isParseSuccessful && isValid(parsed.value as any)) { yield { type: 'object', value: parsed.value as DeepPartial>, } } } } // If the last chunk parses failed, return an error if (!isParseSuccessful) { console.warn(`[parseStreamingJson] Failed to parse JSON: ${removeJsonMarkdown(rawText)}`) yield { type: 'bad-end', rawText, } } }