[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-en-llm-structured-output-zod-streaming-recovery":3},{"id":4,"title":5,"author":6,"body":7,"cover":1059,"description":1060,"draft":1061,"extension":1062,"locale":1063,"meta":1064,"navigation":148,"path":1065,"publishedAt":1066,"seo":1067,"seoTitle":1068,"slug":1069,"stem":1070,"tags":1071,"__hash__":1073},"blog\u002Fen\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery.md","Structured LLM output with Zod: streaming, validation, and recovery","Manuel Tardivo",{"type":8,"value":9,"toc":1050},"minimark",[10,28,62,67,70,92,95,99,114,314,334,389,393,403,425,436,450,454,461,499,825,834,838,841,896,899,903,997,1001,1029,1046],[11,12,13,14,18,19,22,23,27],"p",{},"When an LLM must return JSON your app consumes directly, the most common failure is not an HTTP error: it is a string that looks ",[15,16,17],"strong",{},"syntactically fine"," but is ",[15,20,21],{},"semantically useless","—truncated JSON, wrong key casing, numbers serialized as strings, empty arrays where objects are required. Streaming makes it worse: chunks arrive before the document can be closed and passed to ",[24,25,26],"code",{},"JSON.parse",".",[11,29,30,31,34,35,34,38,41,42,45,46,53,54,57,58,61],{},"This post assumes ",[15,32,33],{},"Node.js 20+",", ",[15,36,37],{},"TypeScript 5.x",[15,39,40],{},"Zod 3.x"," (",[24,43,44],{},".safeParse","), and a client that exposes a token stream (common with the ",[47,48,52],"a",{"href":49,"rel":50},"https:\u002F\u002Fsdk.vercel.ai\u002Fdocs",[51],"nofollow","Vercel AI SDK"," ",[24,55,56],{},"streamText"," plus OpenAI or Anthropic as documented for “JSON mode” or structured output). APIs move—pin and verify your ",[24,59,60],{},"@ai-sdk\u002F*"," or official client versions.",[63,64,66],"h2",{"id":65},"the-problem-valid-tokens-unusable-json","The problem: valid tokens, unusable JSON",[11,68,69],{},"Three recurring failure classes:",[71,72,73,80,86],"ol",{},[74,75,76,79],"li",{},[15,77,78],{},"Parse error",": interrupted stream, unbalanced brackets, stray commas.",[74,81,82,85],{},[15,83,84],{},"Schema error",": valid JSON that breaks your contract (wrong types, missing keys, enums out of range).",[74,87,88,91],{},[15,89,90],{},"Weak semantics",": JSON passes Zod but values are hollow (“description”: “N\u002FA”) because the model filled fields.",[11,93,94],{},"This article addresses (1) and (2) in code. (3) needs business rules (minimum lengths, cross-field checks) or a second validation pass.",[63,96,98],{"id":97},"internal-contract-with-zod","Internal contract with Zod",[11,100,101,102,105,106,109,110,113],{},"Define ",[15,103,104],{},"one schema for your internal API",", not for the raw model prompt. The model may return a superset (extra keys): use Zod ",[24,107,108],{},".strip()"," or ",[24,111,112],{},".pick()"," for stable shapes.",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-typescript shiki shiki-themes github-light github-dark","import { z } from 'zod'\n\nexport const ExtractSchema = z.object({\n  title: z.string().min(1).max(200),\n  tags: z.array(z.string().min(1)).max(10),\n  confidence: z.number().min(0).max(1),\n})\n\nexport type Extract = z.infer\u003Ctypeof ExtractSchema>\n","typescript","",[24,122,123,143,150,176,211,245,273,279,284],{"__ignoreMap":120},[124,125,128,132,136,139],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"szBVR","import",[124,133,135],{"class":134},"sVt8B"," { z } ",[124,137,138],{"class":130},"from",[124,140,142],{"class":141},"sZZnC"," 'zod'\n",[124,144,146],{"class":126,"line":145},2,[124,147,149],{"emptyLinePlaceholder":148},true,"\n",[124,151,153,156,159,163,166,169,173],{"class":126,"line":152},3,[124,154,155],{"class":130},"export",[124,157,158],{"class":130}," const",[124,160,162],{"class":161},"sj4cs"," ExtractSchema",[124,164,165],{"class":130}," =",[124,167,168],{"class":134}," z.",[124,170,172],{"class":171},"sScJk","object",[124,174,175],{"class":134},"({\n",[124,177,179,182,185,188,191,194,197,200,203,205,208],{"class":126,"line":178},4,[124,180,181],{"class":134},"  title: z.",[124,183,184],{"class":171},"string",[124,186,187],{"class":134},"().",[124,189,190],{"class":171},"min",[124,192,193],{"class":134},"(",[124,195,196],{"class":161},"1",[124,198,199],{"class":134},").",[124,201,202],{"class":171},"max",[124,204,193],{"class":134},[124,206,207],{"class":161},"200",[124,209,210],{"class":134},"),\n",[124,212,214,217,220,223,225,227,229,231,233,236,238,240,243],{"class":126,"line":213},5,[124,215,216],{"class":134},"  tags: z.",[124,218,219],{"class":171},"array",[124,221,222],{"class":134},"(z.",[124,224,184],{"class":171},[124,226,187],{"class":134},[124,228,190],{"class":171},[124,230,193],{"class":134},[124,232,196],{"class":161},[124,234,235],{"class":134},")).",[124,237,202],{"class":171},[124,239,193],{"class":134},[124,241,242],{"class":161},"10",[124,244,210],{"class":134},[124,246,248,251,254,256,258,260,263,265,267,269,271],{"class":126,"line":247},6,[124,249,250],{"class":134},"  confidence: z.",[124,252,253],{"class":171},"number",[124,255,187],{"class":134},[124,257,190],{"class":171},[124,259,193],{"class":134},[124,261,262],{"class":161},"0",[124,264,199],{"class":134},[124,266,202],{"class":171},[124,268,193],{"class":134},[124,270,196],{"class":161},[124,272,210],{"class":134},[124,274,276],{"class":126,"line":275},7,[124,277,278],{"class":134},"})\n",[124,280,282],{"class":126,"line":281},8,[124,283,149],{"emptyLinePlaceholder":148},[124,285,287,289,292,295,297,300,302,305,308,311],{"class":126,"line":286},9,[124,288,155],{"class":130},[124,290,291],{"class":130}," type",[124,293,294],{"class":171}," Extract",[124,296,165],{"class":130},[124,298,299],{"class":171}," z",[124,301,27],{"class":134},[124,303,304],{"class":171},"infer",[124,306,307],{"class":134},"\u003C",[124,309,310],{"class":130},"typeof",[124,312,313],{"class":134}," ExtractSchema>\n",[11,315,316,317,319,320,323,324,329,330,333],{},"After ",[24,318,26],{}," (only on a ",[15,321,322],{},"complete"," string) always use ",[15,325,326],{},[24,327,328],{},"safeParse",", not ",[24,331,332],{},"parse",", on hot paths—it returns structured errors without throwing.",[115,335,337],{"className":117,"code":336,"language":119,"meta":120,"style":120},"const parsed = ExtractSchema.safeParse(unknownJson)\nif (!parsed.success) {\n  \u002F\u002F recovery (below)\n}\nreturn parsed.data\n",[24,338,339,357,370,376,381],{"__ignoreMap":120},[124,340,341,344,347,349,352,354],{"class":126,"line":127},[124,342,343],{"class":130},"const",[124,345,346],{"class":161}," parsed",[124,348,165],{"class":130},[124,350,351],{"class":134}," ExtractSchema.",[124,353,328],{"class":171},[124,355,356],{"class":134},"(unknownJson)\n",[124,358,359,362,364,367],{"class":126,"line":145},[124,360,361],{"class":130},"if",[124,363,41],{"class":134},[124,365,366],{"class":130},"!",[124,368,369],{"class":134},"parsed.success) {\n",[124,371,372],{"class":126,"line":152},[124,373,375],{"class":374},"sJ8bj","  \u002F\u002F recovery (below)\n",[124,377,378],{"class":126,"line":178},[124,379,380],{"class":134},"}\n",[124,382,383,386],{"class":126,"line":213},[124,384,385],{"class":130},"return",[124,387,388],{"class":134}," parsed.data\n",[63,390,392],{"id":391},"streaming-buffer-and-when-to-stop","Streaming: buffer and when to stop",[11,394,395,396,402],{},"While streaming, ",[15,397,398,399,401],{},"do not ",[24,400,26],{}," every delta",". Minimal pattern:",[71,404,405,408,415],{},[74,406,407],{},"Append text to a string buffer.",[74,409,410,411,414],{},"Optional: if the provider mixes reasoning or preambles, isolate the JSON payload (some models wrap ",[24,412,413],{},"json … "," fences—strip before parsing).",[74,416,417,418,420,421,424],{},"Only when the stream ends (or an end marker arrives), call ",[24,419,26],{}," on the ",[15,422,423],{},"full"," buffer.",[11,426,427,428,431,432,435],{},"If the provider offers ",[15,429,430],{},"native structured output"," that guarantees valid JSON at end-of-stream, steps 2–3 get easier but ",[15,433,434],{},"Zod stays",": the model can still break semantic constraints.",[11,437,438,441,442,445,446,449],{},[15,439,440],{},"When to skip streaming"," for tabular output: if the UI expects row-by-row updates but the model emits one large JSON blob, partial updates confuse users. Often you stream free text for UX and run a second ",[15,443,444],{},"non-streaming"," JSON call, or stream ",[15,447,448],{},"NDJSON"," (one line = one record) with a simpler per-line schema.",[63,451,453],{"id":452},"recovery-failed-parse-or-failed-zod","Recovery: failed parse or failed Zod",[11,455,456,457,460],{},"Layered strategy with a ",[15,458,459],{},"hard attempt budget"," (e.g. two repairs, then fail):",[71,462,463,472,485],{},[74,464,465,468,469,471],{},[15,466,467],{},"Light repair",": if ",[24,470,26],{}," throws, trimming dangling characters only helps with a trustworthy heuristic—usually safer to send the model a short repair prompt: “This JSON is invalid. Error: …. Return only corrected JSON for this schema.” with the truncated buffer attached.",[74,473,474,477,478,109,481,484],{},[15,475,476],{},"Zod failure",": feed ",[24,479,480],{},"parsed.error.flatten()",[24,482,483],{},"issues"," into that repair prompt so the model knows what to fix.",[74,486,487,490,491,494,495,498],{},[15,488,489],{},"Degrade",": after N attempts return a business error (",[24,492,493],{},"422",") or enqueue human review—",[15,496,497],{},"no infinite loops","; they burn tokens and rate limits.",[115,500,502],{"className":117,"code":501,"language":119,"meta":120,"style":120},"const MAX_REPAIR = 2\n\nasync function extractWithRepair(\n  raw: string,\n  attempt: number,\n): Promise\u003CExtract> {\n  let json: unknown\n  try {\n    json = JSON.parse(raw)\n  } catch {\n    if (attempt >= MAX_REPAIR) throw new SyntaxError('Invalid JSON after repairs')\n    const fixed = await repairJsonWithLlm(raw, 'syntax')\n    return extractWithRepair(fixed, attempt + 1)\n  }\n  const r = ExtractSchema.safeParse(json)\n  if (r.success) return r.data\n  if (attempt >= MAX_REPAIR) throw new Error('Schema validation failed')\n  const fixed = await repairJsonWithLlm(raw, JSON.stringify(r.error.flatten()))\n  return extractWithRepair(fixed, attempt + 1)\n}\n",[24,503,504,516,520,534,549,561,579,592,600,618,629,663,688,707,713,731,745,772,804,820],{"__ignoreMap":120},[124,505,506,508,511,513],{"class":126,"line":127},[124,507,343],{"class":130},[124,509,510],{"class":161}," MAX_REPAIR",[124,512,165],{"class":130},[124,514,515],{"class":161}," 2\n",[124,517,518],{"class":126,"line":145},[124,519,149],{"emptyLinePlaceholder":148},[124,521,522,525,528,531],{"class":126,"line":152},[124,523,524],{"class":130},"async",[124,526,527],{"class":130}," function",[124,529,530],{"class":171}," extractWithRepair",[124,532,533],{"class":134},"(\n",[124,535,536,540,543,546],{"class":126,"line":178},[124,537,539],{"class":538},"s4XuR","  raw",[124,541,542],{"class":130},":",[124,544,545],{"class":161}," string",[124,547,548],{"class":134},",\n",[124,550,551,554,556,559],{"class":126,"line":213},[124,552,553],{"class":538},"  attempt",[124,555,542],{"class":130},[124,557,558],{"class":161}," number",[124,560,548],{"class":134},[124,562,563,566,568,571,573,576],{"class":126,"line":247},[124,564,565],{"class":134},")",[124,567,542],{"class":130},[124,569,570],{"class":171}," Promise",[124,572,307],{"class":134},[124,574,575],{"class":171},"Extract",[124,577,578],{"class":134},"> {\n",[124,580,581,584,587,589],{"class":126,"line":275},[124,582,583],{"class":130},"  let",[124,585,586],{"class":134}," json",[124,588,542],{"class":130},[124,590,591],{"class":161}," unknown\n",[124,593,594,597],{"class":126,"line":281},[124,595,596],{"class":130},"  try",[124,598,599],{"class":134}," {\n",[124,601,602,605,608,611,613,615],{"class":126,"line":286},[124,603,604],{"class":134},"    json ",[124,606,607],{"class":130},"=",[124,609,610],{"class":161}," JSON",[124,612,27],{"class":134},[124,614,332],{"class":171},[124,616,617],{"class":134},"(raw)\n",[124,619,621,624,627],{"class":126,"line":620},10,[124,622,623],{"class":134},"  } ",[124,625,626],{"class":130},"catch",[124,628,599],{"class":134},[124,630,632,635,638,641,643,646,649,652,655,657,660],{"class":126,"line":631},11,[124,633,634],{"class":130},"    if",[124,636,637],{"class":134}," (attempt ",[124,639,640],{"class":130},">=",[124,642,510],{"class":161},[124,644,645],{"class":134},") ",[124,647,648],{"class":130},"throw",[124,650,651],{"class":130}," new",[124,653,654],{"class":171}," SyntaxError",[124,656,193],{"class":134},[124,658,659],{"class":141},"'Invalid JSON after repairs'",[124,661,662],{"class":134},")\n",[124,664,666,669,672,674,677,680,683,686],{"class":126,"line":665},12,[124,667,668],{"class":130},"    const",[124,670,671],{"class":161}," fixed",[124,673,165],{"class":130},[124,675,676],{"class":130}," await",[124,678,679],{"class":171}," repairJsonWithLlm",[124,681,682],{"class":134},"(raw, ",[124,684,685],{"class":141},"'syntax'",[124,687,662],{"class":134},[124,689,691,694,696,699,702,705],{"class":126,"line":690},13,[124,692,693],{"class":130},"    return",[124,695,530],{"class":171},[124,697,698],{"class":134},"(fixed, attempt ",[124,700,701],{"class":130},"+",[124,703,704],{"class":161}," 1",[124,706,662],{"class":134},[124,708,710],{"class":126,"line":709},14,[124,711,712],{"class":134},"  }\n",[124,714,716,719,722,724,726,728],{"class":126,"line":715},15,[124,717,718],{"class":130},"  const",[124,720,721],{"class":161}," r",[124,723,165],{"class":130},[124,725,351],{"class":134},[124,727,328],{"class":171},[124,729,730],{"class":134},"(json)\n",[124,732,734,737,740,742],{"class":126,"line":733},16,[124,735,736],{"class":130},"  if",[124,738,739],{"class":134}," (r.success) ",[124,741,385],{"class":130},[124,743,744],{"class":134}," r.data\n",[124,746,748,750,752,754,756,758,760,762,765,767,770],{"class":126,"line":747},17,[124,749,736],{"class":130},[124,751,637],{"class":134},[124,753,640],{"class":130},[124,755,510],{"class":161},[124,757,645],{"class":134},[124,759,648],{"class":130},[124,761,651],{"class":130},[124,763,764],{"class":171}," Error",[124,766,193],{"class":134},[124,768,769],{"class":141},"'Schema validation failed'",[124,771,662],{"class":134},[124,773,775,777,779,781,783,785,787,790,792,795,798,801],{"class":126,"line":774},18,[124,776,718],{"class":130},[124,778,671],{"class":161},[124,780,165],{"class":130},[124,782,676],{"class":130},[124,784,679],{"class":171},[124,786,682],{"class":134},[124,788,789],{"class":161},"JSON",[124,791,27],{"class":134},[124,793,794],{"class":171},"stringify",[124,796,797],{"class":134},"(r.error.",[124,799,800],{"class":171},"flatten",[124,802,803],{"class":134},"()))\n",[124,805,807,810,812,814,816,818],{"class":126,"line":806},19,[124,808,809],{"class":130},"  return",[124,811,530],{"class":171},[124,813,698],{"class":134},[124,815,701],{"class":130},[124,817,704],{"class":161},[124,819,662],{"class":134},[124,821,823],{"class":126,"line":822},20,[124,824,380],{"class":134},[11,826,827,830,831,833],{},[24,828,829],{},"repairJsonWithLlm"," is yours: preferably ",[15,832,444],{},", low temperature, known pricing.",[63,835,837],{"id":836},"telemetry-what-to-log-without-pii","Telemetry: what to log without PII",[11,839,840],{},"At minimum log:",[842,843,844,861,872,885],"ul",{},[74,845,846,849,850,34,853,856,857,860],{},[15,847,848],{},"model"," id (e.g. ",[24,851,852],{},"gpt-4.1",[24,854,855],{},"claude-3-5-sonnet-*",") and ",[15,858,859],{},"finish_reason"," when exposed.",[74,862,863,864,867,868,871],{},"End-to-end ",[15,865,866],{},"latency"," and ",[15,869,870],{},"token"," in\u002Fout if the client surfaces them (OpenAI\u002FAnthropic response bodies or headers per their docs).",[74,873,874,877,878,881,882,199],{},[15,875,876],{},"Repair counters"," and failure kind (",[24,879,880],{},"syntax"," vs ",[24,883,884],{},"zod",[74,886,887,888,891,892,895],{},"A ",[15,889,890],{},"hash"," (e.g. SHA-256) of system + user prompt ",[15,893,894],{},"without"," storing raw user text unless required—helps trace regressions when prompts change.",[11,897,898],{},"Opinion, not law: raw JSON in prod logs is fine in staging; in production sample or mask it for compliance and cost.",[63,900,902],{"id":901},"verified-sources-structured-outputs-json-mode-ai-sdk","Verified sources: Structured Outputs, JSON mode, AI SDK",[842,904,905,939,979],{},[74,906,907,910,911,915,916,919,920,923,924,929,930,934,935,938],{},[15,908,909],{},"OpenAI — Structured Outputs vs JSON mode",": official docs separate ",[912,913,914],"em",{},"JSON mode"," (valid JSON syntax) from ",[15,917,918],{},"Structured Outputs"," tied to a ",[15,921,922],{},"JSON Schema",": the latter constrains the emitted object where supported, cutting structural failures compared with “please return JSON” prompting. See ",[47,925,928],{"href":926,"rel":927},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",[51],"Structured model outputs"," and the ",[47,931,914],{"href":932,"rel":933},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fjson-mode",[51]," guide. ",[15,936,937],{},"Keep Zod"," (or equivalent) as your portability layer across providers, for business-only rules, and when you still call models without structured-output support.",[74,940,941,943,944,947,948,950,951,954,955,958,959,962,963,968,969,41,972,34,975,978],{},[15,942,52],{},": current guidance is ",[24,945,946],{},"Output.object({ schema: z.object({…}) })"," with ",[24,949,56],{}," \u002F ",[24,952,953],{},"generateText",", consuming ",[15,956,957],{},"typed partial objects"," via ",[24,960,961],{},"partialOutputStream",". Reference: ",[47,964,967],{"href":965,"rel":966},"https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-core\u002Fgenerating-structured-data",[51],"Generating structured data",". Pin and read release notes for your ",[15,970,971],{},"installed major",[24,973,974],{},"ai",[24,976,977],{},"@ai-sdk\u002Fopenai",", …)—the surface changes frequently.",[74,980,981,984,985,988,989,992,993,996],{},[15,982,983],{},"Hard limits",": even with schema guarantees, handle ",[15,986,987],{},"truncated streams",", timeouts, and ",[15,990,991],{},"model refusals","; in the AI SDK, streaming errors may surface through stream events (",[24,994,995],{},"onError",") rather than only thrown exceptions—verify against the version you ship.",[63,998,1000],{"id":999},"operational-summary","Operational summary",[71,1002,1003,1011,1017,1023],{},[74,1004,1005,1008,1009,27],{},[15,1006,1007],{},"Zod after parse",", always ",[24,1010,328],{},[74,1012,1013,1016],{},[15,1014,1015],{},"Stream = single buffer",", parse at end (unless you designed NDJSON).",[74,1018,1019,1022],{},[15,1020,1021],{},"Repairs"," with a fixed budget and metrics on failures.",[74,1024,1025,1026,1028],{},"Prefer ",[15,1027,444],{}," for critical monolithic JSON.",[11,1030,1031,1032,1035,1036,53,1041,1043,1044,27],{},"Keep handy: OpenAI ",[47,1033,918],{"href":926,"rel":1034},[51]," \u002F JSON mode, Anthropic’s structured-output docs, and ",[47,1037,1040],{"href":1038,"rel":1039},"https:\u002F\u002Fzod.dev",[51],"Zod",[24,1042,328],{}," + ",[24,1045,800],{},[1047,1048,1049],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":120,"searchDepth":145,"depth":145,"links":1051},[1052,1053,1054,1055,1056,1057,1058],{"id":65,"depth":145,"text":66},{"id":97,"depth":145,"text":98},{"id":391,"depth":145,"text":392},{"id":452,"depth":145,"text":453},{"id":836,"depth":145,"text":837},{"id":901,"depth":145,"text":902},{"id":999,"depth":145,"text":1000},"\u002Fblog\u002F25_llm_structured_output_zod_streaming_recovery_1x.webp","TypeScript patterns to validate structured LLM output, handle streaming, and recover when JSON or the schema fails once you are in production.",false,"md","en",{},"\u002Fen\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery","2026-05-11",{"title":5,"description":1060},"Structured LLM output with Zod recovery","llm-structured-output-zod-streaming-recovery","en\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery",[1072],"mvp","N4kjH77O3nKeJ49ZeugV98I039Tcy2eFg3JLrduPyA8"]