Skip to content

Migrating a Guardrails Configuration Between Providers

11 min read · updated August 11, 2026

A moderation config written against one provider is not a policy. It is a policy already compiled into that provider’s category names and threshold scale — and the target may express safety at a different layer, with different categories, or with no configuration surface whatsoever. Decompiling it back to the policy is the migration.

A guardrails config is three things

Pull the config apart before you try to move it, because the three parts have very different portability:

  • Classification — what taxonomy the content is sorted into. Provider-specific, and the least portable part. Names, granularity and boundaries all differ.
  • Thresholds — how strict you are per category. Semi-portable: the intent (“we block this outright, we flag that for review”) ports; the numbers do not, because the scales are not the same scale.
  • Enforcement — what happens when something trips. Block, redact, route to a human, log and continue, substitute a canned response. This is your code, and it is the part that ports completely, provided you did not delegate it to the provider.

If your enforcement is “the provider refuses and we surface whatever it says”, you have delegated all three layers and the migration is a rewrite rather than a port. That is worth knowing at the start.

Three shapes of safety surface

Providers expose safety in structurally different places, and this decides what a config can even say.

A separate classifier endpoint. OpenAI publishes a Moderations endpoint that you call yourself with a piece of text and which returns a flagged boolean, a categories object of booleans and a category_scores object of numbers across categories such as harassment, hate, self-harm, sexual, violence and their qualified sub-categories. It does not generate anything and it does not block anything — the enforcement is entirely yours, which is why this shape is the easiest to migrate from: your enforcement code already exists.

Inline settings on the generation call. Google’s Gemini API takes safety settings as part of the request, with harm categories named in a HARM_CATEGORY_ convention and a block threshold per category; when a generation is blocked you learn it from the response — a finish reason indicating a safety stop, and prompt-level feedback with a block reason — rather than from a separate call. Azure’s OpenAI deployments sit in the same family, with content filters configured on the deployment and a content_filter finish reason surfacing on the completion.

No configuration surface. Anthropic’s API has no per-category safety knobs and no separate moderation endpoint; behaviour is trained into the model and shows up as the model declining in prose, or as a dedicated refusal stop reason. There is no value you can set that corresponds to “block medium and above for harassment”.

Category vocabularies, threshold names and the exact set of safety fields are documented surfaces that vendors revise. Confirm the current members against OpenAI’s moderation guide and Google’s safety settings reference before you write them into a config.

What does not survive

  • The threshold numbers. A continuous score from one classifier and a coarse probability band from another are not on a common scale, and there is no conversion. A cutoff you tuned by watching false positives has to be re-tuned, from data, on the new surface.
  • Category boundaries. Two taxonomies can both have a “violence” category that classifies the same passage differently, because one splits graphic depiction from threat and the other does not. Mapping category to category by name is the most confident-looking mistake available here.
  • The point in the request where blocking happens. A separate classifier lets you check the input before spending a generation, and check the output before showing it. An inline setting decides during the call. If your pipeline currently rejects input cheaply and you move to a provider that only stops mid- generation, you pay for tokens on requests you were previously refusing for free.
  • Per-category granularity, when the target has none. Migrating from a taxonomy of a dozen categories to a model with no dials means the config cannot be expressed at all. What you do instead is keep the classifier: nothing stops you calling one provider’s moderation endpoint on text you are about to send to another provider’s model, or writing your own classifier pass. The classification layer and the generation layer do not have to be the same vendor, and decoupling them is usually the right answer regardless of which way you are migrating.

Rebuilding the ruleset

  1. Write the policy in prose first, with no vendor names in it. One line per rule: what content, what action, who is allowed to override. If your team cannot produce this document, the existing config is the policy and nobody knows what it says — which is the real reason these migrations go badly.
  2. Build a labelled sample from your own traffic. A few hundred requests: the ones the current config blocked, the ones it flagged, and a control set of ordinary traffic. Redact it and store it as a fixture. Without this you are tuning blind, and every later step in this list depends on it.
  3. Define an internal decision enum that your enforcement code consumes — something like ALLOW, REVIEW, BLOCK — and make both the old and new surfaces produce it through an adapter. Enforcement branches on your enum, never on a provider’s field.
  4. Implement the target adapter. For a classifier endpoint, that is a call plus a threshold table. For inline settings, it is reading the finish reason and any prompt-level block feedback and translating them. For a provider with no surface, it is your retained classifier plus a refusal detector on the output.
  5. Replay the labelled sample through both adapters and build the confusion matrix between them. You are not looking for agreement everywhere; you are looking for the cells you did not expect — content the old config blocked and the new one allows, most of all.
  6. Tune the new thresholds against the control set until the false-positive rate on ordinary traffic is one you can live with, then re-check the blocked set. Record the numbers you chose and the date, because the next person will ask why.

Verifying before cutover

A guardrail is a claim, and the test suite is what makes the claim checkable. Two properties matter and they pull in opposite directions: the rules block what they say they block, and they do not block ordinary traffic. Test both explicitly — testing that a guardrail blocks its claimed cases and testing the false-positive rate are two suites, not one, and a migration that only runs the first will ship a config that blocks a tenth of your legitimate requests.

Run both surfaces in parallel over live traffic for a period, with only the old one enforcing, and log the disagreements. That shadow period is the only thing that gives you the disagreement rate on real inputs rather than on your fixture. Cut over when the disagreements are ones you have read and accepted — not when the counts happen to match, because two configs can agree in aggregate and disagree on every individual case that matters.

One thing not to fold into this work: injection resistance. Content moderation and prompt-injection defence look adjacent and behave nothing alike, and a migration touches them differently. That is what a migration does to your prompt injection defences.