Skip to content

fix(ai): populate RUN_ERROR rawEvent with provider error body (#672)#673

Open
tombeckenham wants to merge 4 commits into
mainfrom
672-stream-adapters-collapse-provider-errors-to-message-code-and-never-populate-ag-ui-rawevent-upstream-error-detail-is-unrecoverable
Open

fix(ai): populate RUN_ERROR rawEvent with provider error body (#672)#673
tombeckenham wants to merge 4 commits into
mainfrom
672-stream-adapters-collapse-provider-errors-to-message-code-and-never-populate-ag-ui-rawevent-upstream-error-detail-is-unrecoverable

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

@tombeckenham tombeckenham commented Jun 1, 2026

Closes #672.

Problem

When a streaming chat call failed, the RUN_ERROR event reaching consumers carried only an opaque { message, code } headline (e.g. "Provider returned error"). Adapters discarded the provider's structured error body, and no adapter populated AG-UI's purpose-built rawEvent field — so the upstream cause (provider name, the upstream model's error body, rate-limit/overload/BYOK detail) was unrecoverable.

Changes

  • New helper toRunErrorRawEvent(error) (packages/ai/src/activities/error-payload.ts, exported via @tanstack/ai/adapter-internals). Extracts only known provider-body fields — error.rawEvent → object-valued error.errorerror.metadata — and never the raw SDK exception object (which can carry request metadata such as auth headers). Returns undefined so the field is omitted, not nulled. The { message, code } contract of toRunErrorPayload is untouched.
  • Systemic rawEvent population on every fatal-catch RUN_ERROR emission via a conditional spread:
    • packages/openai-basechat-completions-text.ts, responses-text.ts
    • packages/ai-openroutertext.ts (incl. preserving the mid-stream chunk.error on the rethrow) and responses-text.ts (incl. chunk.response.error sites)
    • packages/ai-anthropic/src/adapters/text.ts, packages/ai-gemini/src/adapters/text.ts
  • Consumer surfacing — the StreamProcessor now attaches code and rawEvent to the Error it surfaces via onError / useChat's error, so the upstream cause is recoverable in application code. Backward-compatible (message unchanged).

Tests

  • Unit: toRunErrorRawEvent coverage (priority order, string-error rejection, no header leakage); adapter emission tests in openai-base (forwards .error body / omits when absent) and ai-openrouter (mid-stream body → rawEvent).
  • E2E: error-handling.spec.ts asserts rawEvent survives SSE transport + the strip-to-spec middleware and reaches the consumer.
  • Verified locally: test:types, test:eslint (0 errors), unit suites (ai, openai-base, ai-anthropic, ai-gemini, ai-openrouter, ai-client), and the error-handling E2E spec — all green.

⚠️ Note on the OpenRouter mid-stream repro

The original repro (recovering OpenRouter's error.metadata from a mid-stream error) is not achievable on @openrouter/sdk@0.12.35: the SDK parses each in-band stream chunk's error through a strict Zod schema ({ code, message }), stripping metadata before the adapter sees chunk.error. The metadata survives only on pre-stream HTTP errors (rate-limit / overload / BYOK rejection), whose typed error class exposes the full body via .error — which the new helper forwards. Documented in code comments and the changeset.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Error events now include a provider-provided structured error payload in a conditional rawEvent field, alongside existing error codes — improving visibility of upstream error details without changing existing error contracts.
  • Tests

    • Added unit and end-to-end tests across adapters and tooling to verify rawEvent is correctly surfaced and preserved through streaming and SSE paths.

Streaming chat adapters previously collapsed provider errors to an opaque
`{ message, code }` headline and never populated AG-UI's purpose-built
`rawEvent` field, so the upstream provider detail was unrecoverable.

Add a `toRunErrorRawEvent` helper that extracts only known provider-body
fields (`error.rawEvent` -> object-valued `error.error` -> `error.metadata`),
never the raw SDK exception (which can carry auth headers). Wire it into every
fatal-catch RUN_ERROR emission across openai-base (chat-completions +
responses), ai-openrouter (text + responses, incl. the mid-stream `chunk.error`
rethrow), ai-anthropic, and ai-gemini. The `{ message, code }` contract of
`toRunErrorPayload` is unchanged.

The StreamProcessor now also attaches `code` and `rawEvent` to the Error it
surfaces via `onError` / `useChat`'s `error`, so the upstream cause is
recoverable in application code.

Note: the OpenRouter SDK parses each in-band stream chunk's `error` through a
strict schema (`{ code, message }`), so provider `metadata` survives only on
pre-stream HTTP errors, whose typed error class exposes the full body via
`.error`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

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: 9e4db3ed-1fcc-41ab-ba03-561b59985535

📥 Commits

Reviewing files that changed from the base of the PR and between 740653f and ffa91a9.

📒 Files selected for processing (7)
  • .gitignore
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/ai-gemini/tests/gemini-adapter.test.ts
  • packages/ai-openrouter/tests/openrouter-responses-adapter.test.ts
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/tests/stream-processor.test.ts
  • packages/openai-base/tests/responses-text.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai/src/activities/chat/stream/processor.ts

📝 Walkthrough

Walkthrough

This PR extends RUN_ERROR events emitted by chat adapters to include a rawEvent field populated with provider-structured error bodies. A new toRunErrorRawEvent helper extracts provider metadata (prioritizing explicit rawEvent, then object-valued error, then metadata) while filtering out raw SDK exceptions. All text adapters now compute and conditionally forward rawEvent alongside the existing sanitized { message, code } contract, enabling consumers to recover upstream error detail.

Changes

Error Enrichment with Provider Metadata

Layer / File(s) Summary
Core toRunErrorRawEvent helper and exports
packages/ai/src/activities/error-payload.ts, packages/ai/src/adapter-internals.ts, packages/ai/tests/error-payload.test.ts, .changeset/runerror-raw-event.md
New toRunErrorRawEvent() exported helper extracts provider-shaped error bodies by priority (rawEvent → object-valued errormetadata) from caught values, returning undefined for non-structured inputs. Re-exported alongside toRunErrorPayload. Unit tests and a changeset were added.
Stream processor enriches error objects with upstream details
packages/ai/src/activities/chat/stream/processor.ts, packages/ai/tests/stream-processor.test.ts
handleRunErrorEvent now constructs an Error and conditionally attaches optional code and rawEvent from incoming RUN_ERROR chunks before invoking events.onError; test verifies these properties are forwarded.
OpenRouter adapter: preserve and forward structured error details
packages/ai-openrouter/src/adapters/text.ts, packages/ai-openrouter/src/adapters/responses-text.ts, packages/ai-openrouter/tests/*
OpenRouter adapters import toRunErrorRawEvent, compute rawEvent in chatStream, structuredOutputStream, and processStreamChunks, rethrow mid-stream chunk.error with preserved rawEvent, and emit RUN_ERROR with optional rawEvent. Tests assert in-stream and outer-catch handling; aborts omit rawEvent.
OpenAI-base, Anthropic, and Gemini adapters: consistent rawEvent integration
packages/openai-base/src/adapters/*, packages/ai-anthropic/src/adapters/text.ts, packages/ai-gemini/src/adapters/text.ts, tests under packages/openai-base/tests/*, packages/ai-anthropic/tests/*, packages/ai-gemini/tests/*
Adapters follow the same pattern: import toRunErrorRawEvent, compute rawEvent in error handlers, and conditionally include rawEvent on emitted RUN_ERROR events when available. Tests verify that provider-bodied errors populate rawEvent and plain errors omit it.
Test coverage: helper unit tests, adapter integration tests, and E2E verification
packages/ai/tests/error-payload.test.ts, packages/ai-openrouter/tests/*, packages/openai-base/tests/*, testing/e2e/src/routes/*, testing/e2e/tests/*
Unit tests cover extraction priority and security boundary; adapter tests verify rawEvent forwarding; E2E tests serialize rawEvent into hidden DOM metadata and assert it survives SSE/middleware to consumers.
Misc / Gitignore
.gitignore
Adds .claude/scheduled_tasks.lock to ignore list.

Sequence Diagram(s)

sequenceDiagram
  participant Adapter
  participant Extract
  participant StreamProc
  participant Events
  Adapter->>Extract: compute rawEvent from caught error
  Adapter->>StreamProc: emit RUN_ERROR {message, code, rawEvent?}
  StreamProc->>StreamProc: build Error(message) and attach code/rawEvent
  StreamProc->>Events: call onError(Error with code/rawEvent)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TanStack/ai#411: Adds stripToSpecMiddleware that strips rawEvent globally; related to RUN_ERROR wire-shape and how rawEvent is handled downstream.

Suggested reviewers

  • AlemTuzlak

Poem

I’m a rabbit in the logs, nose twitching at the scene,
I found the hidden errors tucked between machine,
Now adapters whisper metadata, raw and true,
No auth leaks, just provider truth — hop, hop, woohoo! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main change: populating RUN_ERROR rawEvent with provider error body, matching the core objective of issue #672.
Description check ✅ Passed The PR description is comprehensive, detailing the problem, changes, tests, and verification. However, it lacks explicit checklist items from the provided template (Contributing guide and changeset confirmation).
Linked Issues check ✅ Passed The PR comprehensively addresses all objectives from issue #672: new toRunErrorRawEvent helper with security boundary, systemic rawEvent population across adapters, and consumer surfacing via StreamProcessor.
Out of Scope Changes check ✅ Passed All changes are directly related to populating RUN_ERROR rawEvent with provider error bodies. The .gitignore change is minimal and reasonable. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 672-stream-adapters-collapse-provider-errors-to-message-code-and-never-populate-ag-ui-rawevent-upstream-error-detail-is-unrecoverable

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🚀 Changeset Version Preview

13 package(s) bumped directly, 17 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-anthropic 0.11.2 → 1.0.0 Changeset
@tanstack/ai-code-mode 0.1.24 → 1.0.0 Changeset
@tanstack/ai-code-mode-skills 0.1.24 → 1.0.0 Changeset
@tanstack/ai-gemini 0.12.1 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.10.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.7.1 → 1.0.0 Changeset
@tanstack/ai-react 0.13.1 → 1.0.0 Changeset
@tanstack/ai-solid 0.11.1 → 1.0.0 Changeset
@tanstack/ai-svelte 0.11.1 → 1.0.0 Changeset
@tanstack/ai-vue 0.11.1 → 1.0.0 Changeset
@tanstack/openai-base 0.4.1 → 1.0.0 Changeset
@tanstack/ai-elevenlabs 0.2.14 → 1.0.0 Dependent
@tanstack/ai-event-client 0.4.2 → 1.0.0 Dependent
@tanstack/ai-fal 0.7.17 → 1.0.0 Dependent
@tanstack/ai-grok 0.9.1 → 1.0.0 Dependent
@tanstack/ai-groq 0.2.8 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.24 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.24 → 1.0.0 Dependent
@tanstack/ai-ollama 0.6.23 → 1.0.0 Dependent
@tanstack/ai-openai 0.10.4 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.4 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.4 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.23.1 → 0.24.0 Changeset
@tanstack/ai-client 0.14.1 → 0.15.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-devtools-core 0.4.2 → 0.4.3 Dependent
@tanstack/ai-isolate-cloudflare 0.2.15 → 0.2.16 Dependent
@tanstack/ai-vue-ui 0.2.8 → 0.2.9 Dependent
@tanstack/preact-ai-devtools 0.1.45 → 0.1.46 Dependent
@tanstack/react-ai-devtools 0.2.45 → 0.2.46 Dependent
@tanstack/solid-ai-devtools 0.2.45 → 0.2.46 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jun 1, 2026

View your CI Pipeline Execution ↗ for commit ffa91a9

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 10s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-01 06:45:11 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@673

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@673

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@673

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@673

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@673

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@673

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@673

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@673

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@673

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@673

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@673

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@673

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@673

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@673

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@673

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@673

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@673

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@673

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@673

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@673

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@673

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@673

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@673

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@673

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@673

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@673

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@673

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@673

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@673

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@673

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@673

commit: ffa91a9

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai-anthropic/src/adapters/text.ts (1)

183-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize the Anthropic logger payloads before attaching rawEvent.

Both catch blocks still send the raw SDK exception to logger.errors(...). That can leak request metadata to user loggers and undercuts the security boundary this PR is adding around rawEvent. Please switch these paths to toRunErrorPayload(...) and only spread code when defined.

🛡️ Suggested pattern
-import { toRunErrorRawEvent } from '`@tanstack/ai/adapter-internals`'
+import {
+  toRunErrorPayload,
+  toRunErrorRawEvent,
+} from '`@tanstack/ai/adapter-internals`'

     } catch (error: unknown) {
-      const err = error as Error & { status?: number; code?: string }
+      const errorPayload = toRunErrorPayload(
+        error,
+        'anthropic.chatStream failed',
+      )
       const rawEvent = toRunErrorRawEvent(error)
       logger.errors('anthropic.chatStream fatal', {
-        error,
+        error: errorPayload,
         source: 'anthropic.chatStream',
       })
       yield {
         type: EventType.RUN_ERROR,
         model: options.model,
         timestamp: Date.now(),
-        message: err.message || 'Unknown error occurred',
-        code: err.code || String(err.status),
+        message: errorPayload.message,
+        ...(errorPayload.code !== undefined && { code: errorPayload.code }),
         ...(rawEvent !== undefined && { rawEvent }),
         error: {
-          message: err.message || 'Unknown error occurred',
-          code: err.code || String(err.status),
+          message: errorPayload.message,
+          ...(errorPayload.code !== undefined && { code: errorPayload.code }),
         },
       }

Apply the same pattern in processAnthropicStream.

Also applies to: 1155-1174

🤖 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-anthropic/src/adapters/text.ts` around lines 183 - 203, The catch
block is currently logging the raw SDK exception and attaching rawEvent via
toRunErrorRawEvent, which may leak sensitive request metadata; replace
logger.errors(..., { error, ... }) with a sanitized payload using
toRunErrorPayload(error) and when building the yielded object use the sanitized
payload (e.g., payload = toRunErrorPayload(error)) instead of rawEvent, and only
spread code into the event/error objects when payload.code is defined; apply the
same change in processAnthropicStream and any other catch blocks that call
toRunErrorRawEvent or pass the raw error to logger.errors so all logs and
emitted RUN_ERROR events use toRunErrorPayload(...) and conditional spreading of
code.
🤖 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.

Outside diff comments:
In `@packages/ai-anthropic/src/adapters/text.ts`:
- Around line 183-203: The catch block is currently logging the raw SDK
exception and attaching rawEvent via toRunErrorRawEvent, which may leak
sensitive request metadata; replace logger.errors(..., { error, ... }) with a
sanitized payload using toRunErrorPayload(error) and when building the yielded
object use the sanitized payload (e.g., payload = toRunErrorPayload(error))
instead of rawEvent, and only spread code into the event/error objects when
payload.code is defined; apply the same change in processAnthropicStream and any
other catch blocks that call toRunErrorRawEvent or pass the raw error to
logger.errors so all logs and emitted RUN_ERROR events use
toRunErrorPayload(...) and conditional spreading of code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec50406a-df0a-4696-8a9f-b709740552a5

📥 Commits

Reviewing files that changed from the base of the PR and between 8036b50 and 740653f.

📒 Files selected for processing (17)
  • .changeset/runerror-raw-event.md
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-openrouter/src/adapters/responses-text.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/tests/openrouter-adapter.test.ts
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/src/activities/error-payload.ts
  • packages/ai/src/adapter-internals.ts
  • packages/ai/tests/error-payload.test.ts
  • packages/openai-base/src/adapters/chat-completions-text.ts
  • packages/openai-base/src/adapters/responses-text.ts
  • packages/openai-base/tests/chat-completions-text.test.ts
  • testing/e2e/src/routes/api.tools-test.ts
  • testing/e2e/src/routes/tools-test.tsx
  • testing/e2e/tests/error-handling.spec.ts
  • testing/e2e/tests/tools-test/helpers.ts

Add tests verifying provider error bodies are forwarded as
RUN_ERROR.rawEvent for Anthropic, Gemini, OpenRouter, and openai-base
adapters, and that StreamProcessor attaches code/rawEvent to onError.
Replace an `any` cast in the processor with a typed runId guard, and
ignore the local scheduled_tasks.lock file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham requested a review from AlemTuzlak June 1, 2026 03:51
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.

Stream adapters collapse provider errors to {message, code} and never populate AG-UI rawEvent — upstream error detail is unrecoverable

2 participants