Files
OmniRoute/tests/unit/github-model-not-supported-lockout.test.ts
Tobias Andersen d8879371ea fix(combo): lock GitHub models rejected as "not supported" for future requests (#11781)
Follow-up to #11762/#11774, same bug class in combo's own model-lockout wiring: GitHub rejects several models (gpt-5.4, gpt-5.3-codex, etc.) with a 400 that's permanently unavailable for this account's Copilot integration, but nothing recorded a cross-request lockout — combo's #5249 in-request advance guard is correct but doesn't persist, so the same doomed model gets retried from scratch on every new request, indefinitely.

Fix: on a model-scoped 400 (`isModelScoped400`), call `lockModelIfPerModelQuota(provider, connectionId, rawModel, "model_capacity", 1h)`. GitHub already has per-model-quota enabled, so only the rejected model locks — siblings keep working. `isModelLocked()` is already checked pre-dispatch, so no other wiring needed.

Validated: 3/3 new tests + fixed a pre-existing test-isolation gap in combo-model-scoped-400-advance.test.ts (shared model name across sub-tests without clearing lockout state). Thanks!
2026-08-29 05:21:50 -03:00

51 lines
2.2 KiB
TypeScript

/**
* Regression guard: a GitHub Copilot 400 "The requested model is not
* supported" / "not available for integrator ..." — permanent for THIS
* account/integration — must get locked out via lockModelIfPerModelQuota so
* future, separate requests skip the same dead model instead of retrying it
* on every single auto-combo request forever (observed: every request in
* production logs wasted several upstream 400 calls on the same GitHub
* models — gpt-5.4, gpt-5.5, gpt-5.6-luna, etc. — all day).
*
* Combo's existing #5249/#2101 guard already lets the current request keep
* rotating to the next target — that behavior is unchanged and untested
* here. This guards the NEW cross-request lockout side effect only.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { isModelScoped400 } = await import("../../open-sse/services/combo/comboPredicates.ts");
const { lockModelIfPerModelQuota, isModelLocked, hasPerModelQuota } =
await import("../../open-sse/services/accountFallback.ts");
const GITHUB_NOT_SUPPORTED_400 = "[400]: The requested model is not supported.";
const GITHUB_INTEGRATOR_400 =
'[400]: The requested model is not available for integrator "vscode-chat". ' +
"Available models: [gpt-4.1 claude-fable-5]. Verify the correct Copilot-Integration-Id header is being sent.";
test("isModelScoped400 matches GitHub's two 'model not supported' phrasings", () => {
assert.equal(isModelScoped400(GITHUB_NOT_SUPPORTED_400), true);
assert.equal(isModelScoped400(GITHUB_INTEGRATOR_400), true);
});
test("github has per-model quota (locks the model, not the whole connection)", () => {
assert.equal(hasPerModelQuota("github"), true);
});
test("lockModelIfPerModelQuota locks a model-not-supported GitHub model for future requests", () => {
const connectionId = `github-${Date.now()}`;
const locked = lockModelIfPerModelQuota(
"github",
connectionId,
"gpt-5.4",
"model_capacity",
60 * 60 * 1000
);
assert.equal(locked, true);
assert.equal(isModelLocked("github", connectionId, "gpt-5.4"), true);
// A sibling model on the same connection must stay eligible.
assert.equal(isModelLocked("github", connectionId, "gpt-5.5"), false);
});