# Adding a second model to my agent fleet: the wiring was the cheap part

Author: Maksym Tytarenko | Date: 2026-08-30 | Category: AI & ML | Tags: ai-agents, autonomous-agents, code-review, llm-ops, engineering-management
Canonical: https://www.tytarenkoagency.com/blog/adding-a-second-model-to-my-agent-fleet-the-wiring-was-the-cheap-part

> A second model went onto my autonomous coding fleet with one environment swap. The expensive work was deciding who reviews whom, and three defects the diff could not show me.

I added a second model to my agent fleet last night. The wiring took one environment swap. Everything that actually cost me time came afterwards, and almost none of it was visible in the diff.

Here is what happened, in the order it happened, because the order is the lesson.

## The provider I was about to over-engineer

My fleet runs autonomous coding agents in containers: one picks up a task from the board, writes the code, opens a PR, another reviews it, and I get a card in Telegram asking whether to merge. Until yesterday every executor was Claude, plus a narrow short-circuit that sends small, spec-complete tasks to Grok.

I have a documented checklist for adding a non-Claude executor. It is a real checklist, written when Grok went in: its own launch branch, its own credential contract, its own result parser, and a context pack, because a foreign CLI cannot read my rules and skills on its own.

I opened that checklist and stopped. Z.ai serves the Anthropic protocol. That means a GLM run is not a different CLI at all: it is the *same* binary, in the *same* image, with two environment variables pointed somewhere else. Every hardening layer that took work to extend to Grok (the git and gh path shims, the read-only mounts, the pre-tool guard, the container limits) was already in force, unchanged. Rules and skills load natively. There was no pack to build and nothing to deduplicate.

So the vocabulary was the load-bearing decision. A **vendor** is which CLI runs inside the container: Claude Code, or Grok's own binary, each with its own credential, its own result format and its own idea of how to read project rules. A **backend** is which API answers when that CLI is Claude Code: Anthropic, or Z.ai. Swapping the vendor means a new adapter. Swapping the backend means two environment variables. Both are needed, and conflating them is what produces an adapter nobody needs. I wrote that down as an architecture decision record and put a warning at the top of the vendor checklist, because the next person to reach for it will be wrong the same way I nearly was.

