ProviderHistoryCompat
The ProviderHistoryCompat processor handles provider-specific history incompatibilities. It can rewrite the outbound language model prompt before a provider call, or react to API errors and retry with repaired message history.
Use it when an agent may switch between model providers or reuse message history across providers. It also handles providers that reject fields emitted by another provider.
Usage exampleDirect link to Usage example
Mastra adds this processor to every agent's errorProcessors by default when your list doesn't already contain that id. Among the added defaults it comes first. Its outbound prompt rules run before the other defaults, including the retry processor's blind resend. Supply your own instance to add custom rules or position it yourself:
import { Agent } from '@mastra/core/agent'
import { ProviderHistoryCompat } from '@mastra/core/processors'
export const agent = new Agent({
id: 'my-agent',
name: 'my-agent',
instructions: 'You are a helpful assistant.',
model: 'anthropic/claude-sonnet-4-5',
errorProcessors: [new ProviderHistoryCompat({ additionalRules: [/* ... */] })],
})
A supplied ProviderHistoryCompat is used instead of the default. The default is only added when no processor in your list carries that id. Place an instance in inputProcessors when you want the prompt-rewriting rules to run in the input lane instead.
The placement decides which half of the processor works:
- Reactive rules need
errorProcessors. On the standard loop,processAPIErrorruns from any of the three lists, so an input-processor registration does repair the message list, but the retry it asks for is only honored when the agent has a retry budget. With the default error processors in place the resolved list is non-empty, so a safety cap of3applies. The budget is leftundefinedonly when the caller runs with no error processors at all (errorProcessorDefaults: falsewith none configured) and doesn't setmaxProcessorRetriesexplicitly. Registered only as an input processor in that no-budget state, a reactive rule repairs the history and the request still fails. On the durable path the API-error pass additionally requires a non-emptyerrorProcessorslist. If that list is empty, an input-processor registration can't trigger reactive recovery there at all, even withmaxProcessorRetriesset. - Preemptive rules run from either list.
processLLMRequestruns over the input processors and over the resolved error processors, so aProviderHistoryCompatinerrorProcessorsgets its prompt rewrites as well as its reactive recovery. An instance registered ininputProcessorsruns once, in its input position.
Registering in errorProcessors alone, as the example above does, covers both halves. Double registration can't cause a double retry: the runner stops at the first processor that asks for one, and each reactive rule bails once retryCount is above zero, so a repaired turn is attempted exactly once more.
Constructor parametersDirect link to Constructor parameters
opts?:
additionalRules?:
PropertiesDirect link to Properties
id:
name:
processLLMRequest:
processAPIError:
Built-in rulesDirect link to Built-in rules
ProviderHistoryCompat includes these built-in compatibility rules:
| Rule | Provider | Timing | Behavior |
|---|---|---|---|
anthropic-tool-id-format | Anthropic | Preemptive prompt rewrite | Rewrites tool call IDs that contain characters outside [a-zA-Z0-9_-] in the outbound prompt, on both the call and its result. A sanitized ID that would collide with one already in the prompt gets a _2, _3, … suffix. The provider's rejection never fires, and persisted history keeps the original IDs. A reactive fallback covers providers the preemptive check doesn't recognize, such as Vertex-hosted Claude. |
cerebras-strip-reasoning-content | Cerebras | Preemptive prompt rewrite | Removes assistant reasoning parts from the outbound prompt so they're not serialized as unsupported reasoning_content fields. |
anthropic-strip-foreign-reasoning-content | Anthropic | Preemptive prompt rewrite | Removes non-Anthropic assistant reasoning parts from the outbound prompt. Anthropic-native thinking history is preserved. |
anthropic-strip-foreign-signed-reasoning | Anthropic-compatible | Preemptive prompt rewrite | Drops signed thinking from the outbound prompt when its origin turn was stamped with a different provider (for example Kimi For Coding ↔ anthropic/claude-sonnet-4-6), since the receiving provider can't verify another provider's signature. Turns emptied of all content by the drop are removed from the prompt. Unstamped history is left untouched. |
azure-system-reminder-transform | Azure OpenAI | Preemptive prompt rewrite | Renames <system-reminder> wrappers in user text and system instructions to <memory-context> for the outbound request. Stored history remains unchanged. |
openai-orphan-item-id | OpenAI, Azure OpenAI (Responses) | Reactive API error recovery | Recovers history that OpenAI rejects with Item 'msg_…' of type 'message' was provided without its required 'reasoning' item. Drops the Responses itemId from every item-bearing part of an assistant message that carries one but has no reasoning part, and retries. The message then replays by value instead of as an unsatisfiable item_reference. The repair is applied to the in-memory list for the current turn. |
Preemptive rules run through processLLMRequest after Mastra converts messages to the model prompt format and before the prompt is sent to the provider. These rewrites affect only the current provider call. Persisted history is never changed, so the IDs and content in memory and in the Studio stay exactly as written.
Reactive rules run through processAPIError after a provider rejection. They can update the persisted messageList and request a retry.
anthropic-tool-id-format implements both. Its preemptive hook normally prevents the rejection from happening at all, which leaves the reactive hook dormant. See Tool call ID repair is prompt-scoped below.
CompatRuleDirect link to compatrule
A CompatRule defines one provider history compatibility fix:
import type { CompatRule } from '@mastra/core/processors'
const removeUnsupportedPromptParts: CompatRule = {
name: 'remove-unsupported-prompt-parts',
applyToPrompt({ prompt, model }) {
// Return a modified LanguageModelV2Prompt, or undefined to leave it unchanged.
return undefined
},
}
name:
errorPatterns?:
fix?:
applyToPrompt?:
Custom rulesDirect link to Custom rules
Pass custom rules through additionalRules. Custom rules run after the built-in rules:
import { Agent } from '@mastra/core/agent'
import { ProviderHistoryCompat, type CompatRule } from '@mastra/core/processors'
const stripUnsupportedAssistantMetadata: CompatRule = {
name: 'strip-unsupported-assistant-metadata',
applyToPrompt({ prompt, model }) {
if (typeof model !== 'string' || !model.startsWith('example-provider/')) {
return undefined
}
let changed = false
const nextPrompt = prompt.map(message => {
if (message.role !== 'assistant' || typeof message.content === 'string') {
return message
}
const nextContent = message.content.map(part => {
if (!('providerOptions' in part)) return part
changed = true
const { providerOptions: _providerOptions, ...rest } = part
return rest
})
return { ...message, content: nextContent }
})
return changed ? nextPrompt : undefined
},
}
export const agent = new Agent({
id: 'custom-provider-agent',
name: 'custom-provider-agent',
instructions: 'You are a helpful assistant.',
model: 'example-provider/model',
inputProcessors: [
new ProviderHistoryCompat({
additionalRules: [stripUnsupportedAssistantMetadata],
}),
],
})
Use applyToPrompt for provider-specific rewrites that shouldn't be saved to memory. This is what new rules should normally reach for, and it's the hook the built-in rules use. Use fix with errorPatterns only when the repaired history must persist and be reused on future turns, and when the provider can't be stopped from rejecting the request up front.
Tool call ID repair is prompt-scopedDirect link to Tool call ID repair is prompt-scoped
Anthropic enforces ^[a-zA-Z0-9_-]+$ on tool_use.id. A tool call whose ID came from another provider, containing . or :, is rejected. The rule sanitizes those IDs in the outbound request:
// What the provider receives
tool_use.id: 'call_abc_1'
// What stays in memory, in the returned messages, and in the Studio
toolCallId: 'call.abc:1'
Both the tool-call and its paired tool-result are rewritten from the same map, so call/result pairing survives the rewrite. Replacement IDs are assigned in encounter order and never collide with an ID the prompt already carries: if a.b sanitizes to a_b and the prompt already has an a_b, the rewritten ID becomes a_b_2.
Because nothing is persisted, a caller that reuses the same message list in a later turn sees the original IDs again, and the repair runs again on that request.
Reactive fallbackDirect link to Reactive fallback
The rule's errorPatterns and fix hooks repair the same rejection after the provider raises it. The preemptive rewrite only runs when isMaybeAnthropic(model) matches. The reactive path stays the fallback for Claude served through a provider the check doesn't recognize, such as Vertex-hosted Claude, and the only repair once a call has already been rejected. A rule can use both hooks together, and fix on the CompatRule interface itself isn't deprecated: custom rules legitimately use it.