Options
createAILogger(log, options?) accepts a single options bag. Every option is opt-in. The defaults stay quiet, and they stay safe: nothing a model was sent or returned reaches your drain until you ask for it by name.
| Option | Type | Default | Description |
|---|---|---|---|
toolInputs | boolean | ToolInputsOptions | false | Capture tool call inputs alongside their names (off by default to avoid leaking sensitive data). |
prompt | boolean | CaptureOptions | false | Capture the prompt sent to the first model call as ai.prompt (off by default to avoid leaking sensitive data). |
output | boolean | CaptureOptions | false | Capture the text generated by the last model call as ai.output (off by default to avoid leaking sensitive data). |
cost | Record<string, ModelCost> | undefined | Pricing map. Keys are model IDs, values are { input, output } in dollars per 1M tokens. |
Tool Inputs
By default, ai.toolCalls is a string[] of tool names. Enable toolInputs to capture inputs too, which suits debugging agent behaviour or auditing what data the model reached for.
maxLength and transform before raw capture in production.Capture everything
const ai = createAILogger(log, { toolInputs: true })
Truncate long inputs
const ai = createAILogger(log, { toolInputs: { maxLength: 200 } })
Redact sensitive fields
const ai = createAILogger(log, {
toolInputs: {
maxLength: 500,
transform: (input, toolName) => {
if (toolName === 'queryDB') return { sql: '***' }
return input
},
},
})
| Sub-option | Type | Description |
|---|---|---|
maxLength | number | Truncate stringified inputs exceeding this character length (appends …). |
transform | (input, toolName) => unknown | Custom transform applied before maxLength. Use to redact fields or reshape data. |
When toolInputs is enabled, ai.toolCalls becomes an Array<{ name, input }> instead of a plain string array.
Prompt and Output Capture
By default, nothing the model was sent or returned reaches your drain. Two independent options turn capture on, one per direction: prompt records what was sent, output records what came back. Enable one, the other, or both.
const ai = createAILogger(log, { prompt: true, output: true })
The wide event then carries:
ai.prompt: the formatted text of the prompt sent to the first model call. One block per message prefixed with its role; text parts are inlined, and other part types become[tool-call name],[tool-result name]or[file]markers.ai.output: the text generated by the last model call, the final answer in a multi-step run. Streamed responses accumulate from the text chunks.
Capture only the direction you need:
const ai = createAILogger(log, { prompt: true }) // ai.prompt only
const ai = createAILogger(log, { output: true }) // ai.output only
maxLength and transform before raw capture in production.Truncate long content
Each option takes its own maxLength:
const ai = createAILogger(log, { prompt: { maxLength: 500 } })
Redact or reshape
transform receives the captured text and runs before maxLength:
const ai = createAILogger(log, {
prompt: {
maxLength: 500,
transform: text => redact(text),
},
})
| Sub-option | Type | Description |
|---|---|---|
maxLength | number | Truncate captured text exceeding this character length (appends …). |
transform | (content) => string | Custom transform applied before maxLength. |
createAILogger and createAIMiddleware. The standalone createEvlogIntegration observes telemetry events and has no access to the model call parameters, so ai.prompt and ai.output stay absent there.Cost Estimation
Pass a cost map to compute estimated dollar cost per call. The middleware multiplies token usage by the per-million rates and sets ai.estimatedCost on the wide event.
const ai = createAILogger(log, {
cost: {
'claude-sonnet-4.6': { input: 3, output: 15 },
'gpt-4o': { input: 2.5, output: 10 },
},
})
Read the result from your handler with ai.getEstimatedCost(), which suits billing dashboards or warning users before expensive calls.
cost map in one file alongside model selection so renaming a model in production also updates pricing. Per-route maps drift the moment two routes disagree about which model they call, so keep one.Error Handling
If a model call fails, the middleware captures the error into the wide event before re-throwing:
{
"ai": {
"calls": 1,
"model": "claude-sonnet-4.6",
"provider": "anthropic",
"finishReason": "error",
"error": "API rate limit exceeded"
}
}
Stream errors (e.g. content filter) are also captured from the stream's error chunks. Your error-handling code (try/catch, route-level error handlers) keeps working as usual, since the middleware only observes.