![An infographic showing the concept of vendor vs backend in software architecture.](https://media.tytarenkoagency.com/ai-generated/uploads/20260830_202156_370dc0d4.png)


Cost of the wiring: one function that returns a different environment dict, and one orthogonal axis in the router.

## The part that was actually a design problem

If half the work runs on a different model, who reviews it?

![A flowchart illustrating the review process for various coding models.](https://media.tytarenkoagency.com/ai-generated/uploads/20260830_202138_cf16e67e.png)


The answer I chose in a grilling session: whoever did not write it. Executor Claude, reviewer GLM. Executor GLM, reviewer Claude. Full symmetry, and the reviewer's verdict gates the merge. It is not an advisory second opinion.

That decision immediately killed something I had been paying for. My pipeline already ran a cross-vendor skeptic pass on every PR: a diff-only reviewer from a third provider, whose entire premise is that a same-vendor reviewer shares the author's blind spots. Under paired review that premise stops holding on most PRs.

I did not decide that on taste. I had measured it. A strict recount of its yield put honest confirmation of its findings at **17%**. Confirmed here means something specific: the next same-vendor review independently raised the same finding, or I acted on it myself. An earlier, friendlier figure of 38% counted a third signal, post-approval commits by the bot, which turned out to be indistinguishable from automated CI fixes made by the same identity. Once those were excluded, the clean count of findings I confirmed myself was **zero**. A follow-up slice hinted its value concentrated on the non-Claude vendor's diffs, 2.5x more confirmed findings, but that conclusion was withdrawn: seven events and an attribution defect. So the honest state was "unanswered", not "answered negatively".

The pass now runs only when there is no pair: when the share is set to one side, when a backend is degraded, out of credits, or keyless. That is the exact case where its premise still holds.

One more thing came out of the same session. My danger-class scanner, the thing that flags a diff touching authentication, money, cross-service contracts or migrations, existed, worked, and was **annotation only**. It labelled the merge card and changed nothing about the review. Now a hit puts the checklist into the reviewer's prompt (input normalization, key agreement across service boundaries, invariant belongs in the schema, migration rollback story, the original repro must die at the boundary it was found at) and lifts that review to the top rung of whichever backend is reviewing.

## Three defects the diff could not show me

I shipped it. Then I found three things wrong, and none of them by re-reading my own code.

![An illustration of common software defects encountered during code review.](https://media.tytarenkoagency.com/ai-generated/uploads/20260830_202124_f9a128bf.png)


**One: stickiness that quietly decays.** A PR must not be half-written by two backends, so the draw sticks: I recorded which backend wrote it and reused that value for every later revise and re-review. I derived it from the executor's run record, because the model id is a perfect fingerprint. Then I looked at the retention policy: run records are pruned to the newest **20 files fleet-wide**, not per task. My fleet runs 16 tasks in parallel. On a busy night a PR's own record is gone within minutes, the sticky value silently becomes a fresh draw, and the PR flips vendor mid-flight, which is the exact failure stickiness exists to prevent. I moved it onto the PR's review track, which has precisely the right lifetime.

**Two: I weakened review exactly where it was strongest.** Watching the live fleet, I saw a PR written by Grok get reviewed by GLM. My pairing rule had no idea a third vendor existed: a Grok PR has no backend, my code coerced that to "Anthropic", and the rule dutifully flipped it to the other side. But a Grok PR is *already* cross-vendor (Grok writes, Claude verifies) and that reviewer is deliberately the strongest Claude, precisely because Grok is the executor I trust least. I had swapped it for the cheap tier of a brand-new provider. The tier was right the whole time; the backend silently was not.

**Three: the card lied.** My "took the task" notification prints the model. With one backend, the tier name was the model. With two, it stopped being: a task drawn to the new backend still displayed a Claude tier while a different model ran. And the merge card, the one card that gates a real decision, named no model at all. Asked directly whether the cards say GLM is doing the work, I had to answer that they say the opposite: a task running on the new model announced itself as `sonnet5`, a Claude tier name.

## How they were actually found

Two methods, neither of which is reading the diff again.

**Diff the test failures, not the code.** The full suite cannot finish in one run on my laptop, so I split it into six chunks and ran each one twice: on my branch and on clean main. Then I compared failure sets file for file. That is how the first defect surfaced: not as a failure, but as a question I only asked because I was staring at retention behaviour. It is also what stopped me from panicking: 14 failures reproduced identically on main (a Windows-only path spawning a shell stub), and one test that fails on main *passes* on my branch. Without the baseline I would have chased ghosts for an hour.

**Watch the fleet, not the tests.** Defects two and three were invisible to every test I had, because both were about what the system does with real traffic and what it tells a human. They showed up in one log line and one screenshot of a Telegram card.

## The lesson I had to learn three times in one night

I added a second argument to a model-resolution function. Twelve tests broke, every one of them a test double whose stub takes one argument.

I fixed that and moved a call into a different function. Nine more broke, because that function is called with a doubled controller in a dozen tests.

I fixed that and widened a persistence call. Four more broke, same shape.

In this codebase a widely-stubbed call shape is effectively public API. The cheap move is never a wider old entry point: it is a narrow new one, or moving the code to where the fact already exists. The final version of the card fix adds no parameter anywhere. The notification is simply sent from the place where the model is already known.

## What it costs, and what I verified before believing it

The plan is credit-based, not token-billed: **28,000 credits per 5-hour window, 140,000 per week**, and off-peak hours plus all weekend cost half. The flash tier I default to is documented by Z.ai as 320B total parameters with 18B active, a 1M-token context, and a 3x quota multiplier, which is what makes a 50% share affordable at all. Its output is capped at 128K, and the API rejects anything outside 1..131072 for that parameter, which is the sort of thing you find by asking for a million and reading the error.

The pool is shared with my laptop. That is a real risk to someone's session, mine, so the fleet degrades onto Claude on exhaustion, and degradation is keyed to the *backend*, never to the Claude tiers. Marking a healthy tier because a third-party endpoint had a bad night would route traffic away from the vendor that is actually up.

Before enabling anything I verified the network floor in both directions from inside a real task container. Before the change, the new endpoint through the proxy returned nothing at all. After, it answered with an authentication error, which is the correct answer, because it means the request arrived. A non-allowlisted provider still returns nothing, so the allowlist did not quietly become permissive. And with the proxy environment stripped, the container has no route out at all. Then, end to end: a container on the isolated network, with the deployed key, got a real completion back.

I also broke my own deploy doing this. I had hand-edited a tracked file on the box to test the network change before the PR landed; the deploy does a fast-forward pull, and it refused. Nothing was lost, because I checked the local edit was byte-identical to what had merged, but the lesson is cheap to state and annoying to learn: editing a tracked file on a production host before its change lands puts a mine under the next deploy.

## Where it stands

It is live at a 50/50 split. The first real proof was a Telegram card reading `sonnet5 → glm-5.3-flash`: a Claude tier name, an arrow, and the model that is actually serving it. Early draws ran five to two; the sample is far too small to mean anything and I am not going to pretend otherwise.

Three things are still unverified, and I would rather name them than let them look settled: what an exhausted quota actually returns, whether reasoning-effort settings survive the compatibility endpoint, and how fast a shared credit pool drains under real fleet load. All three are written into the pull request rather than into my head.

If you are adding a second model to something that already works, the order that helped me was:

![A checklist graphic highlighting the steps for adding a second model to a software system.](https://media.tytarenkoagency.com/ai-generated/uploads/20260830_202106_f93492f7.png)

 get the taxonomy right before the code, decide who reviews whom before you decide the split, and then assume your diff cannot show you the three things that will actually be wrong.

## FAQ

### How do you know which model actually ran a task?

You have to make the system say it. Ours could not: the notification named the tier, and a tier
stopped identifying a model the moment two providers could serve it. The fix was to send that card
from the point in the code where the choice is already made, and to record the reviewing model on the
pull request's own state so the merge card can name both sides.

### What does a second provider cost when the plan is credit-based rather than per-token?

Different arithmetic than an API bill. My plan grants a five-hour credit window and a weekly pool, and
off-peak hours plus the whole weekend are charged at half rate. That makes the interesting number not
price per million tokens but how much of a shared weekly pool one busy night consumes, which is why I
started at a measured share instead of switching everything over.

### How do you stop a failing provider from poisoning routing for the healthy one?

Key the health mark to the thing that actually failed. A bad night at a third-party endpoint says
nothing about the model tiers it was asked to serve, so degradation is recorded against the backend.
Marking the tier would have routed traffic away from the vendor that was still up.

### What should you check before letting a new backend touch production traffic?

The network floor, in both directions, from inside a real container: that the new endpoint is
reachable through the egress proxy, that a non-allowlisted one still is not, and that with the proxy
stripped there is no route out at all. Then one end-to-end call with the deployed credential, because
reachability and authentication fail differently.

## Related reading

- [Unknown Is Not Clean: Metrics for Code Your Agents Write](https://www.tytarenkoagency.com/blog/unknown-is-not-clean-metrics-for-code-your-agents-write)
- [Why I Rejected an 817-Skill Security Pack and Wrote a 2-File Threat-Model Skill](https://www.tytarenkoagency.com/blog/why-i-rejected-an-817-skill-security-pack-and-wrote-a-2-file-threat-model-skill)
- [Graph engineering is easy. Not blowing your budget on one workflow is the hard part](https://www.tytarenkoagency.com/blog/graph-engineering-is-easy-not-blowing-your-budget-on-one-workflow-is-the-hard-part)

