{"_id":"ai-sdk-heal","name":"ai-sdk-heal","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"ai-sdk-heal","version":"0.2.0","description":"Heal broken Vercel AI SDK message arrays before they hit the provider. Fixes orphaned tool calls, missing reasoning signatures, invalid tool names, and other provider rejections.","type":"module","license":"MIT","author":{"name":"Pontus Abrahamsson"},"repository":{"type":"git","url":"git+https://github.com/pontusab/ai-sdk-heal.git"},"homepage":"https://github.com/pontusab/ai-sdk-heal#readme","bugs":{"url":"https://github.com/pontusab/ai-sdk-heal/issues"},"keywords":["ai","ai-sdk","vercel","anthropic","openai","gemini","tool-calls","messages","sanitize","repair","heal","middleware","llm","agent"],"sideEffects":false,"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"engines":{"node":">=18"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"bun test src/test","check":"bun run typecheck && bun test src/test","prepublishOnly":"bun run check && bun run build"},"peerDependencies":{"ai":">=5.0"},"peerDependenciesMeta":{"ai":{"optional":false}},"devDependencies":{"@types/node":"^25.9.1","ai":"^6.0.195","tsup":"^8.5.1","typescript":"^6.0.3"},"publishConfig":{"access":"public"},"gitHead":"3e56f80eeea6399c58979e9196665bb9f0189332","_id":"ai-sdk-heal@0.2.0","_nodeVersion":"22.14.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-weKwnzQL7RQO3C/4kBeL/j01xCVtrmxA0bvWWvwyqQ158GxTR5cfz0ZTv3S6LuL5O1I01n4ZH73hcg6xVR3eng==","shasum":"670d6fcaf428dbd93605276f8902e1740a3acc46","tarball":"https://registry.npmjs.org/ai-sdk-heal/-/ai-sdk-heal-0.2.0.tgz","fileCount":5,"unpackedSize":48544,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICaJV/e3mlRlUKTk7/zrEgQn58oVTAQwoukVqDcZrB/lAiBCGFYL6XaOPY2I7gPDuD8hTWAFFeWDwnVh6zEY8496pg=="}]},"_npmUser":{"name":"pontus-midday","email":"pontus@midday.ai"},"directories":{},"maintainers":[{"name":"pontus-midday","email":"pontus@midday.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ai-sdk-heal_0.2.0_1780496899543_0.07290440165656986"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-03T14:28:19.274Z","0.2.0":"2026-06-03T14:28:19.696Z","modified":"2026-06-03T14:28:20.751Z"},"maintainers":[{"name":"pontus-midday","email":"pontus@midday.ai"}],"description":"Heal broken Vercel AI SDK message arrays before they hit the provider. Fixes orphaned tool calls, missing reasoning signatures, invalid tool names, and other provider rejections.","homepage":"https://github.com/pontusab/ai-sdk-heal#readme","keywords":["ai","ai-sdk","vercel","anthropic","openai","gemini","tool-calls","messages","sanitize","repair","heal","middleware","llm","agent"],"repository":{"type":"git","url":"git+https://github.com/pontusab/ai-sdk-heal.git"},"author":{"name":"Pontus Abrahamsson"},"bugs":{"url":"https://github.com/pontusab/ai-sdk-heal/issues"},"license":"MIT","readme":"# ai-sdk-heal\n\n![ai-sdk-heal](./assets/og-header.png)\n\nKeep your AI SDK conversations valid. `ai-sdk-heal` normalizes message arrays so they satisfy each provider's structural rules — pairing tool calls with results, coercing tool inputs to objects, preserving reasoning blocks correctly, and more.\n\nOne function. Pure. Idempotent. Safe on the hot path and on persisted history.\n\n```ts\nimport { healMessages } from \"ai-sdk-heal\";\n\nconst { messages, repairs } = healMessages(rawMessages, { provider: \"anthropic\" });\nawait streamText({ model, messages });\n```\n\n## What it does\n\nProviders each have their own rules for what a valid message history looks like:\n\n- Anthropic requires every `tool_use` to be paired with a matching `tool_result`, reasoning blocks to carry a `signature`, and rejects assistant messages that contain only reasoning.\n- OpenAI's Responses API expects reasoning items to be followed by a same-flow item.\n- All of them require tool inputs to be objects and tool names to match `^[a-zA-Z0-9_-]{1,64}$`.\n\nAgents, retries, and persisted conversations make it easy to drift out of those rules — especially across long multi-turn flows with thinking models and parallel tool calls. `ai-sdk-heal` checks the whole history against the active provider's rules and returns a normalized copy, plus an audit trail of every change it made.\n\n## Rules\n\n| Rule | What it does |\n|---|---|\n| `orphan-tool-use` | Assistant `tool-call` with no matching `tool-result`: inserts a placeholder result (or drops the call) so the pairing invariant holds |\n| `orphan-tool-result` | `tool-result` referencing a call that isn't in history: drops it |\n| `invalid-tool-input` | Tool input stored as a raw string because JSON parsing failed upstream: coerces to `{ raw: \"…\" }` so subsequent turns stay usable |\n| `invalid-tool-name` | Tool names with characters outside `^[a-zA-Z0-9_-]{1,64}$`: sanitizes while keeping the call/result pair linked |\n| `duplicate-tool-result` | Same `toolCallId` appearing twice after a retry: dedupes |\n| `empty-assistant-message` | Assistant message with no substantive content: drops it |\n| `orphan-reasoning-only-message` (Anthropic) | After pruning, an assistant message contains only reasoning blocks: drops it |\n| `missing-reasoning-signature` (Anthropic) | Reasoning block with no `providerOptions.anthropic.signature`: drops it (Anthropic won't accept thinking without the signature on replay) |\n| `reasoning-without-following-item` (OpenAI) | Trailing reasoning part with no following item in the Responses flow: drops it |\n\nEvery change is captured in the `repairs` array so you can log it, alert on it, or surface it in admin tooling.\n\nEach rule maps to a documented scenario tracked upstream: [#8516](https://github.com/vercel/ai/issues/8516), [#9141](https://github.com/vercel/ai/issues/9141), [#11602](https://github.com/vercel/ai/issues/11602), [#13430](https://github.com/vercel/ai/issues/13430), [#13645](https://github.com/vercel/ai/issues/13645), [#14259](https://github.com/vercel/ai/issues/14259), [#8379](https://github.com/vercel/ai/issues/8379), [#7729](https://github.com/vercel/ai/issues/7729), [#12504](https://github.com/vercel/ai/issues/12504).\n\n## Where this fits in the pipeline\n\n`ai-sdk-heal` operates on `ModelMessage[]` — the array you pass to `generateText` / `streamText`. If you persist conversations as `UIMessage[]` (the React/UI shape) and call `convertToModelMessages`, that conversion sits *before* `healMessages`:\n\n```\nDB / state                       AI SDK                          ai-sdk-heal                provider\n──────────                       ──────                          ───────────                ────────\nUIMessage[] ──convertToModelMessages──> ModelMessage[] ──healMessages──> ModelMessage[] ──> Anthropic / OpenAI / …\n                       │                                  │\n                       └─ pass `ignoreIncompleteToolCalls: true`\n                          to drop UI-level orphans during conversion\n```\n\nThe two layers solve different problems:\n\n- `convertToModelMessages({ ignoreIncompleteToolCalls: true })` filters `state: \"input-available\"` UI parts that haven't received a result yet. Use it for *live* UI message arrays where the user might have aborted mid-tool-call.\n- `healMessages` repairs anything that survives conversion or that lives only in `ModelMessage[]` form: missing reasoning signatures, invalid tool names, malformed tool inputs, duplicate tool results, OpenAI Responses ordering, persisted DB rows from older SDK versions, and the orphans that `pruneMessages` *creates* ([#13430](https://github.com/vercel/ai/issues/13430), [#12504](https://github.com/vercel/ai/issues/12504)).\n\nA defense-in-depth setup combines both:\n\n```ts\nconst modelMessages = await convertToModelMessages(uiMessages, {\n  ignoreIncompleteToolCalls: true,\n});\nconst { messages } = healMessages(modelMessages, { provider: \"anthropic\" });\nawait streamText({ model, messages });\n```\n\n## Install\n\n```bash\nnpm install ai-sdk-heal\n```\n\nPeer dependency: `ai >= 5.0`.\n\n## Usage\n\n### Heal before the provider call\n\n```ts\nimport { healMessages } from \"ai-sdk-heal\";\nimport { anthropic } from \"@ai-sdk/anthropic\";\nimport { streamText } from \"ai\";\n\nconst { messages, repairs } = healMessages(rawMessages, {\n  provider: \"anthropic\",\n  onRepair: (r) => logger.info({ repair: r }, \"message-normalized\"),\n});\n\nconst result = streamText({\n  model: anthropic(\"claude-sonnet-4-20250514\"),\n  messages,\n});\n```\n\nIf you want to hard-fail during development instead:\n\n```ts\nhealMessages(rawMessages, { provider: \"anthropic\", throwOnRepair: true });\n```\n\n### Wrap your model once with `withHealing`\n\nIf you'd rather not remember to call `healMessages` on every request, wrap\nthe model itself. The wrapper heals the prompt as it passes through the AI\nSDK middleware layer:\n\n```ts\nimport { withHealing } from \"ai-sdk-heal\";\nimport { anthropic } from \"@ai-sdk/anthropic\";\n\nconst model = withHealing(anthropic(\"claude-sonnet-4-5\"), {\n  onHealed: ({ repairs }) =>\n    logger.warn({ repairs }, \"prompt-auto-healed\"),\n});\n\n// Every generateText / streamText call now gets auto-healed.\nawait streamText({ model, messages });\n```\n\nProvider is auto-detected from the underlying model; override via\n`{ provider: \"anthropic\" }` for custom gateways.\n\n**Scope.** The middleware runs after the AI SDK's prompt conversion, so it\nhandles issues only the provider would reject — invalid tool names,\nmalformed tool inputs, unsigned reasoning, duplicate tool results,\nreasoning-without-following-item. **Orphan tool calls** still need\n`healMessages` up-front, because the SDK validates pairing during its own\n`convertToLanguageModelPrompt` pass. A robust setup combines both:\n\n```ts\nconst healedMessages = healMessages(rawMessages, { provider: \"anthropic\" }).messages;\nawait streamText({ model: withHealing(anthropic(\"claude-sonnet-4-5\")), messages: healedMessages });\n```\n\nYou can also compose `healMiddleware` manually via `wrapLanguageModel`:\n\n```ts\nimport { wrapLanguageModel } from \"ai\";\nimport { healMiddleware } from \"ai-sdk-heal\";\n\nconst model = wrapLanguageModel({\n  model: anthropic(\"claude-sonnet-4-5\"),\n  middleware: [healMiddleware(), otherMiddleware()],\n});\n```\n\n### Validate without mutating\n\nUse `validateMessages` in tests or CI to assert a conversation is\nprovider-ready without changing it:\n\n```ts\nimport { validateMessages } from \"ai-sdk-heal\";\n\nconst { valid, issues } = validateMessages(messages, { provider: \"anthropic\" });\nif (!valid) {\n  // `issues` is the same `Repair[]` shape healMessages returns.\n  throw new Error(`conversation is not provider-ready: ${issues.map((i) => i.rule).join(\", \")}`);\n}\n```\n\n### Heal after `pruneMessages`\n\n`pruneMessages` (built into the AI SDK) trims reasoning and tool turns to fit a context window, but in the process it can leave orphaned `tool_use` blocks ([#13430](https://github.com/vercel/ai/issues/13430), [#12504](https://github.com/vercel/ai/issues/12504)). Running `healMessages` after pruning fixes the structure the prune left behind:\n\n```ts\nimport { pruneMessages } from \"ai\";\nimport { healMessages } from \"ai-sdk-heal\";\n\nconst pruned = pruneMessages({\n  messages: history,\n  reasoning: \"before-last-message\",\n  toolCalls: \"before-last-message\",\n});\nconst { messages } = healMessages(pruned, { provider: \"anthropic\" });\nawait streamText({ model, messages });\n```\n\n### Heal persisted conversations\n\nBecause `healMessages` is idempotent — running it twice produces the same result — it's safe to apply on every read, or as a one-shot migration:\n\n```ts\nimport { healMessages } from \"ai-sdk-heal\";\n\nfor await (const row of db.selectFrom(\"chat\").execute()) {\n  const { messages, repairs } = healMessages(row.messages, {\n    provider: row.provider,\n  });\n  if (repairs.length === 0) continue;\n  await db\n    .updateTable(\"chat\")\n    .set({ messages, healed_at: new Date() })\n    .where(\"id\", \"=\", row.id)\n    .execute();\n}\n```\n\n### Auto-detect the provider\n\n```ts\nimport { healMessages, inferProvider } from \"ai-sdk-heal\";\n\nconst provider = inferProvider(model);\nconst { messages } = healMessages(rawMessages, { provider });\n```\n\n## Policies\n\nEvery rule has a default action picked to keep conversations usable. Override any of them:\n\n```ts\nhealMessages(rawMessages, {\n  provider: \"anthropic\",\n  policy: {\n    orphanToolUse: \"drop-call\",          // default: \"stub-result\"\n    invalidToolName: \"drop-pair\",         // default: \"rename\"\n    invalidToolInput: \"empty-object\",     // default: \"coerce-object\"\n    duplicateToolResult: \"dedupe-first\",  // default: \"dedupe-last\"\n    missingReasoningSignature: \"keep\",    // default: \"drop-reasoning\"\n  },\n});\n```\n\nSee `Policy` in the types for every option.\n\n## Design\n\n- **Pure and idempotent.** No side effects, no I/O. Running `heal(heal(x))` always equals `heal(x)` — this is enforced in the test suite and makes the package safe to apply unconditionally.\n- **Provider-aware.** Shared rules run for every provider; provider-specific rules (Anthropic, OpenAI) layer on top.\n- **Auditable.** Every change returns a `Repair` record with the rule name, message index, and reason.\n- **Composable.** Individual rules are exported so you can build your own pipeline.\n\n### Notes & caveats\n\n- **Tool-name collisions after sanitization.** If two distinct invalid tool names normalise to the same string (e.g. `\"foo bar\"` and `\"foo!bar\"` both become `\"foo_bar\"`), they keep their distinct `toolCallId`s but share a name. The provider still accepts the conversation; the model can disambiguate via the call IDs.\n- **Google / Gemini.** Shared rules apply automatically. We don't ship a Google-specific signature rule because `@ai-sdk/google` (≥ the May 2026 release) now auto-injects `skip_thought_signature_validator` for Gemini 3 tool-call replays at conversion time. Replicating it here would require model-ID detection that `ModelMessage[]` doesn't carry.\n- **Middleware vs. `healMessages`.** `withHealing` runs *after* the SDK's `convertToLanguageModelPrompt`, so it can't repair orphan tool-use (the SDK validates pairing during conversion and throws first). Always run `healMessages` on the message array up-front; use `withHealing` as a defensive second layer for everything that slips through.\n\n## Related\n\n- [`toolpick`](https://github.com/pontusab/toolpick) — dynamic tool selection for the AI SDK so the model only sees the tools that matter on each step.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-295b179f154675b27f9626f173bced40"}