Engineering6 Min Read
Running Two Model Providers in Production

Running Two Model Providers in Production

Aadesh Ingle
Try it yourselfProvider contract inspector

Across the client systems we run, two model providers are live in production: OpenAI's GPT series and Google's Gemini through the Vertex AI SDK. One call-analytics deployment handles transcription, summarization, sentiment, and intent on Vertex end to end; other systems mix providers by task. Supporting both affects prompts, evaluation, tracing, and incident response.

Why bother

The reasons are mostly operational.

Clients arrive with cloud commitments, and a GCP customer may require Vertex. Models also perform differently by task, with that difference changing between releases. A second provider gives us a migration path during sustained rate limits or degraded latency, although moving traffic still takes evaluation and prompt work. In practice, the advantage is movement measured in days instead of quarters, not instant failover.

Running two providers also forces the team to define an application contract: what the model receives, what it may return, which side owns retries, and what evidence each run leaves behind. That contract keeps provider-specific behavior at a boundary the team can inspect.

The myth of the portable prompt

I expected to reuse prompts with small provider-specific adjustments. In practice, the same instructions produce different formatting, refusal, and tool-calling behavior. One model may truncate where another embellishes; another may comply where the first refuses. We therefore version prompt variants by provider rather than treating the prompt as portable application code.

The eval suite transfers because its cases describe the product contract rather than a provider's behavior. We run the full suite before a provider or model change reaches a critical path. Aggregate improvements do not waive regressions on rate limits, tool arguments, or refusal behavior that matter to a particular workflow.

I would design the evaluation and tracing harness before the router. The provider adapter should record which model served a request, which prompt and tool contract it saw, what it tried to do, and how the result compared with the same case on the other path.

Provider comparison

Two adapters, one definition of correct

Follow the shared case through each prompt variant. Then introduce a missing field and inspect the common gate.

Scenario

Synthetic scheduling case and invented outputs. A and B are illustrative adapters, not results attributed to OpenAI or Gemini.

Sequence

  1. One product contractCurrent
  2. Adapter AUpcoming
  3. Adapter BUpcoming
  4. Same golden-case gateUpcoming
  5. Keep the evidenceUpcoming
One request forks through two adapters into the same contract gateSYNTHETIC CASE · COMPARISON BRANCHES, NO AUTOMATIC FAILOVER
Shared requestschedule an interview
Adapter AA prompt + A request shape
Adapter BB prompt + B request shape
intent: scheduleneedsFollowup: falseintent: scheduleneedsFollowup: false
Same assertionsRequired fields + intent
A: passes this caseB: passes this case
Trace + verdictKeep both run receipts
Prompt wording can vary. The acceptance conditions stay fixed.

One product contract

The same case asks to classify “schedule an interview.” It requires intent = schedule and a boolean needsFollowup. These are illustrative assertions, not a production schema.

Read the full explanation

Both satisfy the case

Synthetic scheduling case and invented outputs. A and B are illustrative adapters, not results attributed to OpenAI or Gemini.

  1. One product contract. The same case asks to classify “schedule an interview.” It requires intent = schedule and a boolean needsFollowup. These are illustrative assertions, not a production schema.
  2. Adapter A. Construct provider A’s request with its own prompt variant. Keep request, response, and error normalization narrow.
  3. Adapter B. Run the same case through a separate adapter and prompt. The branch is an evaluation comparison, not an automatic failover route.
  4. Same golden-case gate. Apply identical assertions to both normalized outputs. Change the selected case to see a missing field block one result while the product contract stays fixed.
  5. Keep the evidence. Keep provider/model identity, prompt and schema versions, raw payload, and the assertion result. A diagram cannot establish that a provider is ready for production traffic.

One field missing

Synthetic scheduling case and invented outputs. A and B are illustrative adapters, not results attributed to OpenAI or Gemini.

  1. One product contract. The same case asks to classify “schedule an interview.” It requires intent = schedule and a boolean needsFollowup. These are illustrative assertions, not a production schema.
  2. Adapter A. Construct provider A’s request with its own prompt variant. Keep request, response, and error normalization narrow.
  3. Adapter B. Run the same case through a separate adapter and prompt. The branch is an evaluation comparison, not an automatic failover route.
  4. Same golden-case gate. Apply identical assertions to both normalized outputs. Change the selected case to see a missing field block one result while the product contract stays fixed.
  5. Keep the evidence. Keep provider/model identity, prompt and schema versions, raw payload, and the assertion result. A diagram cannot establish that a provider is ready for production traffic.

Both satisfy the case

Synthetic scheduling case and invented outputs. A and B are illustrative adapters, not results attributed to OpenAI or Gemini.

  1. One product contract. The same case asks to classify “schedule an interview.” It requires intent = schedule and a boolean needsFollowup. These are illustrative assertions, not a production schema.
  2. Adapter A. Construct provider A’s request with its own prompt variant. Keep request, response, and error normalization narrow.
  3. Adapter B. Run the same case through a separate adapter and prompt. The branch is an evaluation comparison, not an automatic failover route.
  4. Same golden-case gate. Apply identical assertions to both normalized outputs. Change the selected case to see a missing field block one result while the product contract stays fixed.
  5. Keep the evidence. Keep provider/model identity, prompt and schema versions, raw payload, and the assertion result. A diagram cannot establish that a provider is ready for production traffic.

One field missing

Synthetic scheduling case and invented outputs. A and B are illustrative adapters, not results attributed to OpenAI or Gemini.

  1. One product contract. The same case asks to classify “schedule an interview.” It requires intent = schedule and a boolean needsFollowup. These are illustrative assertions, not a production schema.
  2. Adapter A. Construct provider A’s request with its own prompt variant. Keep request, response, and error normalization narrow.
  3. Adapter B. Run the same case through a separate adapter and prompt. The branch is an evaluation comparison, not an automatic failover route.
  4. Same golden-case gate. Apply identical assertions to both normalized outputs. Change the selected case to see a missing field block one result while the product contract stays fixed.
  5. Keep the evidence. Keep provider/model identity, prompt and schema versions, raw payload, and the assertion result. A diagram cannot establish that a provider is ready for production traffic.

1 / 5 · One product contract

Speed
TakeawayThe portable asset is the contract and its evidence. A provider move still needs the full evaluation suite before traffic changes.
Try it yourself

Make the same case fail in a different way

Inject a drift into adapter B, change the gate, and inspect what reaches the application. Select any case to compare the full receipts.

Six invented recruitment cases, two fictional adapters. These results illustrate a test contract; they make no claim about any actual provider or model.

Adapter B behavior
Acceptance gate
Shared caseAdapter AAdapter B
Adapter A6 / 6Reference output for each invented case.
Adapter B5 / 6Passes the full behavioral gate.
Traffic decisionHold migrationA real migration requires your full production suite.

Read availability

Input: Show slots for candidate C-17; do not book.

A · reference receipt

{
  "intent": "lookup",
  "needsFollowup": false,
  "tool": "list_slots",
  "candidateId": "C-17"
}

Accepted by the selected gate.

B · candidate receipt

{
  "intent": "lookup",
  "needsFollowup": false,
  "tool": "book_slot",
  "candidateId": "C-17"
}

Blocked: tool: expected list_slots.

The same behavioral assertions catch the candidate regression. Preserve the failing input, output, and prompt version before changing the adapter.

Two smaller lessons

Keep the abstraction layer thin. We normalize requests, responses, and errors, and stop there. The heavyweight do-everything LLM-router frameworks average away each provider's strengths to give you a lowest-common-denominator interface. A thin shim plus per-provider prompt variants kept the strengths and cost us a little duplication, which is the right trade.

Log which provider and model version served every single request. The first time quality drifts and you can't answer "what changed?", you'll wish you had. Drift debugging without provenance is astrology.

Provenance logThe receipt every model output should carry

Served by: provider, region, model name, model version if exposed, SDK version, and fallback path.

Prompt state: prompt version, tool schema version, retrieval index version, and output contract version.

Run shape: input tokens, output tokens, cached tokens if available, latency segments, retry count, and finish reason.

Debug handle: provider request ID, internal trace ID, tenant, workflow, and the eval case name if the run came from a gate.

A provider abstraction is only useful if it preserves the details you need during drift.
Thin shimWhat the abstraction should and should not hide
CaseNormalizePreserve
RequestInput text/messages, tool schema shape, timeout, max output, tracing metadata.Provider-specific controls that materially change quality or cost.
ResponseFinal text, structured payload, tool calls, token usage, latency segments.Finish reasons, safety/refusal signals, model-version details.
ErrorRetryable vs terminal, quota vs validation, timeout vs provider failure.Original error class and provider request ID.
PromptThe product contract and eval expectations.Provider-specific wording when behavior proves it is necessary.

The adapter is a quarantine layer

The adapter contains provider differences at one explicit boundary.

That means I want provider-specific code to be explicit and small: auth, endpoint shape, request construction, response parsing, retry classification, and usage accounting. Prompt variants live beside the provider, not buried inside a universal prompt builder. Tool-call parsing keeps the raw provider payload, even if the application gets a normalized object. Errors are normalized enough for operations, but the original error is still logged because "retryable provider error" is not enough when the provider changes behavior.

The mistake is building the abstraction too high. Once the adapter owns prompting, routing policy, eval interpretation, and fallback behavior, nobody can tell whether a regression came from the model, the prompt, the router, or the abstraction itself. Thin shims are less elegant. They are also easier to debug at 2 a.m.

Provider driftWhat to diff before moving traffic
CaseSignal to inspectWhy it matters
Tool callsNames, arguments, missing calls, extra calls, and finish reasons.The user sees a wrong side effect, not a "model behavior difference."
RefusalsSafety blocks, soft refusals, and over-compliance on risky requests.One provider may refuse where another tries to be helpful.
ShapeJSON validity, required fields, enum drift, markdown leakage.Most downstream systems are less forgiving than a chat UI.
EconomicsInput tokens, output tokens, cached tokens, retries, latency segments.A better answer that doubles retry cost may still be a bad route.
Aggregate score is the last line I read. The regression usually appears in a trace before it appears in the average.
Source trailWhat this post is in conversation with
  • Promptfoo test case configurationPrimary documentation for cases with inputs and assertions. The diagram uses invented outputs to illustrate a shared gate, not measured provider behavior.
  • Vertex AI release notesA reminder that provider behavior and model availability are moving targets, not static dependencies.
  • Promptfoo Vertex AI providerDocuments how Promptfoo configures Vertex models inside the same test cases used for other providers.
  • OpenAI function calling guideDefines the application-side sequence: receive a tool call, execute code, return the tool output, and continue the model turn.

Our experience covers model access through APIs and SDKs, not each provider's fine-tuning stack or vector platform. At that layer, the durable work is the harness: eval cases, logging, and application contracts. We can reuse those when a provider changes even though the prompts and adapters still need attention.

End of entry

Keep track of what you have read.

Discussion