npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ai-sdk-message-sanitizer

v0.1.3

Published

AI SDK middleware that sanitizes malformed message arrays before LLM API calls

Downloads

467

Readme

ai-sdk-message-sanitizer

AI SDK middleware that fixes malformed message arrays before they reach the LLM provider.

Some APIs (Anthropic in particular) reject calls with trailing assistant messages, empty content arrays, malformed assistant tool-call inputs, or tool results that appear out of order. These conditions arise naturally from context management, multi-step agents, and streaming — and they produce cryptic 400 errors. This middleware fixes them silently before the call goes out.

Installation

npm install ai @ai-sdk/provider ai-sdk-message-sanitizer

@opentelemetry/api is an optional peer dependency. Install it if you want span events.

Quick Start

import { wrapLanguageModel } from "ai";
import { createMessageSanitizerMiddleware } from "ai-sdk-message-sanitizer";

const model = wrapLanguageModel({
  model: baseModel,
  middleware: createMessageSanitizerMiddleware(),
});

That's it. Wrap the model and all calls through it are sanitized transparently.

What It Fixes

Malformed assistant tool-call inputs

Providers expect assistant tool-call parts to carry an object/dictionary input. In real agent traces, malformed tool calls sometimes survive into replayed history as strings, arrays, or other non-object payloads, which causes provider-side 400s on the next turn.

Before: [assistant: tool-call(input: "<parameter ...>")]
After:  [assistant: tool-call(input: { rawInput: "<parameter ...>", ... })]

The sanitizer wraps non-object inputs into a valid object so the prompt remains replayable, while preserving the raw malformed payload for debugging.

Trailing assistant messages

Anthropic's API rejects prompts where the last message is an assistant message without a tool call. This happens when context management strategies strip the user turn that normally follows, leaving a dangling assistant message.

Before: [user] → [assistant: "I found…"] ← Anthropic rejects this
After:  [user]

Assistant messages that end with tool calls are left untouched — they are valid and expected.

Empty content arrays

User and assistant messages with content: [] are invalid for most providers. These can arise from message construction code or after content is stripped by other middleware.

Before: [user: []] → [user: "Real question"]
After:  [user: "Real question"]

System messages (string content) and tool messages are never touched.

Misplaced tool results

In parallel or batched tool-call flows, tool results can end up in the wrong position: later in the prompt than the assistant block that issued the call. Providers require that each assistant block's tool results appear in the immediately following user/tool block.

Before:
  [assistant: call-A, call-B, call-C]
  [tool: result-A]                    ← only A resolved
  [assistant: call-D]                 ← B and C still dangling
  [tool: result-B, result-C, result-D]

After:
  [assistant: call-A, call-B, call-C]
  [tool: result-A, result-B, result-C]  ← all three resolved here
  [assistant: call-D]
  [tool: result-D]

It also repairs the stricter singleton shape that Anthropic rejects:

Before:
  [assistant: call-A]
  [assistant: call-B]
  [assistant: call-C]
  [tool: result-A]
  [tool: result-B]
  [tool: result-C]

After:
  [assistant: call-A]
  [tool: result-A]
  [assistant: call-B]
  [tool: result-B]
  [assistant: call-C]
  [tool: result-C]

If a tool result is missing entirely, the sanitizer now strips only the unmatched tool-call part instead of forwarding a prompt that the provider will reject. Any surviving assistant text stays in place, and the normal cleanup pass removes assistant messages that become empty or newly invalid after stripping.

Options

createMessageSanitizerMiddleware({
  onFix?: (entry: MessageSanitizerFixEntry) => void;
})

onFix is called once per fix applied. Use it to log to a file, emit metrics, or write to your observability system. If omitted, fixes are applied silently.

const middleware = createMessageSanitizerMiddleware({
  onFix: (entry) => {
    fs.appendFileSync("warn.log", JSON.stringify(entry) + "\n");
  },
});

Fix entry shape

interface MessageSanitizerFixEntry {
  ts: string;          // ISO timestamp
  fix: string;         // one of the fix types below
  model: string;       // "provider:modelId"
  callType: string;    // "stream" | "generate" | "object"
  [key: string]: unknown;
}

Fix types:

| fix value | Triggered when | |---|---| | tool-call-input-wrapped | One or more assistant tool calls had non-object input and were wrapped into a valid dictionary | | empty-content-stripped | One or more user/assistant messages had content: [] | | trailing-assistant-stripped | One or more trailing assistant messages had no tool calls | | invalid-tool-order-detected | An assistant tool-call message's tool results appeared too late (diagnostic only) | | tool-ordering-repaired | Tool results were successfully relocated to the correct position | | unresolved-tool-call-stripped | One or more assistant tool calls had no matching tool result anywhere in the prompt, so the unmatched tool-call parts were removed |

invalid-tool-order-detected fires even when repair isn't possible. When a result is missing entirely, the sanitizer follows that diagnostic with unresolved-tool-call-stripped so the prompt can still be sent.

OpenTelemetry

When @opentelemetry/api is installed and an active span exists, the middleware adds span events automatically:

| Event | Attributes | |---|---| | message-sanitizer.fix-applied | fixes, original/fixed count, removed indices and roles, wrapped tool-call count, model, call type | | message-sanitizer.tool-call-input-wrapped | repairs count, repaired tool call IDs, input types, model, call type | | message-sanitizer.invalid-tool-order-detected | issue count, block starts, missing tool call IDs, model, call type | | message-sanitizer.tool-ordering-repaired | repairs count, repaired tool call IDs, model, call type | | message-sanitizer.unresolved-tool-call-stripped | stripped count, stripped tool call IDs/names, assistant message indices, model, call type |

OTel is optional — if the package is not installed, the middleware runs without it.

Runnable Examples

| Example | What to look for | |---|---| | 01-basic-sanitization.ts | Trailing assistant and empty content removed before the call | | 02-tool-ordering-repair.ts | Misplaced tool results relocated to their correct positions | | 03-with-logging.ts | onFix callback captures every applied fix as structured data |

Run any example from the repo root:

cd examples && npx tsx 01-basic-sanitization.ts

Running Locally

bun test
bun run typecheck
bun run build

Why Not Fix This Upstream?

These conditions are bugs in the prompt construction layer, not in the provider. But the right fix (tracing back through multi-step agent code, multi-middleware stacks, or context management) takes time. This middleware is a reliable shim at the model boundary — catches problems before they become provider errors, logs what it changed, and gets out of the way.

Use it alongside ai-sdk-context-management or any other prompt-rewriting middleware. Stack order doesn't matter: the sanitizer runs on the final prompt regardless of what other middleware produced.