Skip to content

feat(ai-gemini): stream structured outputs natively#971

Open
Missing-Identity wants to merge 2 commits into
TanStack:mainfrom
Missing-Identity:feat/gemini-structured-output-stream
Open

feat(ai-gemini): stream structured outputs natively#971
Missing-Identity wants to merge 2 commits into
TanStack:mainfrom
Missing-Identity:feat/gemini-structured-output-stream

Conversation

@Missing-Identity

@Missing-Identity Missing-Identity commented Jul 21, 2026

Copy link
Copy Markdown

What changed

  • add native structuredOutputStream support to the standard Gemini text adapter
  • add native structured-output streaming to the experimental Gemini Interactions adapter
  • accumulate and parse streamed JSON before emitting structured-output.complete
  • preserve interaction ID and run-finished event ordering
  • add focused adapter coverage, enable Gemini in the structured-output-stream E2E matrix, and add a minor changeset

Closes #570.

Validation

  • focused Gemini adapter tests: 46 passed
  • complete @tanstack/ai-gemini test suite: 239 passed
  • @tanstack/ai-gemini TypeScript check: passed
  • @tanstack/ai-gemini ESLint: no errors; seven pre-existing warnings
  • @tanstack/ai-gemini production build: passed
  • git diff --check: passed

Limitations

The hosted environment could not download Playwright Chromium, so browser E2E execution was not claimed. The existing structured-output-stream E2E matrix was updated to include Gemini.

Summary by CodeRabbit

  • New Features
    • Added native structured-output streaming for Gemini text generation.
    • Added structured-output streaming support for the experimental Gemini Interactions API.
    • Streaming now produces a structured-output.complete custom event with the parsed JSON result (plus the original text) before the final completion event.
  • Improved Reliability
    • Enhanced error reporting for empty, truncated, parse-failing, or provider-failing streams.
  • Tests
    • Expanded structured-output streaming end-to-end and adapter test coverage for Gemini.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ff29408-410c-4cb4-a19d-a223a5f2df13

📥 Commits

Reviewing files that changed from the base of the PR and between 35b371e and 32f4a91.

📒 Files selected for processing (3)
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/tests/gemini-adapter.test.ts
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ai-gemini/tests/gemini-adapter.test.ts
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts

📝 Walkthrough

Walkthrough

Native structured-output streaming was added to the Gemini text and experimental Gemini Interactions adapters. Both accumulate JSON deltas, emit parsed completion events before terminal events, handle stream failures, and add tests plus E2E feature coverage.

Changes

Gemini structured-output streaming

Layer / File(s) Summary
Gemini text streaming
packages/ai-gemini/src/adapters/text.ts, packages/ai-gemini/tests/gemini-adapter.test.ts
GeminiTextAdapter streams JSON responses, emits incremental content, and produces structured-output.complete before RUN_FINISHED, with standardized error events for failures.
Gemini Interactions streaming
packages/ai-gemini/src/experimental/text-interactions/adapter.ts, packages/ai-gemini/tests/text-interactions-adapter.test.ts
GeminiTextInteractionsAdapter streams schema-constrained responses, preserves interaction IDs, parses accumulated JSON, and validates completion ordering and chaining.
Streaming coverage and release metadata
testing/e2e/src/lib/feature-support.ts, .changeset/tidy-gemini-streams.md
Gemini is added to the structured-output-stream feature matrix and the package receives a minor changeset.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GeminiAdapter
  participant GeminiAPI
  participant Consumer
  Caller->>GeminiAdapter: structuredOutputStream(options)
  GeminiAdapter->>GeminiAPI: Request JSON schema with streaming enabled
  GeminiAPI-->>GeminiAdapter: JSON text deltas
  GeminiAdapter-->>Consumer: TEXT_MESSAGE_CONTENT chunks
  GeminiAdapter-->>Consumer: structured-output.complete
  GeminiAdapter-->>Consumer: RUN_FINISHED
Loading

Possibly related PRs

  • TanStack/ai#587: Updates structured-output custom event typing used by this runtime behavior.
  • TanStack/ai#781: Provides the Interactions API event and response-format migration used by this adapter.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: native structured output streaming for Gemini adapters.
