Files
OmniRoute/tests/unit/account-rotation.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

216 lines
9.1 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert";
import {
isAccountReady,
pickAccount,
markCooldown,
markSuccess,
maskAccountId,
isNetworkErrorRotatable,
isEmptyUpstreamRejection,
extractChatcmplId,
type RotatableAccount,
} from "../../open-sse/executors/accountRotation.ts";
function account(overrides: Partial<RotatableAccount> = {}): RotatableAccount {
return {
fingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
cooldownUntil: 0,
consecutiveFails: 0,
proxy: null,
...overrides,
};
}
describe("accountRotation", () => {
it("isAccountReady is true when cooldownUntil is in the past", () => {
assert.strictEqual(isAccountReady(account({ cooldownUntil: Date.now() - 1000 })), true);
});
it("isAccountReady is false when cooldownUntil is in the future", () => {
assert.strictEqual(isAccountReady(account({ cooldownUntil: Date.now() + 60_000 })), false);
});
it("markCooldown increments consecutiveFails and sets a future cooldownUntil", () => {
const acct = account();
markCooldown(acct);
assert.strictEqual(acct.consecutiveFails, 1);
assert.ok(acct.cooldownUntil > Date.now());
});
it("markCooldown backs off exponentially with consecutive failures", () => {
const acct = account();
markCooldown(acct);
const firstCooldown = acct.cooldownUntil;
markCooldown(acct);
assert.strictEqual(acct.consecutiveFails, 2);
// Second backoff (base*2^1) must be strictly larger than the first
// (base*2^0), modulo the shared jitter window — compare the floor.
assert.ok(acct.cooldownUntil - Date.now() > firstCooldown - Date.now() - 1000);
});
it("markCooldown uses the same magnitude regardless of why it was called (429 or network throw)", () => {
// No `short`/severity parameter: proxy-attributable failures (429, dead
// proxy) and shared-egress network throws use the identical formula —
// the repo's own established "transient, not clearly attributable"
// cooldown (errorConfig.ts TRANSIENT_COOLDOWN_MS/transientMax) already
// covers both cases at the same magnitude. The behavioral fix for
// shared-egress accounts lives in the caller's skip logic, not here.
const a = account();
const b = account();
markCooldown(a);
markCooldown(b);
// Both draw from the same base backoff ± up to 1s jitter — same formula,
// no separate "short" magnitude for either call site.
assert.ok(
Math.abs(a.cooldownUntil - b.cooldownUntil) <= 1000,
"same account state must produce cooldowns within the shared jitter window"
);
});
it("markSuccess resets consecutiveFails to 0", () => {
const acct = account({ consecutiveFails: 5 });
markSuccess(acct);
assert.strictEqual(acct.consecutiveFails, 0);
});
it("maskAccountId masks a real fingerprint to its first 8 chars + ellipsis", () => {
assert.strictEqual(maskAccountId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), "aaaaaaaa…");
});
it("maskAccountId reports the empty/default fingerprint as 'direct'", () => {
assert.strictEqual(maskAccountId(""), "direct");
});
it("pickAccount skips accounts in cooldown and rotates nextAccountIdx", () => {
const a = account({ fingerprint: "a", cooldownUntil: Date.now() + 60_000 });
const b = account({ fingerprint: "b", cooldownUntil: 0 });
const state = { nextAccountIdx: 0 };
const picked = pickAccount([a, b], state);
assert.strictEqual(picked.fingerprint, "b", "must skip the account still in cooldown");
});
it("pickAccount falls back to the next index when every account is in cooldown", () => {
const a = account({ fingerprint: "a", cooldownUntil: Date.now() + 60_000 });
const b = account({ fingerprint: "b", cooldownUntil: Date.now() + 60_000 });
const state = { nextAccountIdx: 0 };
const picked = pickAccount([a, b], state);
assert.strictEqual(picked.fingerprint, "a", "must still return an account, not throw/hang");
});
it("pickAccount accepts a custom isReady predicate (e.g. JWT-freshness-aware)", () => {
const a = account({ fingerprint: "a", cooldownUntil: 0 });
const b = account({ fingerprint: "b", cooldownUntil: 0 });
const state = { nextAccountIdx: 0 };
// Custom predicate rejects "a" for a reason cooldown alone wouldn't catch.
const picked = pickAccount([a, b], state, (acct: RotatableAccount) => acct.fingerprint !== "a");
assert.strictEqual(picked.fingerprint, "b");
});
it("isNetworkErrorRotatable is true only when the account has a configured proxy", () => {
const withProxy = account({
proxy: { type: "http", host: "127.0.0.1", port: 8080 },
});
const withoutProxy = account({ proxy: null });
assert.strictEqual(isNetworkErrorRotatable(withProxy), true);
assert.strictEqual(isNetworkErrorRotatable(withoutProxy), false);
});
});
describe("isEmptyUpstreamRejection", () => {
it("matches the observed malformed completion envelope (no error field, empty content, null finish_reason)", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(isEmptyUpstreamRejection(400, observed), true);
});
it("does not match a non-400 status", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(isEmptyUpstreamRejection(200, observed), false);
assert.strictEqual(isEmptyUpstreamRejection(429, observed), false);
assert.strictEqual(isEmptyUpstreamRejection(502, observed), false);
});
it("does not match when an error field is present", () => {
const withError = JSON.stringify({
error: { message: "bad request", type: "invalid_request_error" },
});
assert.strictEqual(isEmptyUpstreamRejection(400, withError), false);
const emptyError = JSON.stringify({ error: {} });
assert.strictEqual(isEmptyUpstreamRejection(400, emptyError), false);
});
it("does not match when content is non-empty or tool_calls present", () => {
const nonEmpty = JSON.stringify({
choices: [{ message: { role: "assistant", content: "hi" }, finish_reason: "stop" }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, nonEmpty), false);
const toolCalls = JSON.stringify({
choices: [
{ message: { role: "assistant", tool_calls: [{ id: "x" }] }, finish_reason: "tool_calls" },
],
});
assert.strictEqual(isEmptyUpstreamRejection(400, toolCalls), false);
});
it("does not match when content is a non-string non-null value (number, block array)", () => {
const numericContent = JSON.stringify({
choices: [{ message: { role: "assistant", content: 123 }, finish_reason: null }],
});
assert.strictEqual(
isEmptyUpstreamRejection(400, numericContent),
false,
"non-string non-null content is not eligible"
);
const reasoningContent = JSON.stringify({
choices: [
{ message: { role: "assistant", reasoning_content: "thinking" }, finish_reason: null },
],
});
assert.strictEqual(isEmptyUpstreamRejection(400, reasoningContent), false);
});
it("does not match when choices or message are absent", () => {
const noChoices = JSON.stringify({ id: "chatcmpl_x", model: "muse" });
assert.strictEqual(isEmptyUpstreamRejection(400, noChoices), false);
const noMessage = JSON.stringify({ choices: [{ finish_reason: null }] });
assert.strictEqual(isEmptyUpstreamRejection(400, noMessage), false);
});
it("does not match when finish_reason is a literal value (not null)", () => {
const stopReason = JSON.stringify({
choices: [{ message: { role: "assistant" }, finish_reason: "stop" }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, stopReason), false);
});
it("matches an empty string content (treated as eligible)", () => {
const emptyContent = JSON.stringify({
choices: [{ message: { role: "assistant", content: "" }, finish_reason: null }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, emptyContent), true);
});
it("returns false for unparseable JSON rather than throwing", () => {
assert.strictEqual(isEmptyUpstreamRejection(400, "not json"), false);
assert.strictEqual(isEmptyUpstreamRejection(400, ""), false);
});
});
describe("extractChatcmplId", () => {
it("extracts the chatcmpl id from an observed envelope", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(extractChatcmplId(observed), "chatcmpl_44fn2g6e7kk");
});
it("falls back to 'unknown' when no id is present", () => {
assert.strictEqual(extractChatcmplId("{choices:[]}"), "unknown");
assert.strictEqual(extractChatcmplId(""), "unknown");
assert.strictEqual(extractChatcmplId("not json"), "unknown");
});
});