Skip to content

OpenRouter chat/LLM router simulation#328

Merged
jpr5 merged 2 commits into
mainfrom
feat/openrouter-chat-integration
Jul 23, 2026
Merged

OpenRouter chat/LLM router simulation#328
jpr5 merged 2 commits into
mainfrom
feat/openrouter-chat-integration

Conversation

@jpr5

@jpr5 jpr5 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

OpenRouter chat/LLM router simulation

Adds aimock's first OpenRouter chat/router integration. Where aimock already mocks OpenRouter's video surface, this adds the chat/LLM surface — and, more importantly, deterministically simulates OpenRouter's routing layer offline (model fallback / failover, provider selection, cost), a category no existing mock fills.

What it does

  • Detection: OpenRouter requests are identified by the canonical /api/v1/ base prefix (captured before the compat-normalizer rewrites it). OpenAI /v1/… callers are byte-for-byte unchanged (pinned by tests, incl. streaming).
  • Response shaping (streaming + non-streaming): gen- id, top-level provider (winning slug's author by default, fixture-overridable), both finish_reason and native_finish_reason, always-present system_fingerprint/service_tier, and rich fixture-scriptable usagecost, three-field cost_details, is_byok, and token-detail objects.
  • models[] fallback simulation — the wedge: iterate [model, ...models], serve the first non-error match, fall through 429/503 runtime-error candidates, honor provider.allow_fallbacks:false (fail-closed), and echo the winning slug as response.model. Lets a test assert "primary is down → my app consumed the fallback" with zero network.
  • Discovery: GET /api/v1/models, /api/v1/key, /api/v1/credits.
  • Error envelope: { error: { code, message, metadata? } } with code == HTTP status.
  • Opt-in : OPENROUTER PROCESSING SSE keepalive.

Wire fidelity — verified against real bytes

Field shapes were adversarially verified against three independent sources before implementation: OpenRouter's OpenAPI spec, both official SDKs' source (@openrouter/sdk, @openrouter/ai-sdk-provider), and a live capture with a real key. This caught that the spec's chatcmpl- id example and nested-provider shape do not match real runtime bytes (which use gen- ids and a top-level provider) — a deviation that docs-only research would have shipped.

Competitive matrix + drift automation

Adds an "OpenRouter router / fallback simulation" row to the landing competitive matrix (aimock ✓; MSW / VidaiMock / mock-llm / piyook / mokksy ✗ — none mock OpenRouter's router), plus a FEATURE_RULES entry so the weekly drift job tracks whether any competitor ships it.

Testing / review

  • Full suite green (pnpm test); 20+ new tests including OpenAI-streaming byte-isolation, tool-call shaping, allow_fallbacks:false fail-closed, and scriptable-cost.
  • Red→green proven against the real HTTP surface (server + real fetch), not fakes.
  • Reviewed through an exhaustive multi-round CR (converged; Procedure 3 bucket-(c) audit: 0 promotions).
  • No version bump — release is a separate, gated step.

Follow-ups (intentionally out of scope)

  • v1.1 feature: error-class-bound fallthrough (reproduce OpenRouter's real inconsistent-failover bug; anchor: openclaw#60191), streaming reasoning_details mirror, and the deferred discovery/legacy endpoints.
  • Separate PR: pre-existing bugs in scripts/update-competitive-matrix.ts (broken migration-page paths, unanchored keyword regexes, loose provider counting) — predate this change; our added tracking row is unaffected (verified).
  • Adding the OpenRouter chat surface to the live drift canary once an OpenRouter key is wired into CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_018yy1y2Cwc31DaB4UNqvT8K

jpr5 added 2 commits July 22, 2026 17:02
Detect OpenRouter by the canonical /api/v1 base prefix and shape the
OpenAI-compatible chat response into OpenRouter form: gen- id, top-level
provider (winner slug-author default, fixture-overridable), finish_reason
+ native_finish_reason, always-present system_fingerprint/service_tier, and
rich fixture-scriptable usage (cost, three-field cost_details, is_byok,
token-detail objects). Field shapes verified against real captured bytes.

models[] fallback simulation: iterate [model, ...models], serve the first
non-error match, fall through 429/503 runtime-error candidates, honor
provider.allow_fallbacks:false (fail-closed), echo the winning slug as
response.model. Discovery GET /api/v1/models|key|credits. OpenRouter error
envelope {error:{code,message,metadata?}} (code==status). Opt-in
: OPENROUTER PROCESSING SSE keepalive. Usage estimation unified across
streaming/non-streaming. OpenAI /v1 callers byte-for-byte unchanged.

Adds a FEATURE_RULES entry so the competitive-matrix drift job tracks
whether any competitor ships OpenRouter router mocking. 20+ tests incl.
OpenAI-streaming byte-isolation and tool-call shaping.
New docs/openrouter-chat page (examples from real captured bytes), sidebar/
landing/compatible-providers/fixtures/record-replay/migrate pages, README
(13 providers / 15 surfaces), write-fixtures skill, CHANGELOG. Adds the
'OpenRouter router / fallback simulation' row to the competitive matrix
(aimock deterministic; competitors none).
@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@328

commit: df6c20e

@jpr5
jpr5 marked this pull request as ready for review July 23, 2026 00:44
@jpr5
jpr5 merged commit 27da86f into main Jul 23, 2026
24 checks passed
@jpr5
jpr5 deleted the feat/openrouter-chat-integration branch July 23, 2026 00:44
jpr5 added a commit that referenced this pull request Jul 23, 2026
## Error-class-bound fallthrough for the OpenRouter `models[]` fallback
engine

Follow-up to #328. That PR shipped `models[]` failover (an error-fixture
candidate falls through to the next model, unless
`provider.allow_fallbacks:false`). But real OpenRouter fails over
**inconsistently by error class** — the loudest authentic user complaint
(openclaw#60191, #45834, #19249): a `403 budget-exceeded` or generic
"provider error" does **not** advance to the next model (it retries the
same primary), while 429/503 do. Users can't rehearse that.

### What this adds
An optional **`fallthrough?: boolean`** on the error fixture response:
- **absent (default)** → current behavior: the error candidate falls
through to the next `models[]` candidate (fully backward-compatible).
- **`fallthrough: false`** → the error is **terminal**: the loop stops
and serves that error, no failover — reproducing "this error class
doesn't fail over."

It composes with the request-level `provider.allow_fallbacks:false`
(either signal makes an error terminal). A dev models an error *class*
by setting `fallthrough:false` on that class's fixture (e.g. the 403)
and leaving 429/503 fixtures to default — so they can reproduce the
exact failure that burned them and assert their handling, *and* the
positive "fallback saved me" path.

### Correctness
- **Load-time validation**: `validateFixtures` rejects a non-boolean
`fallthrough` (`"false"`, `0`, `null`) — without this, a JSON author's
`"false"` string would silently invert TERMINAL→fall-through
(fail-closed quietly failing open).
- A winning non-error slug is echoed as `response.model`; a terminal
error is served as the OpenRouter error envelope
(`{error:{code,message,metadata?}}`, no `model` field).
- Default path is byte-identical to #328's fall-through.

### Testing / review
- Red→green against the real HTTP surface; new tests for
`fallthrough:false` terminal, default fall-through, composition, and the
non-boolean load-time rejection.
- Full suite green (`pnpm test`); tsc/lint/build/exports clean. No
version bump (release is separate).
- Reviewed via a multi-round CR (converged; silent-failure clean,
gate/threading/validation confirmed correct).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018yy1y2Cwc31DaB4UNqvT8K
jpr5 added a commit that referenced this pull request Jul 23, 2026
…tion) (#333)

## OpenRouter chat drift leg (cost-safe live canary + static envelope
triangulation)

Follow-up to #328, which shipped the OpenRouter chat/router mock
(`src/openrouter-chat.ts`, `/api/v1/chat/completions` etc.) with **zero
drift coverage**. This adds
`src/__tests__/drift/openrouter-chat.drift.ts`, mirroring the existing
`openrouter-video.drift.ts` pattern **cost-safely** (no paid
completions).

### Two parts
1. **Live canary (FREE, key-gated)** — a new `listOpenRouterModels()`
GETs the public `GET /api/v1/models` catalog (metadata only, never a
paid generation) and asserts the chat-model families aimock mirrors are
still present and the model-object schema hasn't drifted.
`describe.skipIf(!OPENROUTER_API_KEY)` — runs in CI where the secret
exists, skips cleanly locally.
2. **Static envelope triangulation (no key)** — drives the real aimock
handler over HTTP and triangulates the OpenRouter chat envelopes it
emits (non-streaming `gen-` id / top-level `provider` /
`native_finish_reason` / `usage`+`cost_details` / `system_fingerprint` /
`service_tier`, a streaming chunk, and the error envelope) against
hand-authored conformant exemplars.

Registers the `openrouter-chat` surface in `surface-registry.ts`.
`OPENROUTER_API_KEY` is already a repo secret and wired into
`test-drift.yml`.

### Review
- Drift-detection demonstrated: perturbing an exemplar produced a real
`API DRIFT DETECTED / TYPE MISMATCH` report; reverted → green.
- CR caught and fixed a **`system_fingerprint` false-pass**
(existence-only assertion) — now value-asserted and **mutation-proven**
(forcing the mock to emit `null` fails the leg); plus a named
`json.data` catalog-drift guard and an empty-catalog guard so the canary
fails loudly, never silently.
- Tests-inclusive typecheck clean (the drift file is under
`src/__tests__`, excluded from the base `tsc`); leg green (static pass /
live skip); build green. No version bump.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018yy1y2Cwc31DaB4UNqvT8K
jpr5 added a commit that referenced this pull request Jul 23, 2026
…safety) (#331)

## Why

`scripts/update-competitive-matrix.ts` runs weekly
(`.github/workflows/update-competitive-matrix.yml`) and **auto-commits
to public, customer-facing docs** (`docs/index.html` and
`docs/migrate-from-*/index.html`). Six pre-existing bugs (all predate
#328) caused it to write wrong data or silently skip updates. This PR
fixes all six and adds real red→green regression coverage.

`#328`'s single new FEATURE_RULES entry ("OpenRouter router / fallback
simulation") and its migration-variant entry are **left untouched** and
are covered by an explicit regression guard.

## The 6 bugs & fixes

| # | Bug | Fix |
|---|-----|-----|
| 1 | `COMPETITOR_MIGRATION_PAGES` mapped to `docs/migrate-from-*.html`,
but the pages are **directories** (`.../index.html`), so `existsSync`
always failed → every migration-page update silently skipped.
`mokksy/ai-mocks` had no mapping at all. | Paths → `/index.html`; added
the `mokksy/ai-mocks` mapping. (Note: `VidaiMock` was already mapped;
the genuinely missing competitor was `mokksy/ai-mocks`.) |
| 2 | Unanchored keyword regexes: `cli`→"client"/"click", `sse`→"assess"
→ false ✗→✓ flips in public docs. | New `keywordRegex()` wraps each
keyword in non-alphanumeric boundary lookarounds
`(?<![a-z0-9])…(?![a-z0-9])`; works for literal tokens, path/regex
keywords, and edge-punctuation keywords alike. |
| 3 | `countProviders` over-counted via substrings (`cohere`→"coherent",
`aws`→"flaws") plus a redundant `gemini.*interactions` group
double-counting Gemini. | Bounded matching (same helper) + removed the
redundant group (13 groups, each provider counted once). |
| 4 | `String.replace(pattern, htmlString)` let literal `$`, `$&`, `$1`
in HTML be interpreted → corrupted/duplicated output (3 sites). |
Switched the HTML-derived replacements to **function form** `(…) =>
text` so replacement text stays literal. |
| 5 | Migration feature-cell updater assumed the competitor column was
adjacent to the label; the provider-count updater located it by header
**name** — they disagreed when an intervening column (e.g. aimock)
existed. | Both now use one shared `findMigrationCompetitorColumn()`
(token-aware header-name match — avoids the `"vidaimock"⊇"aimock"`
substring trap). |
| 6 | Orphaned variant key `"Realtime translate/whisper"` never matched
its rule label `"Realtime transcription/translation"` → that rule got no
migration variants, cells never flipped. | Renamed the key to match the
rule label exactly. |

## Tests

A 1168-line `competitive-matrix.test.ts` already existed but
**reimplemented the script logic inline** (a mirror), so it tested
copies that mirrored the bugs — it could never catch them. Converted it
to **import the real script** (added `export`s + an `import.meta`
main-guard so importing doesn't run `main()`), fixed the one assertion
that encoded the buggy behavior (`countProviders` all-groups `14`→`13`),
and added red→green repros for each bug plus a regression guard for the
OpenRouter entry.

## Red → Green (fixture-based, no network)

**RED** — bug repros run against the pre-fix script (enabling refactor
only, no behavioral change):

```
Tests  13 failed | 49 passed (62)
```
The 13 failures were exactly:
- Bug 1: `maps every mapped competitor to a file that actually exists on
disk`; `includes a mapping for the mokksy/ai-mocks competitor`
- Bug 2: `does not flip "CLI server" from the words "client"/"click"`;
`does not flip "Chat Completions SSE" from the word "assess"`
- Bug 3: `does not count "cohere" inside "coherent" or "aws" inside
"flaws"`; `counts Gemini exactly once`; `counts all 13 provider groups
…`
- Bug 4: `applyChanges does not duplicate the row when replacement text
contains $&`; `updateProviderCounts does not corrupt the table when a
cell contains $&`
- Bug 5: `flips the competitor cell even when aimock is the first data
column`; `resolves the column when the header text differs from the
competitor key (Mokksy)`
- Bug 6: `returns variants for the real rule label 'Realtime
transcription/translation'`; `flips a migration row that uses a variant
label for the realtime rule`

(The #328 OpenRouter guard tests and the positive-control tests passed
in the RED run, as expected.)

**GREEN** — after the 6 fixes:

```
✓ src/__tests__/competitive-matrix.test.ts (62 tests)
Test Files  1 passed (1)
      Tests  62 passed (62)
```

**Full suite / build / lint** (no regressions):

```
Tests  4706 passed | 44 skipped (4750)     # vitest run
Build complete                              # pnpm build (tsdown)
All matched files use Prettier code style!  # pnpm format:check
eslint .  → 0 problems                      # pnpm lint
```

**Live end-to-end** — `npx tsx scripts/update-competitive-matrix.ts
--dry-run` against real GitHub still fires `main()`, parses all 4
competitors + the 38-row / 6-column matrix, and reports "No changes
detected" (no spurious flips).

## Scope / safety
- No package version bump (this is scripts/docs tooling, not the
published package surface).
- `#328`'s FEATURE_RULES + variant entry unchanged; guarded by
regression tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01KamK73Wu5heLxJEogM6hHC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant