mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 11:52:26 +03:00
Behind the new `PROXY_SKIP_RECENTLY_FAILED` flag (default off), pool rotation and the opencode account rotation remember a proxy that just failed (refused probe or 429) and skip it for a doubling cooldown instead of re-serving it immediately.
Maintainer rework before merge (kept the idea, no default behavior change):
- The original was on by default and re-queried the DB on every request while a member was set aside; selection now caches a refusal sequence number and re-runs the cascade once per set-aside event.
- `src/lib/db` no longer imports the heavy dispatcher for key normalization (a parity test guarantees the same key as `proxyConfigToUrl()`); `.env.example` and `ENVIRONMENT.md` document the default as false.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
197 lines
7.5 KiB
TypeScript
197 lines
7.5 KiB
TypeScript
import { describe, it, before, after, beforeEach, afterEach, mock } from "node:test";
|
|
import assert from "node:assert";
|
|
import net from "node:net";
|
|
import { OpencodeExecutor } from "../../open-sse/executors/opencode.ts";
|
|
import type { ExecutorLog, ProviderCredentials } from "../../open-sse/executors/base.ts";
|
|
import { resolveProxyForRequest } from "../../open-sse/utils/proxyFetch.ts";
|
|
import * as memory from "../../open-sse/utils/proxyRefusalMemory.ts";
|
|
|
|
// With PROXY_SKIP_RECENTLY_FAILED on, a refusal received on a proxied opencode account sets
|
|
// that member aside across requests; a direct account is never concerned. With the flag off
|
|
// (the default) the rotation is exactly the plain one.
|
|
|
|
const log: ExecutorLog = { debug() {}, info() {}, warn() {}, error() {} };
|
|
const FINGERPRINTS = ["a".repeat(32), "b".repeat(32), "c".repeat(32)];
|
|
const servers: net.Server[] = [];
|
|
const ports: number[] = [];
|
|
|
|
function listen(server: net.Server): Promise<number> {
|
|
return new Promise((resolve) => {
|
|
server.listen(0, "127.0.0.1", () => resolve((server.address() as net.AddressInfo).port));
|
|
});
|
|
}
|
|
|
|
before(async () => {
|
|
for (let i = 0; i < 3; i++) {
|
|
const server = net.createServer((socket) => socket.destroy());
|
|
servers.push(server);
|
|
ports.push(await listen(server));
|
|
}
|
|
});
|
|
|
|
after(() => {
|
|
for (const server of servers) server.close();
|
|
});
|
|
|
|
function proxyFor(index: number) {
|
|
return { type: "http", host: "127.0.0.1", port: ports[index] };
|
|
}
|
|
|
|
function keyFor(index: number) {
|
|
return memory.proxyEgressKey(proxyFor(index));
|
|
}
|
|
|
|
function credentials(
|
|
accounts: Array<{ fp: string; proxyIndex: number | null }>
|
|
): ProviderCredentials {
|
|
return {
|
|
apiKey: null,
|
|
accessToken: null,
|
|
connectionId: "noauth",
|
|
providerSpecificData: {
|
|
fingerprints: accounts.map((a) => a.fp),
|
|
accountProxies: accounts.map((a) => ({
|
|
fingerprint: a.fp,
|
|
proxy: a.proxyIndex === null ? null : proxyFor(a.proxyIndex),
|
|
})),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("OpencodeExecutor proxy refusal memory", () => {
|
|
let originalFetch: typeof globalThis.fetch;
|
|
let observed: string[] = [];
|
|
let statuses: number[] = [];
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch;
|
|
memory.__resetProxyRefusalMemoryForTesting();
|
|
process.env.PROXY_SKIP_RECENTLY_FAILED = "true";
|
|
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
const resolved = resolveProxyForRequest(url);
|
|
observed.push(resolved.proxyUrl ? new URL(resolved.proxyUrl).port : "direct");
|
|
const status = statuses.shift() ?? 200;
|
|
return new Response(JSON.stringify({ ok: status === 200 }), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}) as typeof globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
mock.timers.reset();
|
|
delete process.env.PROXY_SKIP_RECENTLY_FAILED;
|
|
});
|
|
|
|
const proxied = () => credentials(FINGERPRINTS.map((fp, i) => ({ fp, proxyIndex: i })));
|
|
const port = (index: number) => String(ports[index]);
|
|
|
|
async function run(exec: OpencodeExecutor, creds: ProviderCredentials, plan: number[]) {
|
|
statuses = [...plan];
|
|
observed = [];
|
|
const result = await exec.execute({
|
|
model: "muse-spark-1.3-contributor-free",
|
|
body: { messages: [{ role: "user", content: "hi" }], stream: false },
|
|
stream: false,
|
|
signal: null,
|
|
credentials: creds,
|
|
log,
|
|
});
|
|
return { status: (result as { response: Response }).response.status, observed: [...observed] };
|
|
}
|
|
|
|
// Account cooldowns are a separate, shorter mechanism: clear them so each assertion shows
|
|
// the effect of the proxy memory alone.
|
|
function clearCooldowns(exec: OpencodeExecutor) {
|
|
const state = exec as unknown as { accounts: Array<{ cooldownUntil: number }> };
|
|
for (const account of state.accounts) account.cooldownUntil = 0;
|
|
}
|
|
|
|
it("a received refusal sets that member aside for later requests", async () => {
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
const first = await run(exec, proxied(), [429, 200]);
|
|
assert.deepStrictEqual(first.observed, [port(0), port(1)]);
|
|
assert.strictEqual(first.status, 200);
|
|
assert.strictEqual(memory.isProxyAvoided(keyFor(0)), true);
|
|
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(2)]);
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(1)]);
|
|
});
|
|
|
|
it("with the flag at its default (off) the refused proxy is tried again in turn", async () => {
|
|
delete process.env.PROXY_SKIP_RECENTLY_FAILED;
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
await run(exec, proxied(), [429, 200]);
|
|
assert.strictEqual(memory.__proxyRefusalMemorySizeForTesting(), 0);
|
|
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(2)]);
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(0)]);
|
|
});
|
|
|
|
it("once the period ends the proxy is tried again", async () => {
|
|
mock.timers.enable({ apis: ["Date"], now: 1_800_000_000_000 });
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
await run(exec, proxied(), [429, 200]);
|
|
|
|
mock.timers.tick(2 * 60_000 + 1);
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(2)]);
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(0)]);
|
|
});
|
|
|
|
it("with the flag off a member set aside earlier is not skipped", async () => {
|
|
memory.noteProxyRefusal(keyFor(0), "ip_quota_429");
|
|
delete process.env.PROXY_SKIP_RECENTLY_FAILED;
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
assert.deepStrictEqual((await run(exec, proxied(), [200])).observed, [port(0)]);
|
|
});
|
|
|
|
it("when every proxy is set aside one attempt still happens and its success clears it", async () => {
|
|
for (let i = 0; i < 3; i++) memory.noteProxyRefusal(keyFor(i), "ip_quota_429");
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
|
|
const result = await run(exec, proxied(), [200]);
|
|
assert.strictEqual(result.status, 200);
|
|
assert.deepStrictEqual(result.observed, [port(0)]);
|
|
assert.strictEqual(memory.isProxyAvoided(keyFor(0)), false);
|
|
assert.strictEqual(memory.isProxyAvoided(keyFor(1)), true);
|
|
});
|
|
|
|
it("a refusal on a proxyless account writes nothing, direct stays eligible", async () => {
|
|
const mixed = () =>
|
|
credentials([
|
|
{ fp: FINGERPRINTS[0], proxyIndex: null },
|
|
{ fp: FINGERPRINTS[1], proxyIndex: 1 },
|
|
]);
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
|
|
const first = await run(exec, mixed(), [429, 200]);
|
|
assert.deepStrictEqual(first.observed, ["direct", port(1)]);
|
|
assert.strictEqual(memory.__proxyRefusalMemorySizeForTesting(), 0);
|
|
|
|
clearCooldowns(exec);
|
|
assert.deepStrictEqual((await run(exec, mixed(), [200])).observed, ["direct"]);
|
|
});
|
|
|
|
it("a connection without configured accounts never touches the memory", async () => {
|
|
const exec = new OpencodeExecutor("opencode-zen");
|
|
const noAccounts: ProviderCredentials = {
|
|
apiKey: null,
|
|
accessToken: null,
|
|
connectionId: "noauth",
|
|
providerSpecificData: {},
|
|
};
|
|
// The fast path may retry a refusal internally: every planned answer is a refusal.
|
|
await run(exec, noAccounts, [429, 429, 429, 429, 429]);
|
|
assert.strictEqual(memory.__proxyRefusalMemorySizeForTesting(), 0);
|
|
});
|
|
});
|