Debug Logging
Dependency note: This skill builds on ai-core. Read it first for critical rules.
Use this skill when you need to turn debug logging on or off, narrow what's
printed, or pipe logs into a custom logger (pino, winston, etc.). The same
debug option works on every activity — chat(), summarize(),
generateImage(), generateSpeech(), generateTranscription(),
generateVideo().
Turn it on
typescriptimport { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] const stream = chat({ adapter: openaiText('gpt-5.5'), messages, debug: true, // all categories on, prints to console })
Each log line is prefixed with an emoji and [tanstack-ai:<category>]:
📤 [tanstack-ai:request] 📤 activity=chat provider=openai model=gpt-5.2 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
Turn it off
typescriptimport { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ adapter: openaiText('gpt-5.5'), messages, debug: false, // silence everything, including errors })
Omitting debug is not the same as debug: false. When omitted, the
errors category is still on (errors are cheap and important). Use
debug: false or debug: { errors: false } for true silence.
DebugOption — the accepted shapes
typescriptimport type { Logger } from '@tanstack/ai' // As exported by '@tanstack/ai' type DebugOption = boolean | DebugConfig interface DebugConfig { // Per-category flags. Any flag omitted from a DebugConfig defaults to true. request?: boolean provider?: boolean output?: boolean middleware?: boolean tools?: boolean agentLoop?: boolean config?: boolean errors?: boolean // Optional custom logger. Defaults to ConsoleLogger. logger?: Logger }
Resolution rules for the debug?: DebugOption field on every activity:
debug value | Effect |
|---|---|
omitted (undefined) | Only errors is active; default ConsoleLogger. |
true | All categories on; default ConsoleLogger. |
false | All categories off (including errors); default ConsoleLogger. |
DebugConfig object | Each unspecified flag defaults to true; logger replaces ConsoleLogger. |
Narrow what's printed
Pass a DebugConfig object. Unspecified categories default to true, so it's
easiest to toggle by setting specific flags to false:
typescriptimport { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ adapter: openaiText('gpt-5.5'), messages, debug: { middleware: false }, // everything except middleware })
To print only a specific set, set the rest to false explicitly:
typescriptimport { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ adapter: openaiText('gpt-5.5'), messages, debug: { provider: true, output: true, middleware: false, tools: false, agentLoop: false, config: false, errors: true, // keep errors on — they're cheap and important request: false, }, })
Pipe into your own logger
typescriptimport { chat, type Logger } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' 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), } const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ adapter: openaiText('gpt-5.5'), messages, debug: { logger }, // all categories on, piped to pino })
The default console logger is exported as ConsoleLogger if you want to wrap
it:
typescriptimport { ConsoleLogger } from '@tanstack/ai'
Categories
| Category | Logs | Applies to |
|---|---|---|
request | Outgoing call to a provider (model, message count, tool count) | All activities |
provider | Every raw chunk/frame received from a provider SDK | Streaming activities (chat, realtime) |
output | Every chunk or result yielded to the caller | All activities |
middleware | Inputs and outputs around every middleware hook | chat() only |
tools | Before/after tool call execution | chat() only |
agentLoop | Agent-loop iterations and phase transitions | chat() only |
config | Config transforms returned by middleware onConfig hooks | chat() only |
errors | Every caught error anywhere in the pipeline | All activities |
Chat-only categories simply never fire for non-chat activities — those concepts don't exist in their pipelines.
Non-chat activities
Same debug option everywhere:
typescriptimport { summarize, generateImage, generateSpeech, generateTranscription, generateVideo, } from '@tanstack/ai' import { openaiSummarize, openaiImage, openaiSpeech, openaiTranscription, openaiVideo, } from '@tanstack/ai-openai' import { logger } from './logger' import { audio } from './recording' summarize({ adapter: openaiSummarize('gpt-5.5'), text: 'Long article…', debug: true, }) generateImage({ adapter: openaiImage('gpt-image-2'), prompt: 'a cat', debug: { logger }, }) generateSpeech({ adapter: openaiSpeech('tts-1-hd'), text: 'Hello', debug: { request: true }, }) generateTranscription({ adapter: openaiTranscription('gpt-4o-transcribe'), audio, debug: false, }) generateVideo({ adapter: openaiVideo('sora-2'), prompt: 'a wave', debug: { output: true }, })
Realtime session adapters in provider packages (e.g. openaiRealtime,
elevenlabsRealtime) accept the same debug?: DebugOption on their session
options. They emit request, provider, and errors lines; the chat-only
categories don't apply.
Common Mistakes
a. HIGH: Treating omitted debug as silent
typescriptimport { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const adapter = openaiText('gpt-5.5') const messages = [{ role: 'user' as const, content: 'Hello' }] // WRONG — expecting this to be completely silent chat({ adapter, messages }) // Errors still print via [tanstack-ai:errors] ... on failure. // CORRECT — explicit silence chat({ adapter, messages, debug: false }) chat({ adapter, messages, debug: { errors: false } })
debug undefined means "only errors"; debug: false means "nothing at all".
Source: docs/advanced/debug-logging.md
b. MEDIUM: Reaching for middleware when debug would do
typescriptimport { chat, type ChatMiddleware } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const adapter = openaiText('gpt-5.5') const messages = [{ role: 'user' as const, content: 'Hello' }] // WRONG — writing logging middleware to see chunks flow const chunkLogger: ChatMiddleware = { name: 'chunk-logger', onChunk: (ctx, chunk) => { console.log(chunk.type, chunk) }, } chat({ adapter, messages, middleware: [chunkLogger] }) // CORRECT — just turn on the relevant categories chat({ adapter, messages, debug: { provider: true, output: true }, })
For observing the built-in pipeline, the debug option is strictly faster
than writing logging middleware. Reach for middleware when you need to
transform chunks, not just see them.
Source: docs/advanced/debug-logging.md
c. LOW: Logger implementation that can throw
A user-supplied Logger that throws will have its exception swallowed by the
SDK so it never masks the real error that triggered the log call. Still,
prefer implementations that don't throw — silenced exceptions are harder to
debug than loud ones.
typescriptimport type { Logger } from '@tanstack/ai' // WRONG — a logger that can throw on serialization const fragile: Logger = { debug: (msg, meta) => console.debug(msg, JSON.stringify(meta)), // cyclic meta → throws info: (msg, meta) => console.info(msg, JSON.stringify(meta)), warn: (msg, meta) => console.warn(msg, JSON.stringify(meta)), error: (msg, meta) => console.error(msg, JSON.stringify(meta)), } // CORRECT — guard serialization in the logger itself const guarded = (log: (...args: Array<unknown>) => void): Logger['debug'] => (msg, meta) => { try { log(msg, JSON.stringify(meta)) } catch { log(msg) // fall back to the bare message rather than throw } } const safe: Logger = { debug: guarded(console.debug), info: guarded(console.info), warn: guarded(console.warn), error: guarded(console.error), }
Source: packages/ai/src/logger/internal-logger.ts
Cross-References
- See also: ai-core/middleware/SKILL.md — if you need to transform chunks/config, not just observe them.
- See also: Observability (
docs/advanced/observability.md) — the programmatic event client for a richer, structured feed beyond log lines.

