Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 150 additions & 0 deletions src/blog/debug-logging-for-tanstack-ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: 'One Flag, Every Chunk: Debug Logging Lands in TanStack AI'
published: 2026-04-22
excerpt: "Your AI pipeline is a black box: a missing chunk, a middleware that doesn't fire, a tool call with mystery args. TanStack AI now ships pluggable, category-toggleable debug logging across every activity and adapter. Flip one flag and the pipeline prints itself."
authors:
- Alem Tuzlak
---

![Debug Logging for TanStack AI](/blog-assets/debug-logging-for-tanstack-ai/header.png)

You kick off a `chat()` call. A chunk goes missing. A middleware you wrote last week doesn't seem to fire. A tool gets called with arguments you can't explain. Your stream finishes, the UI looks wrong, and you have no idea which layer lied to you.

Up until now your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add comma after introductory phrase.

The sentence should read: "Up until now**,** your options were limited."

📝 Proposed fix
-Up until now your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued.
+Up until now, your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Up until now your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued.
Up until now, your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/blog/debug-logging-for-tanstack-ai.md` at line 13, Add a comma after the
introductory phrase "Up until now" in the sentence that begins "Up until now
your options were limited." so it reads "Up until now, your options were
limited."—locate the sentence in the content block (the line starting with "Up
until now your options were limited.") and insert the comma immediately after
"now".


TanStack AI now has a built-in answer. **Flip one flag and the entire pipeline prints itself.**

## Turn it on

Add `debug: true` to any activity call:

```typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'

const stream = chat({
adapter: openaiText(),
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello' }],
debug: true,
})
```

Every internal event now prints to the console, prefixed with an emoji-tagged category so you can scan dense streaming logs without squinting:

```
📤 [tanstack-ai:request] 📤 activity=chat provider=openai model=gpt-4o messages=1 tools=0 stream=true
🔁 [tanstack-ai:agentLoop] 🔁 run started
📥 [tanstack-ai:provider] 📥 provider=openai type=response.output_text.delta
📨 [tanstack-ai:output] 📨 type=TEXT_MESSAGE_CONTENT
🧩 [tanstack-ai:middleware] 🧩 hook=onOutput
🔧 [tanstack-ai:tools] 🔧 tool=getTodos phase=before
```
Comment on lines +35 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language specifier to fenced code block.

The console output example should specify a language (e.g., text or console) for proper rendering and accessibility.

📝 Proposed fix
-```
+```text
 📤 [tanstack-ai:request] 📤 activity=chat provider=openai model=gpt-4o messages=1 tools=0 stream=true
 🔁 [tanstack-ai:agentLoop] 🔁 run started
 📥 [tanstack-ai:provider] 📥 provider=openai type=response.output_text.delta
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
📤 [tanstack-ai:request] 📤 activity=chat provider=openai model=gpt-4o messages=1 tools=0 stream=true
🔁 [tanstack-ai:agentLoop] 🔁 run started
📥 [tanstack-ai:provider] 📥 provider=openai type=response.output_text.delta
📨 [tanstack-ai:output] 📨 type=TEXT_MESSAGE_CONTENT
🧩 [tanstack-ai:middleware] 🧩 hook=onOutput
🔧 [tanstack-ai:tools] 🔧 tool=getTodos phase=before
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 35-35: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/blog/debug-logging-for-tanstack-ai.md` around lines 35 - 42, The fenced
code block showing the console output (the block starting with the lines like
"📤 [tanstack-ai:request] ..." and "🔧 [tanstack-ai:tools] ...") lacks a
language specifier; update the opening fence from ``` to a language such as
```text or ```console so the snippet renders/accessibility is improved (e.g.,
change the opening fence to "```text" while leaving the block contents
unchanged).


That is the whole setup. No exporters, no sidecar, no dashboard. Just what your pipeline is actually doing, right now, in the terminal you already have open.

## Eight categories, not a log level

Most logging libraries give you `debug`, `info`, `warn`, `error` and ask you to pick one. That mapping is wrong for an AI pipeline. **The noise isn't a severity, it's a source.** When you're chasing a tool bug you don't want provider chunks. When you're chasing a provider bug you don't want middleware chatter.

So `debug` accepts a config object where every category toggles independently:

```typescript
chat({
adapter: openaiText(),
model: 'gpt-4o',
messages,
debug: { middleware: false }, // everything except middleware
})
```

Omitted categories default to `true`, so the common case is "turn off the one thing that's drowning you." Every category maps to a real pipeline surface:

| Category | What it logs |
| ------------ | -------------------------------------------------------------- |
| `request` | Outgoing call to a provider (model, message count, tool count) |
| `provider` | Every raw chunk or frame from the provider SDK |
| `output` | Every chunk or result yielded to the caller |
| `middleware` | Inputs and outputs around every middleware hook |
| `tools` | Before and after tool call execution |
| `agentLoop` | Agent-loop iterations and phase transitions |
| `config` | Config transforms returned by middleware `onConfig` hooks |
| `errors` | Every caught error anywhere in the pipeline |

Chat-only categories like `tools` and `agentLoop` just don't fire for `summarize()` or `generateImage()`, because they don't exist in those pipelines. You don't have to think about it.

## Pipe it anywhere

`console.log` is the right default for local work. It is the wrong default for production, where you want structured JSON going to a log shipper, not ANSI colors going to stdout.

Pass your own `Logger` and the entire category system routes through it:

```typescript
import type { Logger } from '@tanstack/ai'
import pino from 'pino'

const pinoLogger = pino()
const logger: Logger = {
debug: (msg, meta) => pinoLogger.debug(meta, msg),
info: (msg, meta) => pinoLogger.info(meta, msg),
warn: (msg, meta) => pinoLogger.warn(meta, msg),
error: (msg, meta) => pinoLogger.error(meta, msg),
}

chat({
adapter: openaiText(),
model: 'gpt-4o',
messages,
debug: { logger },
})
```

The `Logger` interface is four methods. Anything that writes a line of text fits. Pino, winston, bunyan, a `fetch` to a logging service, a no-op that forwards to your existing observability layer. All valid.

### Your logger can't break the pipeline

This is the detail we lost sleep over. If your custom logger throws (a cyclic-meta `JSON.stringify`, a transport that rejects synchronously, a typo in a bound `this`), the exception should **not** bubble up and mask the real error that triggered the log call in the first place.

Internally, every user-logger invocation is wrapped in a try/catch. A broken logger silently drops the log line. Your actual pipeline error still reaches you through thrown exceptions and `RUN_ERROR` chunks, exactly where you were looking for it.

If you need to know when your own logger is failing, guard inside your implementation. The SDK will not guess how loud you want logger failures to be.

## Every activity, every provider

Debug logging isn't a chat-only feature. Every activity in TanStack AI accepts the same option:

```typescript
summarize({ adapter, text, debug: true })
generateImage({ adapter, prompt: 'a cat', debug: { logger } })
generateSpeech({ adapter, text, debug: { request: true } })
generateTranscription({ adapter, audio, debug: true })
generateVideo({ adapter, prompt, debug: true })
```

Realtime session adapters (`openaiRealtime`, `elevenlabsRealtime`) take it too.

On the provider side, **every adapter in every provider package is wired through the structured logger**: OpenAI, Anthropic, Gemini, Grok, Groq, Ollama, OpenRouter, fal, and ElevenLabs. 25 adapters total. Zero ad-hoc `console.*` calls remain in adapter source code. Whether you're debugging an Anthropic text stream or an ElevenLabs realtime session, the output shape is the same.

## The small decisions that add up

A few calls that look cosmetic but matter once you're staring at a thousand-line log.

**Emoji prefixes on both sides of the tag.** `📨 [tanstack-ai:output] 📨 ...` reads faster than raw brackets in a dense stream, and your eye can hop categories without parsing text.

**`console.dir` with `depth: null`.** Node's default console formatting stops at depth 2, so nested provider chunks render as `[Object]` and you lose the thing you were trying to see. Debug logs surface the entire structure. In browsers, the raw object still lands in DevTools for interactive inspection.

**Errors log unconditionally.** You don't have to remember to turn them on. If you really want total silence, `debug: false` or `debug: { errors: false }` does it. Otherwise errors flow through even when you haven't asked for any other category.

**Internal devtools middleware is muted.** If you have the TanStack AI devtools middleware installed, its own hooks don't flood the `middleware` category. You see the middleware **you** wrote, not the plumbing.

Each of these is a small call on its own. Together they're the difference between "debug output I actually read" and "debug output I pipe to `/dev/null` within ten seconds."

## Getting it

Debug logging ships in the latest `@tanstack/ai`. It's additive, backward-compatible, and available on every activity today. No config file, no exporter, no platform.

Upgrade, add `debug: true` to the call you can't explain, and read the output.

For the full reference, see the [Debug Logging guide](https://tanstack.com/ai/latest/docs/advanced/debug-logging). And if you want an even faster turnaround: TanStack AI ships an agent skill under `packages/typescript/ai/skills/` so your LLM-powered dev tools can discover the flag on their own.

One flag. Every chunk. Your streams are no longer a black box.
Loading