mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 21:32:20 +03:00
* feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset
Mirror Claude Code's /low-priority and /limit-reset for OmniRoute-managed
Claude subscription accounts (wire contract captured from Claude Code 2.1.263).
Both are opt-in per connection (providerSpecificData.lowPriorityMode /
autoLimitReset, Edit connection -> Claude section, default off) and only act
on the 5-hour usage wall: a 429 carrying
anthropic-ratelimit-unified-status: rejected and, when eligible,
anthropic-ratelimit-unified-slow-offer: treatment. Nothing is sent before
that first wall 429.
- Lower-priority lane: on the wall the executor retries the SAME account
with `anthropic-usage-limit: slow` and keeps the header on every request
until anthropic-ratelimit-unified-reset (+60s). The intercepted 429 never
reaches chatCore, so the connection is not cooled down or rotated away.
slot_busy (429) / 529 wait slow-retry-after (20s default, 5-600s, +-30%
jitter) bounded by slow-max-wait (20min default, 1min-6h), then end +
10min cool-off. weekly_limit / budget_exhausted / off / ineligible, a
5h-window rollover, or ineligible + overage-in-use end the lane and let
the response flow to the normal cooldown path.
- Session-limit reset: GET /api/oauth/usage?at_wall=1&skip_spend=1 ->
juniper_tide block; when arm=reset and available, POST
/api/organizations/{org}/reset_rate_limits {program: "juniper_tide"} and
retry at full speed. already_used / not offered memoise next_available_at.
- State is in-memory per connection; the executor owns the abort-aware
sleep; the pure state machine and the HTTP client are separate modules
with unit tests; an executor-level test proves the header/retry wiring
end to end with a mocked upstream.
* fix(sse): make the Claude usage-wall handling race-safe for parallel requests
Two requests on the same Claude OAuth connection can hit the 5-hour wall in
the same instant.
- Lower-priority lane: the executor now tells the decider whether THIS
request carried `anthropic-usage-limit: slow`. A sibling built while the
lane was still idle (no header) whose 429 lands after the lane activated
is re-sent on the lane instead of being misread as a "wall" verdict that
would end it; its 2xx is not counted as lane telemetry either.
- Session-limit reset: concurrent wall hits share one in-flight status+claim
round trip (no duplicate POST reset_rate_limits), and for 60s after a
granted reset stale sibling walls are answered "reset" without touching
the network, so they retry at full speed instead of re-claiming or
falling into the slow lane.
Tests cover both races.
* fix(sse): address adversarial review of the Claude usage-wall handling
Three defects found by a 3-lens review of the two previous commits.
1. Lane wait could outlive the request (high). The slot_busy/529 sleep shares
the request's AbortSignal with chatCore's upstream-start timeout (10 min by
default), while the lane's own max-wait defaults to 20 min and can reach 6h
from the server header. A long slot_busy streak was therefore killed
mid-sleep with a TimeoutError instead of ending gracefully as max_wait with
its cool-off. The decision now takes a waitCeilingMs — what is left of the
executor's own timeout, minus a 5s margin — which caps the effective
max-wait and clamps each individual sleep.
2. A wall 429 surfacing only after a 400-driven intra-attempt retry was missed
(medium). The context-editing / thinking-budget / effort / auto-learn
fallbacks all re-fetch and REASSIGN `response`, and the wall check ran
before them, so such a 429 fell through to the generic path and cooled the
connection down. The check now runs after those retries, on the final
response of the attempt.
3. `ineligible` + `overage-in-use: true` ended the lane as plain `ineligible`
on a 429 (medium) because the status mapping ran first; only the non-429
tail produced `extra_usage`. Overage takeover now wins on every status.
Also bounds the module-level per-connection maps with the same FIFO policy as
the identity caches in claudeIdentity.ts: the state key falls back to the
access token when a connection id is absent, and OAuth tokens rotate on every
refresh, so the maps could grow for the process lifetime.
Tests cover all three fixes, including an executor-level regression for the
400-then-wall ordering.
* fix(i18n): add the Claude usage-wall toggle strings to pt-BR
`tests/unit/i18n-pt-br.test.ts` (#6695) requires pt-BR.json to carry every key
present in en.json; the four new `providers.claude{LowPriorityMode,AutoLimitReset}*`
keys were only added to en and it, so the gate failed on this branch.
* refactor(sse): keep the usage-wall change inside the frozen quality budgets
The three ratchets this PR tripped were all its own, not inherited:
- file-size (frozen, may only shrink): open-sse/executors/base.ts 1857 > 1751
and EditConnectionModal.tsx 1653 > 1631.
- complexity / cognitive-complexity (new-code mode): three functions over the
15 threshold — runClaudeLimitResetAttempt (27), handleClaudeUsageLimitResponse
(19 / cognitive 23) and observeClaudeLowPriorityResponse (17 / 17).
Extractions, all behavior-preserving:
- New open-sse/executors/claudeUsageLimit.ts owns the executor-side glue (header
injection, wait accounting, abort-aware sleep, timeout-derived wait ceiling and
the decision logging) behind a ClaudeUsageLimitGuard, so base.ts keeps a
three-line call site instead of ~100 lines of mechanics.
- Three long-standing Claude blocks leave base.ts for the modules they belong to:
mergeCcHeaders + applyStainlessHeaders into config/anthropicHeaders.ts and
stripClaudeSystemPrefixBlocks into executors/claudeIdentity.ts. base.ts is back
at its frozen 1750 lines.
- The modal's Claude section becomes ClaudeConnectionFields.tsx (mirroring
CcCompatibleRequestDefaultsFields) plus a claudeConnectionFields.ts helper that
de-duplicates the field defaults across the modal's two init sites; the file
drops to 1622, below its frozen 1631.
- The three over-threshold functions are split into focused helpers
(observeErrorResponse / observeSuccessResponse, shouldClaimLimitReset,
resolveLimitResetOffer / runLimitResetClaim / memoiseNotBefore).
Gates now: file-size OK, complexity 0 new violations, cognitive 0 new,
fetch-targets / error-helper / build-scope / deps OK, typecheck clean, ESLint 0,
Prettier clean, 155 unit tests green across the feature and its neighbours.
Still failing and NOT this branch's: pack-policy (unexpected
@omniroute/opencode-plugin-v2 files in the npm artifact) and
mutation-test-coverage (stryker tap.testFiles missing entries for
circuitBreaker.ts and comboStructure.ts) — both reproduce on the untouched base.
---------
Co-authored-by: davidebaraldo <davidebaraldo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
245 lines
9.4 KiB
TypeScript
245 lines
9.4 KiB
TypeScript
/**
|
||
* End-to-end wiring of the Claude OAuth lower-priority lane and the session-limit reset
|
||
* through `BaseExecutor.execute()` (mirrors the fetch-capture pattern of
|
||
* context-editing-executor-injection.test.ts).
|
||
*
|
||
* Proves, on the real outbound request:
|
||
* - no `anthropic-usage-limit` header before the account hits its 5-hour wall;
|
||
* - on the wall 429 (slow-offer: treatment) with `lowPriorityMode` on, the executor
|
||
* retries the SAME account with `anthropic-usage-limit: slow` and returns the 200 —
|
||
* chatCore never sees the 429, so the connection is not cooled down;
|
||
* - later requests on that connection carry the header from the first attempt;
|
||
* - an opt-out connection surfaces the 429 untouched, header never sent;
|
||
* - with `autoLimitReset` on, the wall triggers status + claim and retries at full speed.
|
||
*/
|
||
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
|
||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||
import { _resetClaudeLowPriorityState } from "../../open-sse/services/claudeLowPriority.ts";
|
||
import {
|
||
CLAUDE_LIMIT_RESET_STATUS_URL,
|
||
_resetClaudeLimitResetMemo,
|
||
} from "../../open-sse/services/claudeLimitReset.ts";
|
||
|
||
type Captured = { url: string; headers: Record<string, string> };
|
||
|
||
const NOW_S = Math.floor(Date.now() / 1000);
|
||
|
||
function wall429(extra: Record<string, string> = {}): Response {
|
||
return new Response(
|
||
JSON.stringify({ type: "error", error: { type: "rate_limit_error", message: "usage limit" } }),
|
||
{
|
||
status: 429,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"anthropic-ratelimit-unified-status": "rejected",
|
||
"anthropic-ratelimit-unified-reset": String(NOW_S + 3600),
|
||
"anthropic-ratelimit-unified-representative-claim": "five_hour",
|
||
"anthropic-ratelimit-unified-slow-offer": "treatment",
|
||
"anthropic-ratelimit-unified-slow-retry-after": "20",
|
||
"anthropic-ratelimit-unified-slow-max-wait": "1200",
|
||
...extra,
|
||
},
|
||
}
|
||
);
|
||
}
|
||
|
||
function ok(headers: Record<string, string> = {}): Response {
|
||
return new Response(JSON.stringify({ id: "msg_1", type: "message", content: [] }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json", ...headers },
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Sequenced fetch mock: `/v1/messages` POSTs consume `messages` in order; any other URL
|
||
* (identity bootstrap, usage/status, reset claim) is answered by `others` or an empty 200.
|
||
*/
|
||
function mockFetch(
|
||
messages: Array<() => Response>,
|
||
others: Record<string, () => Response> = {}
|
||
): { calls: Captured[]; restore: () => void } {
|
||
const calls: Captured[] = [];
|
||
const original = globalThis.fetch;
|
||
let i = 0;
|
||
globalThis.fetch = (async (input: unknown, init: RequestInit = {}) => {
|
||
const url = String(input);
|
||
const headers = { ...((init.headers as Record<string, string>) ?? {}) };
|
||
if (url.includes("/v1/messages")) {
|
||
calls.push({ url, headers });
|
||
const next = messages[Math.min(i, messages.length - 1)];
|
||
i++;
|
||
return next();
|
||
}
|
||
const route = Object.entries(others).find(([prefix]) => url.startsWith(prefix));
|
||
if (route) return route[1]();
|
||
return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
|
||
}) as typeof fetch;
|
||
return { calls, restore: () => void (globalThis.fetch = original) };
|
||
}
|
||
|
||
function lower(headers: Record<string, string>): Record<string, string> {
|
||
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
||
}
|
||
|
||
function run(
|
||
connectionId: string,
|
||
providerSpecificData: Record<string, unknown>,
|
||
contextEditingEnabled = false
|
||
) {
|
||
return new DefaultExecutor("claude").execute({
|
||
model: "claude-opus-4-8",
|
||
body: { model: "claude-opus-4-8", messages: [{ role: "user", content: "hi" }], max_tokens: 8 },
|
||
stream: false,
|
||
credentials: {
|
||
connectionId,
|
||
accessToken: `sk-ant-oat-${connectionId}`,
|
||
providerSpecificData,
|
||
},
|
||
clientHeaders: { "user-agent": "Cursor/1.0" },
|
||
contextEditing: { enabled: contextEditingEnabled },
|
||
// Combo-style: the generic 2×2s intra-URL 429 retry is skipped so the test only
|
||
// exercises the lane's own retry (which runs regardless of this flag).
|
||
skipUpstreamRetry: true,
|
||
});
|
||
}
|
||
|
||
test.beforeEach(() => {
|
||
_resetClaudeLowPriorityState();
|
||
_resetClaudeLimitResetMemo();
|
||
});
|
||
|
||
test("wall 429 + lowPriorityMode → same-account retry with anthropic-usage-limit: slow, 200 returned", async () => {
|
||
const { calls, restore } = mockFetch([
|
||
() => wall429(),
|
||
() => ok({ "anthropic-ratelimit-unified-slow-status": "active" }),
|
||
]);
|
||
try {
|
||
const result = await run("conn-lowpri", { lowPriorityMode: true });
|
||
assert.equal(result.response.status, 200, "the intercepted 429 never reaches chatCore");
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(
|
||
lower(calls[0].headers)["anthropic-usage-limit"],
|
||
undefined,
|
||
"not sent before the wall"
|
||
);
|
||
assert.equal(lower(calls[1].headers)["anthropic-usage-limit"], "slow", "sent on the retry");
|
||
assert.equal(lower(calls[1].headers)["anthropic-dispatch-id"], undefined);
|
||
|
||
// Next request on the same connection rides the lane from its first attempt.
|
||
const again = await run("conn-lowpri", { lowPriorityMode: true });
|
||
assert.equal(again.response.status, 200);
|
||
assert.equal(calls.length, 3);
|
||
assert.equal(lower(calls[2].headers)["anthropic-usage-limit"], "slow");
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("wall 429 without the opt-in → 429 surfaced untouched, header never sent", async () => {
|
||
const { calls, restore } = mockFetch([() => wall429(), () => ok()]);
|
||
try {
|
||
const result = await run("conn-optout", {});
|
||
assert.equal(result.response.status, 429);
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(lower(calls[0].headers)["anthropic-usage-limit"], undefined);
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("control arm offer is not accepted even with the opt-in", async () => {
|
||
const { calls, restore } = mockFetch([
|
||
() => wall429({ "anthropic-ratelimit-unified-slow-offer": "control" }),
|
||
() => ok(),
|
||
]);
|
||
try {
|
||
const result = await run("conn-control", { lowPriorityMode: true });
|
||
assert.equal(result.response.status, 429);
|
||
assert.equal(calls.length, 1);
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("lane state is per connection: another account still sees its own wall", async () => {
|
||
const { calls, restore } = mockFetch([() => wall429(), () => ok(), () => wall429(), () => ok()]);
|
||
try {
|
||
await run("conn-a", { lowPriorityMode: true });
|
||
assert.equal(calls.length, 2);
|
||
// conn-b: first attempt has no header (idle), hits the wall, accepts, retries.
|
||
const b = await run("conn-b", { lowPriorityMode: true });
|
||
assert.equal(b.response.status, 200);
|
||
assert.equal(lower(calls[2].headers)["anthropic-usage-limit"], undefined);
|
||
assert.equal(lower(calls[3].headers)["anthropic-usage-limit"], "slow");
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("a wall 429 that only surfaces after a 400-driven intra-attempt retry is still intercepted", async () => {
|
||
// The context-editing 400 fallback re-fetches the same URL and REPLACES `response`.
|
||
// The usage-wall check must classify that final response, otherwise the offer is missed
|
||
// and the 429 reaches chatCore, cooling the connection down — the exact opposite of
|
||
// what lowPriorityMode is for.
|
||
const badRequest = () =>
|
||
new Response(JSON.stringify({ error: { message: "context_management not supported" } }), {
|
||
status: 400,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
const { calls, restore } = mockFetch([
|
||
badRequest, // 1st: 400 → context-editing fallback re-fetches
|
||
() => wall429(), // 2nd: the wall shows up only here
|
||
() => ok({ "anthropic-ratelimit-unified-slow-status": "active" }), // 3rd: lane retry
|
||
]);
|
||
try {
|
||
const result = await run("conn-400-then-wall", { lowPriorityMode: true }, true);
|
||
assert.equal(result.response.status, 200, "the late wall 429 was intercepted, not surfaced");
|
||
assert.equal(calls.length, 3);
|
||
assert.equal(lower(calls[1].headers)["anthropic-usage-limit"], undefined);
|
||
assert.equal(lower(calls[2].headers)["anthropic-usage-limit"], "slow");
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|
||
|
||
test("wall 429 + autoLimitReset → status + claim, then full-speed retry without the slow header", async () => {
|
||
const claimUrl = "https://api.anthropic.com/api/organizations/org-uuid-1/reset_rate_limits";
|
||
const hits: string[] = [];
|
||
const { calls, restore } = mockFetch([() => wall429(), () => ok()], {
|
||
[CLAUDE_LIMIT_RESET_STATUS_URL]: () => {
|
||
hits.push("status");
|
||
return new Response(
|
||
JSON.stringify({
|
||
juniper_tide: { eligible: true, in_experiment: true, arm: "reset", available: true },
|
||
}),
|
||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||
);
|
||
},
|
||
[claimUrl]: () => {
|
||
hits.push("claim");
|
||
return new Response(JSON.stringify({ result: "reset", next_available_at: null }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
},
|
||
});
|
||
try {
|
||
const result = await run("conn-reset", {
|
||
autoLimitReset: true,
|
||
organizationUUID: "org-uuid-1",
|
||
});
|
||
assert.equal(result.response.status, 200);
|
||
assert.deepEqual(hits, ["status", "claim"]);
|
||
assert.equal(calls.length, 2);
|
||
assert.equal(
|
||
lower(calls[1].headers)["anthropic-usage-limit"],
|
||
undefined,
|
||
"full speed, no slow lane"
|
||
);
|
||
} finally {
|
||
restore();
|
||
}
|
||
});
|