Appearance
Payload rules
merido can reshape the outbound request body — the JSON actually sent to the upstream provider — right before it goes over the wire. Two mechanisms do this: a declarative rules file you write yourself, and an automatic retry that recovers from a provider rejecting a field it doesn't support. Both are fail-safe the same way tool-result compression is: neither ever touches messages / system / contents — only top-level request parameters.
Declarative payload rules
Point payload_rules_path at a JSON file and merido applies its rules to every outbound request that matches — clamp a default, force an override, or drop a field — scoped by provider, model, and wire format. This is a file-based, operator-only knob: there's no dashboard UI for it (unlike guardrails custom rules), so it's best suited to an operator who can edit a file next to the running gateway.
Enabling it
bash
MERIDO_PAYLOAD_RULES_PATH=/etc/merido/payload-rules.jsonThe file is hot-reloaded: merido checks its mtime at most once per request and only re-reads it when it changed. A parse error keeps the previously loaded rules in place (logged once for that version) rather than disabling the engine or failing requests — fix the file and the next mtime change picks it up. An unset path disables the engine entirely (the default).
Rule format
json
[{
"match": { "provider": "openai", "model": "gpt-5*", "format": "openai_chat" },
"set_default": { "temperature": 1.0 },
"override": { "stream_options.include_usage": true },
"remove": [ "logit_bias", "metadata.user_id" ]
}]This example, applied only to OpenAI requests targeting a gpt-5* model in the OpenAI chat wire format: defaults temperature to 1.0 when the caller didn't set one, always forces stream_options.include_usage on, and deletes logit_bias and the nested metadata.user_id path regardless of whether the caller sent them.
match (all keys optional — an absent key matches anything):
| Key | Matching |
|---|---|
provider | Exact string match against the resolved provider id. |
model | Glob against the upstream model id — * wildcards, e.g. gpt-5* matches any gpt-5-family model. |
format | One of openai_chat | openai_responses | anthropic | gemini. An unrecognized value never matches (fail-closed on a typo, not fail-open). |
Actions, applied in this fixed order per matching rule:
set_default— write each dotted path only when it's currently absent from the body.override— write each dotted path unconditionally.remove— delete each dotted path.
Paths are dotted (metadata.user_id) and descend objects only — there's no array indexing. Values in set_default / override can be any JSON type.
Every rule whose match matches, applies — in file order. This is not first-match-wins: if two rules both match one request, both fire, and a later rule's action can overwrite an earlier one's on the same path.
Fail-safe by construction
A rule action whose path root is messages, system, or contents is refused at load time (and logged once) — the engine can never grow, empty, or rewrite message/system content, only scalar request parameters like temperature or stream_options.
Foot-guns: don't rewrite model, stream, or usage accounting
The load-time guard only protects message/system content — it does not stop a rule from rewriting top-level keys merido itself depends on. Treat override/remove on these as unsupported:
model— repoints the request at a different upstream after routing and pricing resolved the original, so cost/capability accounting no longer matches what was actually sent.stream—remove/overridefights merido's own streaming decision and can desync the client's expected response shape from what the upstream returns.stream_options.include_usage: false— re-blinds merido's usage accounting for every matched request: merido forces this flag on so streamed token/cost booking is never zero (it strips the trailing usage back out for clients that didn't ask for it), so a rule turning it off silently under-counts spend.
Keep set_default/override to genuine provider params and leave these three to merido.
Reactive field-strip retry
Even without a payload rule written for it, merido recovers automatically from the single most common provider-compatibility failure: an upstream 400 that names a request field it doesn't support (e.g. "Unknown parameter: 'logit_bias'"). When that happens, merido strips exactly that field from the outbound body and retries the same account — no operator rule needed, no failed request surfaced to the caller.
This is controlled by strip_unsupported_fields (default on):
bash
MERIDO_STRIP_UNSUPPORTED_FIELDS=false # see the provider's raw 400 insteadEligible fields are a fixed allow-list of scalar/param fields — never message content:
temperature, top_p, logprobs, top_logprobs, logit_bias, parallel_tool_calls,
stream_options, seed, presence_penalty, frequency_penalty, metadata, store,
reasoning_effort, prediction, service_tier, safety_identifier, max_completion_tokensA field only strips when it's actually present in the sent body and named in the error body (a quoted 'field' / "field" is preferred over a bare substring match, so an error quoting 'top_logprobs' strips top_logprobs rather than the earlier-listed logprobs, which is a substring of it).
Two bounds keep this safe:
- Once per field, per request. A field that's stripped stays stripped for the rest of that request's attempts — including a later fallback to a different account or provider — so a request that hops providers doesn't re-discover the same incompatibility twice.
- At most 3 strips per request. A pathological upstream that rejects field after field can't drive an unbounded retry loop; past the third strip, the next
400surfaces to the caller as-is.
A stripped-and-retried attempt records no circuit-breaker failure, model lock, or cooldown — it's treated as a benign compatibility fix-up, not a reliability signal (see Circuit breaker & cooldowns).
When to turn it off: if you'd rather see a provider's incompatibility immediately (debugging exactly what it rejects, or a field you need honored silently disappearing instead of erroring) set MERIDO_STRIP_UNSUPPORTED_FIELDS=false — you'll get the provider's original 400 on the first occurrence.
Related
- Add providers & keys — the per-provider quirks these two mechanisms paper over.
- Virtual models & fallback — the account/target fallback chain a stripped retry stays on, and the circuit breaker it deliberately doesn't affect.
- Environment variables —
MERIDO_PAYLOAD_RULES_PATH,MERIDO_STRIP_UNSUPPORTED_FIELDS.