Skip to content

fix(generate): stop treating ai@6 raw-text output echo as parsed schema output - #1145

Merged
murdore merged 1 commit into
releasefrom
fix/schema-fallback-text-output
Jul 11, 2026
Merged

murdore merged 1 commit into
releasefrom
fix/schema-fallback-text-output

Conversation

@murdore

@murdore murdore commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Since ai@6 resolves output ?? text() inside generateText, a result produced without an output spec no longer throws from the experimental_output getter — it echoes the raw model text as a string. Two NeuroLink paths produce such results while a schema is active:

  1. the structured-output fallback retry (after NoObjectGeneratedError / tools↔schema conflict / schema-complexity errors), and
  2. the tools↔schema exclusion path (Gemini, native Anthropic surface).

formatEnhancedResult treats any defined experimental_output as the AI-SDK's parsed+validated object, so on those paths it:

  • sets structuredData to the raw text string, and
  • JSON.stringifys it into content — double-encoding every response.

For a non-empty response the facade's JSON-string-literal unwrap accidentally repairs the damage downstream. For an EMPTY completion it survives to the caller as content '""' (the literal two characters) with structuredData '' — so consumers' empty-response handling never fires and users see a literal "". This was found in production-style E2E testing of curator's empty-response retry (its retry predicate never fired on the schema path).

Fix (3 changes)

Site Change
GenerationHandler.formatEnhancedResult Detect the raw-text echo (typeof experimental_output === 'string' && === generateResult.text) and route it through text-mode coercion instead of serialising it as schema output
GenerationHandler coerceTextMode scalar path A JSON-encoded empty string ('""') is an empty completion, not a recovered scalar — normalize to '' content, no structuredData, + WARN
neurolink.ts facade scalar path Same guard for provider-native generate() overrides (Vertex / Google AI / Anthropic native)

Genuine Output.object results are unaffected: their experimental_output is a parsed object (or a parsed string ≠ raw text), never the raw-text echo.

Verification (stubbed Anthropic API, wire-level)

Case Before After
Empty completion + schema (fallback path) content '""', structuredData '' content '', structuredData unset ✅
Non-JSON first attempt → fallback returns valid JSON repaired only by accidental facade unwrap parsed object + single-encoded content at the formatter ✅
Model literally emits "" as its completion content '""', structuredData '' content '', structuredData unset ✅
Empty completion, no schema '' '' (no regression) ✅
  • tsc --noEmit clean
  • continuous-test-suite-structured-coerce / coerce-nested-unwrap / json — all PASS
  • prettier clean; eslint: 0 errors (7 pre-existing max-lines warnings on untouched functions)

🤖 Investigated & authored with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed structured-output handling for empty responses.
    • Prevented echoed model text from being incorrectly treated as parsed structured data.
    • Avoided unwanted JSON-stringified content in generated results.
    • Added warnings when empty or invalid structured-output recovery occurs.

Copilot AI review requested due to automatic review settings July 11, 2026 13:39
@vercel

vercel Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Jul 11, 2026 4:42pm

@github-actions

github-actions Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: 35446e4d730bfc35e89294c00534c76013df209e
  • Message: fix(generate): stop treating ai@6 raw-text output echo as parsed schema output
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@murdore, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b019be9-4db3-47ce-a86e-387b80af4d24

📥 Commits

Reviewing files that changed from the base of the PR and between 403ead1 and 35446e4.

📒 Files selected for processing (3)
  • src/lib/core/modules/GenerationHandler.ts
  • src/lib/neurolink.ts
  • test/continuous-test-suite-schema-empty-normalization.ts
📝 Walkthrough

Walkthrough

Structured-output recovery now treats JSON-encoded empty strings as empty completions. Experimental output identical to raw model text is routed through text coercion to avoid double-encoding.

Changes

Structured-output recovery

Layer / File(s) Summary
Empty completion normalization
src/lib/core/modules/GenerationHandler.ts, src/lib/neurolink.ts
Parsed empty strings produce empty content and warnings without populating structured data. Matching experimental output echoes fall back to text coercion instead of being JSON-stringified.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: Tara-ag

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: raw-text echoes are no longer misclassified as parsed schema output.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/schema-fallback-text-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a behavioral change in ai@6 where experimental_output can echo raw model text (when no output spec was actually applied), which previously caused NeuroLink to misclassify raw text as parsed schema output and double-encode responses—most visibly turning empty completions into the literal string "".

Changes:

  • In GenerationHandler.formatEnhancedResult, detect the experimental_output raw-text echo and route it through text-mode coercion rather than serializing it as schema output.
  • Normalize JSON-encoded empty-string scalar outputs ('""' → parsed as "") to a true empty completion (content: "", no structuredData) in both GenerationHandler and the neurolink.ts facade path.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/lib/neurolink.ts Normalizes parsed scalar empty-string ("") to an empty completion for facade-level schema coercion on provider-native generate overrides.
src/lib/core/modules/GenerationHandler.ts Avoids treating experimental_output raw-text echo as parsed schema output; normalizes parsed scalar empty-string to empty completion in text-mode coercion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +862 to +872
if (scalar === "") {
// A JSON-encoded empty string is an EMPTY completion, not a
// recovered scalar — normalize to a true empty ('' content, no
// structuredData) so callers' empty-response handling fires
// instead of a literal '""' reaching the user.
logger.warn(
"[GenerationHandler] schema requested but the model returned an empty JSON string; normalizing to empty content",
{ provider: this.providerName, model: this.modelName },
);
return "";
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 56c1d01 — test/continuous-test-suite-schema-empty-normalization.ts (pure suite, no API) drives formatEnhancedResult directly through all five shapes: empty raw-text echo (fallback result), literal "" completion, non-empty echo (must coerce, not double-encode), genuine Output.object object (preserved), and a parsed string ≠ raw text (z.string() schema, stays structured). Verified it fails 3/5 against the pre-fix code and passes 5/5 with the fix.

@Tara-ag Tara-ag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Files reviewed: 2 (src/lib/core/modules/GenerationHandler.ts, src/lib/neurolink.ts)

New issues raised this run: 0

Decision: Approve

The fix correctly identifies and routes the ai@6 raw-text echo away from the structured-output path, and the empty-JSON-string normalization addresses the production empty-response bug described in the PR. The changes are surgical, well-commented, and consistent with the existing coercion patterns in coerceJsonToSchema.

No blocking criteria are met: no secrets, no architectural rule violations, no breaking public SDK API changes, and no new MAJOR correctness issues. The existing unresolved review comment from the Copilot reviewer requesting a regression test for the literal "" path is noted; it is not duplicated here per review protocol, but should be resolved before merge.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@Tara-ag Tara-ag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Reviewed 3 changed files. No new blocking issues found.

Files reviewed

  • src/lib/core/modules/GenerationHandler.ts
  • src/lib/neurolink.ts
  • test/continuous-test-suite-schema-empty-normalization.ts

Assessment

  • The raw-text echo detection in formatEnhancedResult correctly identifies the ai@6 output ?? text() fallback case and routes it through text coercion instead of double-encoding.
  • The empty-JSON-string ("") normalization is applied consistently in both the GenerationHandler path and the facade scalar path in neurolink.ts, preventing the literal '""' content bug.
  • The regression test suite covers the key shapes: empty raw-text echo, literal "", non-empty echo, genuine Output.object, and parsed string ≠ raw text.
  • No CLAUDE.md critical-rule violations, security issues, or public SDK API breakage detected.

Existing comment

The existing Copilot review thread requesting a regression test for the literal "" path appears addressed by the added test/continuous-test-suite-schema-empty-normalization.ts.

Decision: Approve.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@Tara-ag Tara-ag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Files reviewed: 3
New issues raised: 1 (MINOR)
Blocking issues: 0

Assessment

The fix correctly addresses the ai@6 raw-text echo regression:

  • GenerationHandler.formatEnhancedResult now detects when experimental_output is the raw model text echo and routes it through text-mode coercion instead of treating it as parsed schema output.
  • The JSON-encoded empty string ("") is normalized to a true empty completion with structuredData unset, which fixes the literal '""' content bug.
  • The same guard is applied in neurolink.ts for provider-native generate() overrides.
  • A pure, no-API regression suite covers the five critical shapes: empty raw-text echo, literal "", non-empty echo coercion, genuine Output.object, and parsed string ≠ raw text.

No hardcoded secrets, security vulnerabilities, architectural rule violations, or public SDK API breaks were found.

Minor note

The new regression suite is not yet wired into package.json or the main test/continuous-test-suite.ts orchestrator, so it will not run in CI. I left an inline suggestion on the test file to add a matching script. This is non-blocking.

Approving — the core fix is sound and the regression coverage is good.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 The new regression suite is well-scoped and covers the critical edge cases. Per CONTRIBUTING.md, new continuous-test-suite-*.ts files should also get a matching test:<name> script in package.json (or be imported by the main test/continuous-test-suite.ts orchestrator) so this runs in CI and doesn’t silently regress. Consider adding e.g.:

"test:schema-empty-normalization": "npx tsx test/continuous-test-suite-schema-empty-normalization.ts"

and wiring it into the relevant aggregate script.

…ma output

Since ai@6 resolves `output ?? text()` inside generateText, a result
produced WITHOUT an output spec — the structured-output fallback retry,
or the tools↔schema exclusion path — no longer throws from the
`experimental_output` getter: it echoes the raw model text as a string.

formatEnhancedResult treated any defined `experimental_output` as the
AI-SDK's parsed+validated object, so on those paths it set
structuredData to the raw TEXT STRING and JSON.stringify'd it into the
content — double-encoding every response, and turning an EMPTY
completion into the literal two-character string '""' with
structuredData ''. Downstream, the facade's string-literal unwrap
accidentally repaired the non-empty case, but the empty case survived
to callers: consumers' empty-response handling never fired and users
saw a literal "".

Three changes:
- formatEnhancedResult: detect the raw-text echo (string identical to
  generateResult.text) and route it through text-mode coercion instead
  of serialising it as schema output.
- coerceTextMode scalar path: a JSON-encoded empty string ('""') is an
  EMPTY completion, not a recovered scalar — normalize to '' content
  with no structuredData (+ WARN) so empty-response handling fires.
- facade scalar path (finalizeGenerateRequestResult): same guard for
  provider-native generate() overrides (Vertex/Google AI/Anthropic).

Adds test/continuous-test-suite-schema-empty-normalization.ts (pure, no
API) driving formatEnhancedResult through all five shapes; fails 3/5
against the pre-fix code, passes 5/5 with the fix. Existing coercion
suites (structured-coerce, coerce-nested-unwrap, json) pass; tsc clean.
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@Tara-ag Tara-ag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Files reviewed: 3
New issues raised: 0
Existing unresolved comments: 1 minor suggestion (package.json wiring for the new test suite — already noted by Tara-ag)

Assessment

The fix correctly addresses the ai@6 raw-text echo behavior in structured-output fallback paths:

  • GenerationHandler.formatEnhancedResult now detects the raw-text echo (experimental_output string identical to generateResult.text) and routes it through text-mode coercion instead of serializing it as parsed schema output.
  • The scalar coercion paths in both GenerationHandler and neurolink.ts normalize a JSON-encoded empty string ('""') to a true empty completion, preventing the literal two-character string from reaching callers.
  • The regression suite covers the critical shapes: empty raw-text echo, literal '""', non-empty echo coercion, genuine Output.object objects, and parsed strings that differ from raw text.

Checked against project rules

  • CLAUDE.md Critical Rule 5 (backward compatibility): No breaking changes to the public SDK API; the change only corrects edge-case output for schema fallback paths.
  • Security: No secrets, credentials, or PII exposure in the new logs; logger.warn only includes provider/model identifiers.
  • Type safety / correctness: No any/non-null assertions introduced in production code; the logic correctly distinguishes raw-text echoes from genuine parsed scalar strings.
  • Testing: New pure suite added in the continuous-test-suite-*.ts pattern.

Note

The existing unresolved comment from Tara-ag about adding a matching test:schema-empty-normalization script in package.json remains unaddressed. That is a valid CI/wiring suggestion but is non-blocking; it can be followed up in this PR or a fast-follow.

Approving — the blocking bug is fixed and the change is low-risk.

@murdore
murdore merged commit dc23936 into release Jul 11, 2026
17 checks passed
@murdore
murdore deleted the fix/schema-fallback-text-output branch July 11, 2026 18:45
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.86.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 35446e4d Deployed Jul 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants