AI coding

Testing Output Against a New Model Without Burning the Budget

Antonio Evans10 min read

A dark desk at night where a laptop shows two side-by-side columns of API calls, one green and one with a few red crosses, next to a printed sampling sheet and a notebook with a hand-drawn rubric table.
Key points

Key points

  1. 1Shadow calls send a copy of real production requests to the candidate model in parallel, keeping its output out of the user-facing path, so no user ever sees an untested response (CodeAnt).
  2. 2Before you compare two models, measure parse failure, schema violation and content error separately, because a 2 percent parse failure rate on 50,000 daily requests is 1,000 broken downstream operations per day (GMI Cloud).
  3. 3Size the shadow sample from a two-proportion formula rather than a gut feel: a pilot at 5 percent and a candidate at 3 percent typically needs a few hundred requests per model to resolve at 95 percent confidence and 80 percent power (Select Statistical Services).
  4. 4A schema-valid response can still be wrong, so the rubric must hold parsing, schema conformance and semantic correctness as three distinct gates before you switch traffic (TrueFoundry).

Why parse failure is the metric to watch before anything else

Most teams evaluate a candidate model on a handful of hand-written prompts, read the outputs, and conclude the model is fine. That process misses the failure mode that actually breaks pipelines. In production, the output is consumed by code: a parser, a validator, an API call. If the JSON does not parse, nothing else in the evaluation matters.

The failure taxonomy is well documented. Models prepend preamble text like "Here is the JSON you requested", truncate mid-object when they hit the max_tokens ceiling, drift field names (user_name where the schema says userName), coerce types (the string "42" where the schema wants a number), break on unescaped quotes, invent enum values, or lose track of nesting in deep objects (GMI Cloud). Research on structured output failures found that 65 percent of schema errors in fine-tuned models fell into just two categories: hallucinated keys and unclosed brackets (tianpan.co).

This matters because the stakes scale with volume. A 2 percent parse failure rate on 50,000 daily requests is 1,000 broken downstream operations per day, and the retry logic that handles them doubles inference cost for those requests (GMI Cloud). Even a 1 percent failure rate on thousands of daily requests produces a steady stream of customer complaints, retries that eat API quota, and cascading downstream failures (jamesm.blog).

The decision rule is simple. If the candidate's additive parse-failure rate (candidate minus baseline) exceeds what your retry and fallback machinery can absorb at current volume, the model is not ready regardless of how good its content looks. Everything else in this article supports that one number.

Setting up shadow calls without touching the user path

Shadow traffic testing duplicates live requests to the candidate model, logs the candidate's responses, and compares them offline. Users only see outputs from the current production model, so there is zero risk to user experience (CodeAnt). The mechanics are straightforward: duplicate requests at the API gateway, application layer, or load balancer; route the copy asynchronously to the candidate; capture both responses with metadata such as latency, token count and timestamp; compare offline.

Four practical rules separate a clean shadow run from a mess:

Keep the candidate out of the rendering path entirely. The candidate call should be fire-and-forget from the user's perspective. Timeouts on the candidate should never block or slow the production response, because a slow candidate that drags down your p99 latency has already contaminated the experiment.

Send the identical input, including the schema. The comparison is only valid if both models receive the same prompt, the same system message and the same JSON Schema. If your baseline uses a provider-native structured output mode with a Pydantic or Zod schema, the candidate must receive the equivalent constraint expressed the same way, not a prose description of the shape (OpenAI). Passing the schema formally rather than describing it in prose reduces field-name drift, because the model copies names rather than reconstructing them (GMI Cloud).

Handle sensitive data deliberately. Your logs now contain two copies of real user input and model output. Apply the same data handling and encryption policies you use for production data (CodeAnt). If the protocol you use for context or tool access includes consent requirements, the shadow copy inherits them: hosts must obtain explicit user consent before exposing user data to external servers (MCP specification).

Record cost per request, not just per token. Parse failures trigger retries, and each retry is a full inference call at full cost and latency (tianpan.co). Log attempt counts so the cost comparison reflects the retry tax, not the sticker price per token.

A minimal recorder looks like this:

import json, time, uuid

def shadow_call(candidate_client, request, schema, endpoint_label):
    record = {
        "id": str(uuid.uuid4()),
        "endpoint": endpoint_label,
        "prompt_hash": hash(request["prompt"]),
        "candidate_raw": None,
        "parse_ok": False,
        "schema_ok": False,
        "latency_ms": None,
        "attempts": 0,
        "tokens": None,
    }
    start = time.monotonic()
    try:
        resp = candidate_client.responses.create(
            input=request["prompt"],
            response_format={"type": "json_schema", "json_schema": schema},
        )
        record["attempts"] = 1
        record["candidate_raw"] = resp.output_text
        record["tokens"] = resp.usage.total_tokens
        record["parse_ok"] = True  # json.loads below confirms
        json.loads(resp.output_text)
    except json.JSONDecodeError:
        record["parse_ok"] = False
    except Exception as exc:
        record["error"] = type(exc).__name__
    finally:
        record["latency_ms"] = int((time.monotonic() - start) * 1000)
    return record

The point of the recorder is that every shadow response lands in a table with the same fields, so scoring is mechanical.

Sizing the sample so the difference is detectable

Picking 50 requests because it is Friday afternoon is the most common way these evaluations produce noise instead of answers. Use a two-proportion power calculation. The standard formula sizes each group as n = (Zα/2 + Zβ)² × (p1(1−p1) + p2(1−p2)) / (p1−p2)², where p1 is the baseline parse-failure rate, p2 is the candidate's, and typical settings are 95 percent confidence (Zα/2 = 1.96) and 80 percent power (Zβ = 0.84) (Select Statistical Services).

Worked example with realistic numbers. Suppose your baseline model fails to parse on 5 percent of requests and you expect the candidate to fail at 3 percent, a two-point improvement. Plugging in: (1.96 + 0.84)² × (0.05 × 0.95 + 0.03 × 0.97) / (0.02)² = 7.84 × (0.0475 + 0.0291) / 0.0004 = 7.84 × 0.0766 / 0.0004, which is roughly 1,501 requests per model. That is the honest number for detecting a two-point difference. If the expected gap is larger, the sample shrinks fast.

Baseline failure rate (p1)Candidate failure rate (p2)GapApprox. requests per model (95% confidence, 80% power)
5%3%2 pointsabout 1,500
5%2%3 pointsabout 680
10%5%5 pointsabout 390
20%10%10 pointsabout 125
5%1%4 pointsabout 390

The difference between the calculator's numbers and your gut matters. The most common mistake is treating 300 to 500 requests as sufficient for a two-point difference. It is sufficient for a five-point difference at moderate baseline rates, not for a two-point one. Use a pilot of 100 to 200 requests first to estimate both rates, then size the full run from the formula. A quick truth check: if the observed candidate parse-failure rate is zero in the pilot and the baseline is 5 percent, you are looking at a large gap and a few hundred requests will resolve it; if the candidate is 4 percent, plan for a bigger run.

Stratify the sample by endpoint and prompt type. A model that parses cleanly on short classification prompts can fail on long extraction requests with array fields, because truncation is the most common failure for schemas with variable-length arrays (GMI Cloud). Stratification ensures every request shape gets adequate representation rather than the endpoint that happens to fire most often.

If you route shadow calls through a single API for multiple candidate models, model swapping for comparison runs is a configuration change rather than an integration project; Quanta Inference exposes text, image, video, audio and embedding models behind one OpenAI-compatible key, which is the practical way to hold the request constant while varying the model (https://quantaintelligence.ai/inference). Just keep the shadow copy out of the render path, as above.

Building the rubric: three gates, not one score

A single 1-to-5 quality score hides the failure that costs money. Score every shadow response against a fixed rubric with three gates, applied in order, so a response that fails gate 1 is never credited with good content.

GateQuestionExamples of failureDownstream action
1. Parse failureDoes the raw output load as JSON with no stripping, regex or repair?Preamble text, markdown fences, truncation mid-object, unescaped quote, trailing commaHard fail. Count toward parse-failure rate. Never retried in scoring.
2. Schema violationDoes the parsed object validate against the JSON Schema (required fields, types, enums, additional properties)?Missing required key, string where number expected, invented enum value, extra fieldFail. Same weight as parse failure for the reliability decision, but track separately because fixes differ.
3. Content errorAre field values semantically correct for the input?Right field, wrong extracted date; hallucinated customer ID; plausible but wrong classificationFail for quality, pass for reliability. Judge by human review or a higher-tier model with the input attached.

Gate 1 is measured mechanically: run json.loads on the raw output only, with no fence stripping. Regex extraction "succeeds but is fragile" (GMI Cloud), so a response that only parses after cleanup is a parse failure for scoring purposes even if your production code would rescue it. The reliability decision uses the strict number because the retry tax is real either way.

Gate 2 catches the class of errors the parser misses. Valid JSON is not a contract: JSON mode can produce parseable output without required fields, correct enums or the expected object shape (TrueFoundry). Validate with a real JSON Schema validator or a typed parser (Pydantic, Zod) that enforces types and local invariants (OpenAI).

Gate 3 is where teams skip work and pay for it later. Shape is not truth: a schema-valid customer ID, amount or date can still violate domain rules (TrueFoundry). For gate 3, either sample 50 to 100 passing outputs for human review or use a stronger model as a judge with the original input attached. Constrained decoding solves structural validity, not semantic correctness, and teams that stop at structure trade a visible failure mode for an invisible one (GMI Cloud).

One rubric detail saves arguments later: score the raw output exactly as the candidate returned it. If the candidate uses a strict schema mode that the baseline lacks (or vice versa), note it in the methodology, because structured output support varies by provider and by mode (Vellum). You are comparing configurations you would actually ship, schema enforcement included.

Running the comparison and reading the result

With shadow calls live and the rubric fixed, the run itself is a table exercise. For each stratum and in aggregate, compute the parse-failure rate, schema-violation rate and content-error rate for both models, plus the confidence interval on each difference. Then apply the decision rule.

A practical decision rule with four outcomes:

  • Candidate is clearly better (difference significant, candidate lower, content parity): schedule the cutover, keep the shadow pipeline running for one week after to catch drift.
  • No detectable difference: switch only for cost, latency or operational reasons, not quality. If the candidate is much cheaper per successful task, the business case may still be strong, but say so explicitly.
  • Candidate worse on reliability: stop. Do not run a canary "to see." A canary or A/B test exposes real users to the candidate, which is appropriate only after shadow validation passes (CodeAnt).
  • Mixed per stratum: the candidate may be fine for some endpoints and not others. Route per endpoint rather than all-or-nothing.

This two-stage discipline matters. Shadow testing is observational and catches regressions in accuracy, formatting, latency and cost; A/B testing is experimental and captures user behavior, but it carries direct user exposure (CodeAnt). Running the shadow stage first is how you make the canary stage low-risk.

Factor in what each failure costs when reading the numbers. A parse failure means a retry at full inference cost and latency, and on a high-volume pipeline the retries compound quickly (tianpan.co). If you have measured your own cost per successful task, the comparison becomes arithmetic: rate difference multiplied by volume multiplied by cost per attempt. Teams that have not measured per-task cost typically end up guessing; a usage-based cost-per-million-tokens figure is the minimum you need to make the switch decision defensible (OpenCost). Using wording such as "usage-based cost" rather than "price per token" matters here, because the two are distinct and self-hosting comparisons quietly conflate them.

A worked plan from empty pipeline to cutover

Here is the full sequence, with the numbers for a hypothetical service that handles 200,000 structured-output requests per day and currently parses cleanly on 95 percent of them.

Week 1, instrument. Add the shadow recorder to the top three endpoints by volume. Capture raw candidate output, parse status, schema validation status, latency, tokens and attempt counts. No scoring yet. Confirm the baseline model reproduces its known 5 percent parse-failure rate in the logs.

Week 2, pilot. Send 200 requests per endpoint per model. Score gates 1 and 2 mechanically. Estimate p1 and p2 from the pilot. If the pilot shows a two-point gap, the full run needs roughly 1,500 requests per model; if it shows five points, a few hundred will do (Select Statistical Services).

Weeks 3 to 4, full run. Collect the sized sample stratified by endpoint and prompt type. Score gate 3 on a subsample of passing outputs. Note that if the candidate receies a formal schema while the baseline receives prose instructions, you are measuring the enforcement mechanism as well as the model (OpenAI); that is often exactly the comparison you want, but state it.

Decision. Bring the confidence interval on the parse-failure difference to the review. If the upper bound of the candidate-minus-baseline difference is below your tolerance (for example, under one point at 200,000 requests per day), proceed. If the interval straddles zero or the sign is unfavorable, extend the sample or stop.

Post-cutover. Keep the old model as the shadow for one week. Distinct failure rates after cutover usually mean schema drift, truncation from token limits or model-specific formatting weaknesses, so track parse failure as its own metric separate from general error rate (GMI Cloud).

Two cautions on interpretation. First, performance differences between endpoints that claim to serve the same model can reach up to 47.21 percentage points, with identity verification failing in 45.83 percent of fingerprint tests in one audit of third-party services (arXiv). If you test a candidate model through a reseller, your result may describe the endpoint, not the model. Second, if your pipeline includes tool calls, a structurally perfect call is still an intended external action, and authorization decides whether it may proceed (TrueFoundry), which is why gate 3 belongs in the rubric even when gates 1 and 2 look clean.

For teams that run these comparisons across many endpoints or business lines, the natural next step is putting the evaluation work in the hands of a specialist rather than a generalist engineer, or having a Quanta expert produce the scoring sheet and decision memo for a line of business (https://quantaintelligence.ai/agents).

FAQ

FAQ

How many requests do I need to compare two models' parse-failure rates?

Size it with a two-proportion power calculation rather than a fixed number. Baseline rates near 5 percent, a candidate near 3 percent, 95 percent confidence and 80 percent power imply roughly 1,500 requests per model (Select Statistical Services). A larger expected gap needs fewer: a 10-point gap from a 20 percent baseline can resolve in about 125 requests per model.

Should I strip markdown fences before scoring parse failures?

No. Score the raw output with a strict parser. A response that parses only after cleanup counts as a parse failure for the reliability decision, because the retry and cleanup logic is real production cost (GMI Cloud). Keep a separate column for "rescued after cleanup" if you want to track how much of your failure budget cleanup absorbs.

How is a schema violation different from a content error?

A schema violation is structural: a missing required field, wrong type, invented enum value or extra property, which a validator catches mechanically (TrueFoundry). A content error is structural success with wrong meaning, such as a schema-valid but hallucinated customer ID. The fixes differ: structural failures point to schema enforcement or prompt changes, content failures point to input quality, model reasoning or domain validation.

Can I skip shadow testing and go straight to a canary or A/B test?

You can, but it puts real users in front of the candidate. Shadow testing is observational, with zero user risk, and captures formatting, latency and cost regressions that benchmarks miss; A/B testing is experimental and is best used after shadow validation passes (CodeAnt). The usual sequence is shadow first, then a small canary or A/B, then full rollout.

What should I log for each shadow call?

At minimum: the raw candidate output, parse status, schema validation status, latency, token count and attempt count, keyed to the request and endpoint (CodeAnt). Logging attempts matters because parse failures trigger full-cost retries (tianpan.co), and without attempt counts your cost comparison understates the candidate's true cost.

Sources

Sources

  1. Structured Output for LLMs: Constrained Decoding
  2. Grammar-Constrained Generation: The Output Reliability ...
  3. Structured Outputs: When Your AI Needs to Follow a Schema
  4. How to Roll Out New LLMs Safely Using Shadow Testing
  5. Structured model outputs | OpenAI API
  6. Specification
  7. Comparing Two Proportions - Sample Size
  8. LLM Structured Outputs with JSON Schema
  9. When should I use function calling, structured outputs or JSON ...
  10. First-of-a-Kind Kubernetes Inference Cost Tracking
  11. shadow APIs - Real Money, Fake Models
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles