Skip to content

Commit 50dc5ef

Browse files
feat(ai): add toolInputs option, and stepsUsage (#222)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b0c26d5 commit 50dc5ef

File tree

7 files changed

+950
-110
lines changed

7 files changed

+950
-110
lines changed

apps/docs/content/3.core-concepts/11.ai-sdk.md

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: AI SDK Integration
33
description: Capture token usage, tool calls, model info, and streaming metrics from the Vercel AI SDK into wide events. Wrap your model and get full AI observability.
44
navigation:
5-
icon: i-lucide-scan-eye
5+
icon: i-simple-icons-vercel
66
links:
77
- label: Wide Events
88
icon: i-lucide-layers
@@ -112,15 +112,47 @@ Your wide event now includes:
112112

113113
## How It Works
114114

115-
`createAILogger(log)` returns an `AILogger` with two methods:
115+
`createAILogger(log, options?)` returns an `AILogger` with two methods:
116116

117117
| Method | Description |
118118
|--------|-------------|
119-
| `wrap(model)` | Wraps a language model with middleware. Accepts a model string (e.g. `'anthropic/claude-sonnet-4.6'`) or a `LanguageModelV3` object. Works with `generateText`, `streamText`, `generateObject`, `streamObject`, and `ToolLoopAgent`. |
119+
| `wrap(model)` | Wraps a language model with middleware. Accepts a model string (e.g. `'anthropic/claude-sonnet-4.6'`) or a `LanguageModelV3` object. Works with `generateText`, `streamText`, `generateObject`, `streamObject`, and `ToolLoopAgent`. Also works with pre-wrapped models (e.g. from supermemory). |
120120
| `captureEmbed(result)` | Manually captures token usage from `embed()` or `embedMany()` results (embedding models use a different type). |
121121

122122
The middleware intercepts calls at the provider level. It does not touch your callbacks, prompts, or responses. Captured data flows through the normal evlog pipeline (sampling, enrichers, drains) and ends up in Axiom, Better Stack, or wherever you drain to.
123123

124+
### Options
125+
126+
| Option | Type | Default | Description |
127+
|--------|------|---------|-------------|
128+
| `toolInputs` | `boolean \| ToolInputsOptions` | `false` | When enabled, `toolCalls` contains `{ name, input }` objects instead of plain strings. Opt-in because inputs can be large and may contain sensitive data. |
129+
130+
Pass `true` to capture all inputs as-is, or an options object for fine-grained control:
131+
132+
| Sub-option | Type | Description |
133+
|------------|------|-------------|
134+
| `maxLength` | `number` | Truncate stringified inputs exceeding this character length (appends ``) |
135+
| `transform` | `(input, toolName) => unknown` | Custom transform applied before `maxLength`. Use to redact fields or reshape data. |
136+
137+
```typescript
138+
// Capture everything
139+
const ai = createAILogger(log, { toolInputs: true })
140+
141+
// Truncate long inputs (e.g. SQL queries)
142+
const ai = createAILogger(log, { toolInputs: { maxLength: 200 } })
143+
144+
// Redact sensitive tool inputs
145+
const ai = createAILogger(log, {
146+
toolInputs: {
147+
maxLength: 500,
148+
transform: (input, toolName) => {
149+
if (toolName === 'queryDB') return { sql: '***' }
150+
return input
151+
},
152+
},
153+
})
154+
```
155+
124156
## Usage Patterns
125157

126158
### streamText
@@ -182,7 +214,9 @@ import { createAILogger } from 'evlog/ai'
182214

183215
export default defineEventHandler(async (event) => {
184216
const log = useLogger(event)
185-
const ai = createAILogger(log)
217+
const ai = createAILogger(log, {
218+
toolInputs: { maxLength: 500 },
219+
})
186220

187221
const agent = new ToolLoopAgent({
188222
model: ai.wrap('anthropic/claude-sonnet-4.6'),
@@ -210,7 +244,17 @@ Wide event after a 3-step agent run:
210244
"outputTokens": 1200,
211245
"totalTokens": 5700,
212246
"finishReason": "stop",
213-
"toolCalls": ["searchWeb", "queryDatabase", "searchWeb"],
247+
"toolCalls": [
248+
{ "name": "searchWeb", "input": { "query": "TypeScript 6.0 features" } },
249+
{ "name": "queryDatabase", "input": { "sql": "SELECT * FROM docs WHERE topic = 'typescript'" } },
250+
{ "name": "searchWeb", "input": { "query": "TypeScript 6.0 release date" } }
251+
],
252+
"responseId": "msg_01XFDUDYJgAACzvnptvVoYEL",
253+
"stepsUsage": [
254+
{ "model": "claude-sonnet-4.6", "inputTokens": 1200, "outputTokens": 300, "toolCalls": ["searchWeb"] },
255+
{ "model": "claude-sonnet-4.6", "inputTokens": 1500, "outputTokens": 400, "toolCalls": ["queryDatabase", "searchWeb"] },
256+
{ "model": "claude-sonnet-4.6", "inputTokens": 1800, "outputTokens": 500 }
257+
],
214258
"msToFirstChunk": 312,
215259
"msToFinish": 8200,
216260
"tokensPerSecond": 146
@@ -302,13 +346,42 @@ const model = ai.wrap(anthropic('claude-sonnet-4.6'))
302346
| `ai.cacheWriteTokens` | `usage.inputTokens.cacheWrite` | Tokens written to prompt cache |
303347
| `ai.reasoningTokens` | `usage.outputTokens.reasoning` | Reasoning tokens (extended thinking) |
304348
| `ai.finishReason` | `finishReason.unified` | Why generation ended (`stop`, `tool-calls`, etc.) |
305-
| `ai.toolCalls` | Content / stream chunks | List of tool names called |
349+
| `ai.toolCalls` | Content / stream chunks | `string[]` of tool names by default, or `Array<{ name, input }>` when `toolInputs` is enabled |
350+
| `ai.responseId` | `response.id` | Provider-assigned response ID (e.g. Anthropic's `msg_...`) |
306351
| `ai.steps` | Step count | Number of LLM calls (only when > 1) |
352+
| `ai.stepsUsage` | Per-step accumulation | Per-step token and tool call breakdown (only when > 1 step) |
307353
| `ai.msToFirstChunk` | Stream timing | Time to first text chunk (streaming only) |
308354
| `ai.msToFinish` | Stream timing | Total stream duration (streaming only) |
309355
| `ai.tokensPerSecond` | Computed | Output tokens per second (streaming only) |
310356
| `ai.error` | Error capture | Error message if a model call fails |
311357

358+
## Composability
359+
360+
`ai.wrap()` works with models that are already wrapped by other tools. If you use supermemory, guardrails middleware, or any other model wrapper, pass the wrapped model to `ai.wrap()`:
361+
362+
```typescript
363+
import { createAILogger } from 'evlog/ai'
364+
import { withSupermemory } from '@supermemory/tools/ai-sdk'
365+
366+
const ai = createAILogger(log)
367+
const base = gateway('anthropic/claude-sonnet-4.6')
368+
const model = ai.wrap(withSupermemory(base, orgId, { mode: 'full' }))
369+
```
370+
371+
For explicit middleware composition, use `createAIMiddleware` to get the raw middleware and compose it yourself via `wrapLanguageModel`:
372+
373+
```typescript
374+
import { createAIMiddleware } from 'evlog/ai'
375+
import { wrapLanguageModel } from 'ai'
376+
377+
const model = wrapLanguageModel({
378+
model: base,
379+
middleware: [createAIMiddleware(log, { toolInputs: true }), otherMiddleware],
380+
})
381+
```
382+
383+
`createAIMiddleware` returns the same middleware that `createAILogger` uses internally. The difference: `createAIMiddleware` does not include `captureEmbed` (embedding models don't use middleware). Use `createAILogger` for the full API, `createAIMiddleware` when you need explicit middleware ordering.
384+
312385
## Error Handling
313386

314387
If a model call fails, the middleware captures the error into the wide event before re-throwing:

apps/nuxthub-playground/app/components/LogGenerator.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ async function fireAll() {
3232
<button @click="fire('/api/test/warn')">
3333
Slow Request
3434
</button>
35+
<button @click="fire('/api/test/ai-wrap')">
36+
AI Wrap Composition
37+
</button>
3538
<button @click="fireAll">
3639
Fire All (x3)
3740
</button>
3841
</div>
39-
<p v-if="lastResult" style="margin-top: 0.5rem; color: #666; font-size: 0.85rem;">
42+
<p v-if="lastResult" style="margin-top: 0.5rem; color: #666; font-size: 0.85rem; word-break: break-all; overflow-wrap: anywhere;">
4043
{{ lastResult }}
4144
</p>
4245
</section>

apps/nuxthub-playground/server/api/chat.post.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default defineEventHandler(async (event) => {
6363

6464
logger.set({ action: 'chat', messagesCount: messages.length })
6565

66-
const ai = createAILogger(logger)
66+
const ai = createAILogger(logger, { toolInputs: true })
6767

6868
try {
6969
const agent = new ToolLoopAgent({
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { gateway, generateText, wrapLanguageModel } from 'ai'
2+
import type { LanguageModelV3Middleware } from '@ai-sdk/provider'
3+
import { createAILogger } from 'evlog/ai'
4+
5+
/**
6+
* Simulates an external middleware (supermemory, guardrails, etc.)
7+
* that injects a system message — proves the middleware actually ran in the chain.
8+
*/
9+
const externalMiddleware: LanguageModelV3Middleware = {
10+
specificationVersion: 'v3',
11+
transformParams({ params }) {
12+
return Promise.resolve({
13+
...params,
14+
prompt: [
15+
{ role: 'system' as const, content: 'Always start your answer with "MIDDLEWARE_OK:"' },
16+
...params.prompt,
17+
],
18+
})
19+
},
20+
}
21+
22+
export default defineEventHandler(async (event) => {
23+
const logger = useLogger(event)
24+
logger.set({ action: 'test-ai-wrap-composition' })
25+
26+
const ai = createAILogger(logger, { toolInputs: true })
27+
28+
const base = gateway('google/gemini-3-flash')
29+
const preWrapped = wrapLanguageModel({ model: base, middleware: externalMiddleware })
30+
const model = ai.wrap(preWrapped)
31+
32+
const result = await generateText({
33+
model,
34+
prompt: 'Say hello.',
35+
maxOutputTokens: 200,
36+
})
37+
38+
const middlewareRan = result.text.startsWith('MIDDLEWARE_OK:')
39+
40+
return {
41+
status: 'ok',
42+
middlewareRan,
43+
text: result.text,
44+
}
45+
})

apps/playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
"typecheck": "vue-tsc --noEmit"
1313
},
1414
"dependencies": {
15+
"@nuxt/ui": "^4.5.1",
1516
"evlog": "workspace:*",
1617
"nuxt": "^4.4.2",
17-
"@nuxt/ui": "^4.5.1",
1818
"tailwindcss": "^4.2.1"
1919
}
2020
}

0 commit comments

Comments
 (0)