Koosha ParidehpourSystems / products / infrastructure
External OSS contribution note9 min

Why we forked OmniRoute — and how to avoid it

OmniRoute is one of the most active AI routing projects on GitHub. I shipped 101 merged PRs to it as an external contributor before we forked it. So why fork at all?

AI InfrastructureOpen SourceForks

Every model provider carries its own quirks, rate limits, and failure modes, and no single vendor handles all of them gracefully. OmniRoute fills that gap: one API surface in front of the long tail — Anthropic, OpenAI, Gemini, Groq, Mistral, OpenRouter, plus community adapters — with retries, fallbacks, and routing on top. It has roughly 60,000 stars and is one of the most active AI routing projects on GitHub.

I contributed to OmniRoute as an external contributor across a focused 28-day window ending July 18, 2026: 101 merged pull requests, work referenced in 21 upstream release notes, and a #5 rank in the upstream contributor census. That is the signal most teams read as 'good enough — keep working upstream.' So why fork it?

The answer isn't that OmniRoute is broken or its maintainer unresponsive. Diego Souza has been consistently responsive, velocity is high, and the upstream architecture is sound. The answer is narrower: how production deployments of AI infrastructure diverge from a general-purpose open-source project, and what to do when they do.

What broke for us at scale

Phenotype runs OmniRoute in production in front of roughly a dozen model providers. By mid-2026 we had specific operational needs that didn't fit the upstream shape:

  • Provider-specific rate limit semantics. Upstream's generic cooldown model covers most providers; we needed curves per provider family, because Anthropic's TPM behavior, OpenAI's RPM behavior, Groq's burst handling, and the OpenRouter pass-through all differ. Those are not upstream anti-patterns — they are simply not modeled yet.
  • Multi-tenant fallback chaining. Traffic spans multiple upstream accounts, so degradation has to be predictable when one burns out. Upstream's fallback cache scoping is a correct default but exposes too few hooks for our workload.
  • Audit trails. Compliance requires per-request forensics on routing decisions: provider tried first, why the fallback fired, the latency profile, the token cost. Upstream's logging is good; our requirements are stricter.
  • CI surface. Roughly 12,000 unit tests across 40 packages must run predictably in a fork-only environment. Upstream's CI fits upstream's release cadence; it lacks the gating our pre-merge cycle needs.
  • Internal tooling. We have home-grown tools — agileplus, phenodag, a custom dockerized runner — that are meaningless to upstream but necessary for our engineering velocity.

None of these is a blocker. Each is a small adaptation. Together they made a fork the right call.

How we keep the fork aligned

The single biggest mistake you can make with a fork of an active upstream is letting it drift. A fork that hasn't rebased in six months is a fork that has to be re-merged by hand, and at that point you've stopped maintaining a fork and started maintaining a competing project.

We use a simple set of disciplines:

  • Daily git fetch upstream, weekly rebase. Feature branches rebase onto upstream/release/v3.8.x before merging into fork main. The current upstream SHA is pinned in .upstream-ref, and CI fails when that file is more than 30 days stale.
  • Linear history on main, no merge commits. Every fork commit is a cherry-pick of upstream, a fork-only change on upstream HEAD, or a back-port of a future-upstream change we needed early. That keeps the diff against upstream trivial to compute and review.
  • Upstream-first PRs. Any fix that could plausibly benefit upstream goes upstream first; otherwise we wait for merge or carry the patch as a fork-only commit with a stable identifier. The 101 PRs I merged upstream before the fork set the pattern: roughly 85% of fork-only commits become upstream PRs within 60 days.
  • Branch protection as a tripwire. Fork main requires one PR review, dismisses stale approvals on push, and blocks force-pushes. Upstream has none (single-maintainer pattern). The stricter setting has caught two of our own mistakes before they reached trunk.
  • A divergence manifest. A machine-readable JSON file lists every fork-only commit with its rationale, upstream-equivalent status (sent / awaiting-review / declined / not-applicable), and merge-odds estimate. CI reads it on every PR to warn when a fork-only commit should have gone upstream first.

What we're contributing back, not carrying

The bulk of our fork-only commits are things like:

  • Provider-specific cooldown curves (now in flight upstream as standalone PRs)
  • An additional audit-log schema (sent upstream, awaiting review)
  • A handful of test-suite reorganizations (some merged, some declined as overly opinionated)
  • Internal-only tooling in .agileplus/ and worklogs/ — not upstream-portable, git-ignored from the public mirror

We carry roughly 700–800 fork-only commits ahead of upstream main at any given time. About 75% of them are CI/build tooling, ~15% are provider-specific behavior, ~10% are internal-only artifacts that never go upstream.

What the fork is not

It's worth being explicit about what our fork isn't:

  • Not a competing product. OmniRoute is the product. We're heavy users of it and depend on it. The fork is an extension, not a replacement.
  • Not a long-term divergence. The point is to converge with upstream as fast as possible; branches not on a path to upstream retire on a 90-day clock.
  • Not an opinionated re-architecture. Where upstream's architecture doesn't fit, we work around it rather than redesign it. Upstream's design choices are upstream's call.
  • Not a candidate for a separate brand. No 'PhenRoute'. The point is keeping the fork's relationship to upstream legible: fork → upstream → fork → upstream.

If you're considering a fork yourself

A few heuristics from our experience:

  • Fork when you have 3+ concrete operational needs that don't fit upstream. One quirk is a patch. Two quirks is a configuration. Three is a fork.
  • Fork when your CI/audit/security posture is meaningfully stricter than upstream's default. If you just need a feature, send a PR upstream. If you need a guarantee upstream can't make, fork.
  • Don't fork if you're going to rebrand or re-architect. That's a competing project, not a fork. Use a different name and a different namespace.
  • Don't fork without the discipline. A drifting fork is a tax you pay forever: without weekly rebases and upstream-first PRs, you rewrite the fork in two years.

What's next

The open question is whether the end-state is 'fork forever' or 'upstream absorbs the fork-only features and we deprecate the fork.' Diego and I have discussed it; the working hypothesis is convergence. Most of what we carry is upstream-portable, so a year from now the fork should be roughly 50 commits deep instead of 700+.

For now the fork exists because it has to, and it costs us very little to maintain. The 101 PRs I shipped upstream first were not wasted — they are the social capital that keeps the fork relationship healthy. Upstream knows us and accepts most of what we send, which beats an anonymous fork with no shared history.