Description check ✅ Passed The description covers changes, validation, and limitations, though it omits the template checklist and release impact section.
Linked Issues check ✅ Passed The PR implements native structuredOutputStream support, event ordering, tests, and Gemini E2E coverage requested in #570.
Out of Scope Changes check ✅ Passed The added changeset, tests, and E2E matrix update are all directly related to the structured-output streaming work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-gemini/src/adapters/text.ts`:
- Around line 229-283: The structuredOutputStream method uses inconsistent run
IDs between stream processing and structured-stream errors. Resolve a single
runId, using the existing override-or-generated fallback behavior, create an
options object containing it, and pass that same object to processStreamChunks
and every structuredStreamError call so all emitted events share the resolved
ID.

In `@packages/ai-gemini/tests/gemini-adapter.test.ts`:
- Line 3: Reorder the named imports from `@tanstack/ai` alphabetically to satisfy
ESLint sort-imports in packages/ai-gemini/tests/gemini-adapter.test.ts:3-3 and
packages/ai-gemini/tests/text-interactions-adapter.test.ts:3-3; update only the
import member ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c313a8d4-d80c-4e8d-a85c-1fc5ab15f7ca

📥 Commits

Reviewing files that changed from the base of the PR and between 7c7aa09 and 35b371e.

📒 Files selected for processing (6)
  • .changeset/tidy-gemini-streams.md
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/src/experimental/text-interactions/adapter.ts
  • packages/ai-gemini/tests/gemini-adapter.test.ts
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts
  • testing/e2e/src/lib/feature-support.ts

Comment thread packages/ai-gemini/src/adapters/text.ts Outdated
Comment on lines +229 to +283
const { chatOptions, outputSchema } = options
const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)

try {
chatOptions.logger.request(
`activity=structuredOutputStream provider=gemini model=${this.model} messages=${chatOptions.messages.length}`,
{ provider: 'gemini', model: this.model },
)
const result = await this.client.models.generateContentStream({
...mappedOptions,
config: {
...mappedOptions.config,
responseMimeType: 'application/json',
responseSchema: outputSchema,
},
})

let rawText = ''
let finished:
| Extract<StreamChunk, { type: typeof EventType.RUN_FINISHED }>
| undefined
let failed = false
for await (const chunk of this.processStreamChunks(
result,
chatOptions,
chatOptions.logger,
)) {
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
rawText += chunk.delta
}
if (chunk.type === EventType.RUN_ERROR) failed = true
if (chunk.type === EventType.RUN_FINISHED) {
finished = chunk
} else {
yield chunk
}
}

if (failed) return
if (!finished) {
yield structuredStreamError(
chatOptions,
'Gemini structured-output stream ended without a terminal event',
'truncated-stream',
)
return
}
if (!rawText) {
yield structuredStreamError(
chatOptions,
'Gemini structured-output stream contained no content',
'empty-response',
)
return
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant symbols and inspect the surrounding code
rg -n "structuredOutputStream|processStreamChunks|structuredStreamError|generateId\\(" packages/ai-gemini/src/adapters/text.ts

echo
echo "--- structuredOutputStream / nearby lines ---"
sed -n '220,320p' packages/ai-gemini/src/adapters/text.ts

echo
echo "--- processStreamChunks / nearby lines ---"
sed -n '330,430p' packages/ai-gemini/src/adapters/text.ts

echo
echo "--- structuredStreamError / nearby lines ---"
sed -n '1040,1085p' packages/ai-gemini/src/adapters/text.ts

Repository: TanStack/ai

Length of output: 8862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the interaction adapter for the pattern mentioned in the comment
rg -n "runId\\s*=|structuredStreamError\\(|processStreamChunks\\(" packages/ai-*/src/adapters/**/*.ts

echo
echo "--- interaction adapter runId handling ---"
sed -n '1,220p' packages/ai-interactions/src/adapters/text.ts

Repository: TanStack/ai

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the TextOptions definition and all structuredOutputStream call sites
rg -n "interface TextOptions|type TextOptions|runId\\??:" packages -g '*.ts'

echo
echo "--- structuredOutputStream call sites ---"
rg -n "structuredOutputStream\\(" packages -g '*.ts'

echo
echo "--- Gemini text adapter type definitions around runId ---"
rg -n "runId" packages/ai-gemini/src/adapters/text.ts packages -g '*.ts' | head -n 80

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the call site that builds structuredCallOptions and invokes structuredOutputStream
sed -n '1990,2055p' packages/ai/src/activities/chat/index.ts

echo
echo "--- runId plumbing around the chat activity ---"
sed -n '640,720p' packages/ai/src/activities/chat/index.ts

echo
echo "--- structured call option types ---"
sed -n '850,1010p' packages/ai/src/types.ts

Repository: TanStack/ai

Length of output: 13293


Use a single resolved runId for structured-output errors. processStreamChunks(...) falls back to generateId(this.name), but structuredStreamError(...) still reads options.runId. The chat activity passes runId: this.runIdOverride, so when the caller omits it the RUN_ERROR events end up with runId: undefined and can’t be correlated. Resolve runId once in structuredOutputStream and pass that same options object to processStreamChunks(...) and each structuredStreamError(...) call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-gemini/src/adapters/text.ts` around lines 229 - 283, The
structuredOutputStream method uses inconsistent run IDs between stream
processing and structured-stream errors. Resolve a single runId, using the
existing override-or-generated fallback behavior, create an options object
containing it, and pass that same object to processStreamChunks and every
structuredStreamError call so all emitted events share the resolved ID.

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { z } from 'zod'
import { chat, summarize } from '@tanstack/ai'
import { chat, EventType, summarize } from '@tanstack/ai'

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

ESLint sort-imports error on the @tanstack/ai import in both test files. Adding EventType to the named import left the members out of alphabetical order, which the rule reports as an error and fails lint CI. Reorder the named members in each import to satisfy sort-imports.

  • packages/ai-gemini/tests/gemini-adapter.test.ts#L3-L3: reorder members in import { chat, EventType, summarize } from '@tanstack/ai'.
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts#L3-L3: reorder members in import { chat, EventType } from '@tanstack/ai'.
🧰 Tools
🪛 ESLint

[error] 3-3: Member 'EventType' of the import declaration should be sorted alphabetically.

(sort-imports)

📍 Affects 2 files
  • packages/ai-gemini/tests/gemini-adapter.test.ts#L3-L3 (this comment)
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts#L3-L3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-gemini/tests/gemini-adapter.test.ts` at line 3, Reorder the named
imports from `@tanstack/ai` alphabetically to satisfy ESLint sort-imports in
packages/ai-gemini/tests/gemini-adapter.test.ts:3-3 and
packages/ai-gemini/tests/text-interactions-adapter.test.ts:3-3; update only the
import member ordering.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ai-gemini): native structuredOutputStream for geminiText and geminiTextInteractions

1 participant