Files
OmniRoute/tests/unit/resolve-routing-model.test.ts
NOXX - Commiter 46eac9813f fix(guardrails/chat): stop Vision Bridge hijacking credentialed models to opencode-zen (#7204)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)

* fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179)

* fix(guardrails/chat): stop Vision Bridge hijacking credentialed models to opencode-zen

OpenCode (and similar clients) often send image parts in long sessions.
Vision Bridge treated the request model as non-vision and whole-request-
rerouted to getBestVisionModel(), which preferred opencode-* (priority 0).
That landed on a noauth connection and returned 401 Missing API key —
while proxies/combos still logged the original target (zai/glm-5.2, grok-cli, …).

Also: after resolveRoutingModel(X-Route-Model), keep body.model aligned so the
post-guardrail "body.model !== modelStr" path cannot undo the routing header.

- visionBridge: skip whole-request reroute when original model has usable creds
- visionBridge: refuse reroute to targets known unusable (noauth without key)
- visionBridgeRouter: deprioritize opencode-* for auto vision pick
- chat: alignBodyModelWithRouting + only adopt true guardrail model mutations
- tests: VB-CRED-01/02 + alignBodyModelWithRouting coverage

* fix(guardrails/chat): keep chat.ts under the file-size ratchet and update stale vision-bridge tests for the credential-aware reroute skip

- Extract the routing-model reconciliation logic (X-Route-Model align,
  post-guardrail reroute policy re-check, hook model override) into
  RoutingModelOps helpers in resolveRoutingModel.ts, shrinking chat.ts back
  under the frozen 1796-line file-size baseline (was 1837).
- Update tests/unit/guardrails/vision-bridge-callmodel.test.ts: the fallback
  mock must match whichever API shape the selected fallback model actually
  calls (OpenAI-compatible vs Anthropic), since the vision-bridge router
  priority fix in this PR can now legitimately select an Anthropic fallback
  model instead of always defaulting to an OpenAI-shaped opencode-* model.
- Update tests/unit/vision-bridge-policy-reroute-6640.test.ts: per this PR's
  own VB-CRED-01 test, a credentialed original model is now intentionally
  never whole-request-rerouted (it always falls through to describe-then-
  forward) — so the pre-existing #6640 tests are updated to assert the
  final, user-facing answer always comes from the original credentialed
  model, matching the new intended behavior instead of the retired
  whole-request-reroute path.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(guardrails/chat): reduce isProviderConnectionUsable cyclomatic complexity to satisfy the project-wide complexity ratchet

The new isProviderConnectionUsable helper (complexity 21) regressed the
project-wide complexity ratchet from 2056 to 2057. Refactor it to use Set
membership checks and small extracted helpers (hasNonEmptyString,
hasOAuthCredential) instead of chained === / || comparisons — same behavior,
verified by the existing "isProviderConnectionUsable rejects noauth without
api key" test, with complexity back under the 15-per-function threshold and
the project-wide ratchet back at the 2056 baseline.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-18 21:18:10 -03:00

66 lines
2.6 KiB
TypeScript

// Regression guard for #4863: X-Route-Model header overrides body.model for routing.
// Also covers alignBodyModelWithRouting — without body alignment the post-guardrail
// path silently restores body.model and undoes the header (zai header + opencode body → 401).
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
alignBodyModelWithRouting,
resolveRoutingModel,
} from "../../src/sse/handlers/resolveRoutingModel.ts";
function req(headers: Record<string, string>) {
return { headers: { get: (n: string) => headers[n.toLowerCase()] ?? null } };
}
describe("resolveRoutingModel (#4863)", () => {
it("uses body.model when no X-Route-Model header is present", () => {
assert.equal(resolveRoutingModel(req({}), { model: "gpt-5.3-codex" }), "gpt-5.3-codex");
});
it("X-Route-Model header overrides body.model", () => {
assert.equal(
resolveRoutingModel(req({ "x-route-model": "my-combo" }), { model: "codex/gpt-5.3-codex" }),
"my-combo"
);
});
it("trims surrounding whitespace from the header value", () => {
assert.equal(
resolveRoutingModel(req({ "x-route-model": " alias-x " }), { model: "fallback" }),
"alias-x"
);
});
it("falls back to body.model when the header is empty/whitespace-only", () => {
assert.equal(resolveRoutingModel(req({ "x-route-model": " " }), { model: "fallback" }), "fallback");
});
});
describe("alignBodyModelWithRouting (X-Route-Model body lockstep)", () => {
it("rewrites body.model when it differs from the routing model", () => {
const body = { model: "opencode-zen/gpt-5.4", messages: [{ role: "user", content: "hi" }] };
const routed = resolveRoutingModel(req({ "x-route-model": "zai/glm-5.2" }), body);
const result = alignBodyModelWithRouting(body, routed);
assert.equal(routed, "zai/glm-5.2");
assert.equal(result.aligned, true);
assert.equal(result.previousModel, "opencode-zen/gpt-5.4");
assert.equal(result.body.model, "zai/glm-5.2");
// Original body object is not mutated
assert.equal(body.model, "opencode-zen/gpt-5.4");
});
it("is a no-op when body.model already matches", () => {
const body = { model: "zai/glm-5.2" };
const result = alignBodyModelWithRouting(body, "zai/glm-5.2");
assert.equal(result.aligned, false);
assert.equal(result.body, body);
});
it("is a no-op when routing model is empty", () => {
const body = { model: "opencode-zen/gpt-5.4" };
const result = alignBodyModelWithRouting(body, null);
assert.equal(result.aligned, false);
assert.equal(result.body.model, "opencode-zen/gpt-5.4");
});
});