claude-code-infinite 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -29
- package/dist/auto-update.d.ts +2 -0
- package/dist/auto-update.d.ts.map +1 -1
- package/dist/auto-update.js +4 -0
- package/dist/auto-update.js.map +1 -1
- package/dist/claude-auth.d.ts +27 -2
- package/dist/claude-auth.d.ts.map +1 -1
- package/dist/claude-auth.js +76 -4
- package/dist/claude-auth.js.map +1 -1
- package/dist/claude-install.d.ts +73 -0
- package/dist/claude-install.d.ts.map +1 -0
- package/dist/claude-install.js +142 -0
- package/dist/claude-install.js.map +1 -0
- package/dist/claude-plan-warning.d.ts +70 -7
- package/dist/claude-plan-warning.d.ts.map +1 -1
- package/dist/claude-plan-warning.js +152 -8
- package/dist/claude-plan-warning.js.map +1 -1
- package/dist/cli-args.d.ts +5 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +9 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-main.js +222 -84
- package/dist/cli-main.js.map +1 -1
- package/dist/diagnostics-schema.d.ts +4 -0
- package/dist/diagnostics-schema.d.ts.map +1 -1
- package/dist/diagnostics-schema.js +20 -0
- package/dist/diagnostics-schema.js.map +1 -1
- package/dist/diagnostics.d.ts +24 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +68 -4
- package/dist/diagnostics.js.map +1 -1
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +57 -25
- package/dist/hooks.js.map +1 -1
- package/dist/install-ping.d.ts +54 -0
- package/dist/install-ping.d.ts.map +1 -0
- package/dist/install-ping.js +177 -0
- package/dist/install-ping.js.map +1 -0
- package/dist/key-page.d.ts +17 -0
- package/dist/key-page.d.ts.map +1 -0
- package/dist/key-page.js +32 -0
- package/dist/key-page.js.map +1 -0
- package/dist/key-status.d.ts +79 -0
- package/dist/key-status.d.ts.map +1 -0
- package/dist/key-status.js +143 -0
- package/dist/key-status.js.map +1 -0
- package/dist/memtree-finder.d.ts +2 -1
- package/dist/memtree-finder.d.ts.map +1 -1
- package/dist/memtree-finder.js +4 -5
- package/dist/memtree-finder.js.map +1 -1
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +17 -7
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree-times.d.ts +35 -0
- package/dist/memtree-times.d.ts.map +1 -0
- package/dist/memtree-times.js +166 -0
- package/dist/memtree-times.js.map +1 -0
- package/dist/memtree-tools.d.ts +12 -0
- package/dist/memtree-tools.d.ts.map +1 -1
- package/dist/memtree-tools.js +63 -7
- package/dist/memtree-tools.js.map +1 -1
- package/dist/memtree.d.ts +23 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +39 -3
- package/dist/memtree.js.map +1 -1
- package/dist/notices.d.ts +5 -0
- package/dist/notices.d.ts.map +1 -1
- package/dist/notices.js +13 -0
- package/dist/notices.js.map +1 -1
- package/dist/paused-session.d.ts +29 -0
- package/dist/paused-session.d.ts.map +1 -0
- package/dist/paused-session.js +148 -0
- package/dist/paused-session.js.map +1 -0
- package/dist/payment-gate.d.ts +44 -13
- package/dist/payment-gate.d.ts.map +1 -1
- package/dist/payment-gate.js +82 -23
- package/dist/payment-gate.js.map +1 -1
- package/dist/payment-link.d.ts +68 -0
- package/dist/payment-link.d.ts.map +1 -0
- package/dist/payment-link.js +123 -0
- package/dist/payment-link.js.map +1 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +4 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/project-trees.d.ts +100 -0
- package/dist/project-trees.d.ts.map +1 -0
- package/dist/project-trees.js +196 -0
- package/dist/project-trees.js.map +1 -0
- package/dist/proxy.d.ts +29 -8
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +167 -25
- package/dist/proxy.js.map +1 -1
- package/dist/rate-limit.d.ts +64 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +126 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/reqlog.d.ts +13 -2
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/package.json +2 -1
package/dist/proxy.js
CHANGED
|
@@ -56,12 +56,14 @@ import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSE
|
|
|
56
56
|
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
57
57
|
import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
|
|
58
58
|
import { hyperlink } from "./payment-gate.js";
|
|
59
|
+
import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
|
|
59
60
|
import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
|
|
60
61
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
61
62
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
62
63
|
import { isResumableSessionId } from "./resume-hint.js";
|
|
63
64
|
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
64
65
|
import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
|
|
66
|
+
import { rateLimitFromResponse } from "./rate-limit.js";
|
|
65
67
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
66
68
|
const DEFAULT_UPSTREAM = "https://api.anthropic.com";
|
|
67
69
|
const HOOK_BODY_LIMIT = 64 * 1024;
|
|
@@ -93,23 +95,39 @@ const SKIP_RESPONSE_HEADERS = new Set([
|
|
|
93
95
|
"transfer-encoding",
|
|
94
96
|
]);
|
|
95
97
|
/**
|
|
96
|
-
* Budget fallback until the server reports `model_budget_tokens
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
98
|
+
* Budget fallback until the server reports `model_budget_tokens`, mirroring the
|
|
99
|
+
* server's ccc policy (polychat index_budget.py): every Claude model compresses
|
|
100
|
+
* near its window, NEAR_WINDOW_FALLBACK_RATIO of it but no later than Claude
|
|
101
|
+
* Code's own auto-compact point (window - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS)
|
|
102
|
+
* and never below FALLBACK_BUDGET_WINDOW_RATIO: 900k of 1M, 167k of 200k; at
|
|
103
|
+
* most a Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
|
|
104
|
+
* FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts a 1M
|
|
105
|
+
* window at 967k, so MemTree compresses no earlier than ~70k before it would;
|
|
106
|
+
* ccc lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
|
|
102
107
|
* jumps past the budget reaches the proxy and is compressed here.
|
|
103
108
|
*/
|
|
104
109
|
export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
|
|
105
110
|
export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
|
|
106
|
-
/**
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
/** Claude Code 2.1.302 auto-compacts at window - min(max_tokens, 20k) - 13k. */
|
|
112
|
+
const CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS = 33_000;
|
|
113
|
+
/** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
|
|
114
|
+
const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
|
|
115
|
+
/**
|
|
116
|
+
* polychat USABLE_CONTEXT_CEILING_TOKENS_BY_MODEL: the Sonnets degrade well
|
|
117
|
+
* before their window. Sonnet 5 / 5.5 550k; every other Sonnet 400k. First match wins.
|
|
118
|
+
*/
|
|
119
|
+
const SONNET_CEILINGS = [
|
|
120
|
+
[/sonnet-5(?![0-9])/i, 550_000],
|
|
121
|
+
[/sonnet/i, 400_000],
|
|
122
|
+
];
|
|
123
|
+
/** The fallback whole-request budget for `model` on this window (see above). */
|
|
124
|
+
export function fallbackBudgetTokens(model, modelContextLimit) {
|
|
125
|
+
if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
|
|
126
|
+
return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
|
|
127
|
+
}
|
|
128
|
+
const nearWindow = Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), Math.min(Math.floor(modelContextLimit * NEAR_WINDOW_FALLBACK_RATIO), modelContextLimit - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS));
|
|
129
|
+
const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
|
|
130
|
+
return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
|
|
113
131
|
}
|
|
114
132
|
/**
|
|
115
133
|
* An explicit target is capped at this share of the budget: a compaction to
|
|
@@ -304,11 +322,16 @@ export function startProxy(opts) {
|
|
|
304
322
|
const shutdownAbort = new AbortController();
|
|
305
323
|
const state = {
|
|
306
324
|
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
325
|
+
keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
326
|
+
keyInvalid: () => opts.memtree.keyInvalid,
|
|
327
|
+
keyPageUrl: opts.memtree.keyPageUrl,
|
|
307
328
|
pausedNearWindowWarned: new Set(),
|
|
308
329
|
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
309
330
|
pausedDetail: () => opts.memtree.pausedDetail,
|
|
310
331
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
311
332
|
sessionStartNotice: opts.sessionStartNotice,
|
|
333
|
+
promptSubmitNotice: opts.promptSubmitNotice,
|
|
334
|
+
projectTrees: opts.projectTrees,
|
|
312
335
|
diagnostics: opts.diagnostics,
|
|
313
336
|
notices: new NoticeDeliveryQueue(),
|
|
314
337
|
mainNoticePending: false,
|
|
@@ -726,17 +749,20 @@ onMainStop) {
|
|
|
726
749
|
recordSessionStart(state, parsed.source);
|
|
727
750
|
noteMainSessionStart(state, parsed.source, parsed.session_id);
|
|
728
751
|
}
|
|
752
|
+
// Earlier trees in this project: asked now, so its wait overlaps the resume's.
|
|
753
|
+
const contextPromise = projectTreesContext(state.projectTrees, parsed);
|
|
729
754
|
// A resume waits briefly for the newest tree; a fresh session has none to wait for.
|
|
730
755
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
731
756
|
await state.newestTrees?.settle(parsed.session_id);
|
|
732
757
|
}
|
|
733
758
|
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
|
|
734
|
-
|
|
759
|
+
const context = await contextPromise;
|
|
760
|
+
if (!line && !context) {
|
|
735
761
|
res.writeHead(204);
|
|
736
762
|
res.end();
|
|
737
763
|
return;
|
|
738
764
|
}
|
|
739
|
-
const body = Buffer.from(JSON.stringify(
|
|
765
|
+
const body = Buffer.from(JSON.stringify(sessionStartOutput(line, context)), "utf-8");
|
|
740
766
|
res.writeHead(200, {
|
|
741
767
|
"content-type": "application/json",
|
|
742
768
|
"content-length": String(body.length),
|
|
@@ -782,6 +808,17 @@ onMainStop) {
|
|
|
782
808
|
state.promptAccounting.add(parsed.session_id, parsed.prompt_id, parsed.prompt);
|
|
783
809
|
recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
|
|
784
810
|
}
|
|
811
|
+
const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
|
|
812
|
+
if (line) {
|
|
813
|
+
const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
|
|
814
|
+
res.writeHead(200, {
|
|
815
|
+
"content-type": "application/json",
|
|
816
|
+
"content-length": String(body.length),
|
|
817
|
+
"cache-control": "no-store",
|
|
818
|
+
});
|
|
819
|
+
res.end(body);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
785
822
|
res.writeHead(204);
|
|
786
823
|
res.end();
|
|
787
824
|
return;
|
|
@@ -1697,6 +1734,13 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1697
1734
|
// it pending for the next turn.
|
|
1698
1735
|
if (result && sessionId !== undefined)
|
|
1699
1736
|
state.compactNow.delete(sessionId);
|
|
1737
|
+
// A first 402 starts fetching the short checkout link (memtree.ts): give the
|
|
1738
|
+
// paused notice below a moment to show it instead of the long one. Only a turn
|
|
1739
|
+
// that can show the notice waits, and only while that fetch is in flight.
|
|
1740
|
+
if (!result && displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration &&
|
|
1741
|
+
state.paymentNotice.due() && opts.memtree.paymentRequiredDetail !== null) {
|
|
1742
|
+
await opts.memtree.paymentLinkSettled?.(PAYMENT_LINK_NOTICE_WAIT_MS);
|
|
1743
|
+
}
|
|
1700
1744
|
if (downstreamClosedDuringCompression ||
|
|
1701
1745
|
(res.destroyed && !res.writableFinished)) {
|
|
1702
1746
|
releaseUncommittedRouteDecision();
|
|
@@ -1724,7 +1768,15 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1724
1768
|
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1725
1769
|
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1726
1770
|
const overBudget = reachedBudget(rec.compaction);
|
|
1727
|
-
if (
|
|
1771
|
+
if (state.keyInvalid()) {
|
|
1772
|
+
// A refused key (401/403) says why MemTree is off and how to fix it;
|
|
1773
|
+
// throttled like the payment notice, and quiet in between.
|
|
1774
|
+
if (mayQueueNotice && state.keyNotice.due()) {
|
|
1775
|
+
// A refusal of this very request says the same and withdraws it.
|
|
1776
|
+
pausedNotices.set(rec, state.notices.queueSuffix(keyInvalidNoticeText(state.keyPageUrl), () => state.keyNotice.markShown(), noticePromptId));
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
else if (mayQueueNotice && paymentDetail !== null &&
|
|
1728
1780
|
(state.paymentNotice.due() || (overBudget && !nearWindowWarned(state, sessionId)))) {
|
|
1729
1781
|
// A paused refusal of this very request withdraws it (refuseWholeRequest),
|
|
1730
1782
|
// and any later one withdraws its near-window line.
|
|
@@ -1739,7 +1791,24 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1739
1791
|
state.nearWindowNotice = handle;
|
|
1740
1792
|
}
|
|
1741
1793
|
else if (mayQueueNotice && paymentDetail === null) {
|
|
1742
|
-
|
|
1794
|
+
// Under the budget the whole request is what compression would have
|
|
1795
|
+
// sent: the turn is not degraded, so the failure stays in the logs.
|
|
1796
|
+
const warn = () => {
|
|
1797
|
+
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
1798
|
+
rec.degradedNotice = "shown";
|
|
1799
|
+
};
|
|
1800
|
+
if (compressionNeeded(rec.compaction)) {
|
|
1801
|
+
warn();
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
// Delivery sizes the request again (an exact count, else a
|
|
1805
|
+
// conservative bound): a turn it finds over the budget warns then.
|
|
1806
|
+
rec.degradedNotice = "quiet-under-budget";
|
|
1807
|
+
quietDegrades.set(rec, () => {
|
|
1808
|
+
if (state.mainPromptGeneration === noticePromptGeneration)
|
|
1809
|
+
warn();
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1743
1812
|
}
|
|
1744
1813
|
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1745
1814
|
noteWholeSize();
|
|
@@ -1973,6 +2042,15 @@ function resumePaymentNotice(state, input) {
|
|
|
1973
2042
|
state.diagnostics?.record("paused_notice", { trigger: "resume" });
|
|
1974
2043
|
return state.notices.warningText(paymentNoticeText(detail));
|
|
1975
2044
|
}
|
|
2045
|
+
/** ProxyOptions.promptSubmitNotice's line for this prompt; never throws. */
|
|
2046
|
+
function promptSubmitLine(state) {
|
|
2047
|
+
try {
|
|
2048
|
+
return state.promptSubmitNotice?.();
|
|
2049
|
+
}
|
|
2050
|
+
catch {
|
|
2051
|
+
return undefined;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1976
2054
|
/** The caller's extra SessionStart line; a throwing provider shows nothing. */
|
|
1977
2055
|
function extraSessionStartLine(provider, input) {
|
|
1978
2056
|
if (!provider || input.agent_id !== undefined || input.source === "compact")
|
|
@@ -1984,11 +2062,39 @@ function extraSessionStartLine(provider, input) {
|
|
|
1984
2062
|
return undefined;
|
|
1985
2063
|
}
|
|
1986
2064
|
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Earlier sessions in this project (project-trees.ts) for a main session's
|
|
2067
|
+
* start: fresh, resumed, cleared or forked, never a compaction. Never throws.
|
|
2068
|
+
*/
|
|
2069
|
+
async function projectTreesContext(hint, input) {
|
|
2070
|
+
if (!hint || input.agent_id !== undefined || input.source === "compact")
|
|
2071
|
+
return undefined;
|
|
2072
|
+
try {
|
|
2073
|
+
return await hint.contextFor(input.session_id);
|
|
2074
|
+
}
|
|
2075
|
+
catch {
|
|
2076
|
+
return undefined;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* The SessionStart hook's answer: the banner line for the user
|
|
2081
|
+
* (`systemMessage`), model context (`additionalContext`), or both.
|
|
2082
|
+
*/
|
|
2083
|
+
export function sessionStartOutput(line, context) {
|
|
2084
|
+
return {
|
|
2085
|
+
...(line ? { systemMessage: line } : {}),
|
|
2086
|
+
...(context
|
|
2087
|
+
? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } }
|
|
2088
|
+
: {}),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
1987
2091
|
function joinNoticeLines(...parts) {
|
|
1988
2092
|
const present = parts.filter((part) => !!part);
|
|
1989
2093
|
return present.length ? present.join("\n") : undefined;
|
|
1990
2094
|
}
|
|
1991
2095
|
export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
|
|
2096
|
+
/** How long a paused turn waits for the short checkout link before its notice (once). */
|
|
2097
|
+
export const PAYMENT_LINK_NOTICE_WAIT_MS = 1_000;
|
|
1992
2098
|
/** The reply to a ccc slash command, or undefined for an ordinary prompt. */
|
|
1993
2099
|
function sessionCommandReply(state, sessionId, prompt) {
|
|
1994
2100
|
const unpaid = state.unpaidDetail();
|
|
@@ -2500,8 +2606,8 @@ function serverBudgetKey(model, modelContextLimit) {
|
|
|
2500
2606
|
/**
|
|
2501
2607
|
* The session's whole-request budget: CCC_BUDGET_TOKENS when set (tests),
|
|
2502
2608
|
* else the model budget the server last reported for this model and window,
|
|
2503
|
-
* else
|
|
2504
|
-
*
|
|
2609
|
+
* else fallbackBudgetTokens (servers that predate `model_budget_tokens`, and
|
|
2610
|
+
* the first request before any response).
|
|
2505
2611
|
*/
|
|
2506
2612
|
function resolveBudget(opts, state, model, modelContextLimit) {
|
|
2507
2613
|
if (opts.budgetTokensOverride !== undefined) {
|
|
@@ -2511,7 +2617,7 @@ function resolveBudget(opts, state, model, modelContextLimit) {
|
|
|
2511
2617
|
if (reported !== undefined)
|
|
2512
2618
|
return { tokens: reported, source: "server" };
|
|
2513
2619
|
return {
|
|
2514
|
-
tokens:
|
|
2620
|
+
tokens: fallbackBudgetTokens(model, modelContextLimit),
|
|
2515
2621
|
source: "window-ratio",
|
|
2516
2622
|
};
|
|
2517
2623
|
}
|
|
@@ -3554,7 +3660,9 @@ async function recoverToolRouteMiss(args) {
|
|
|
3554
3660
|
// a later turn. An unpaid key (402, possibly set by this very compress)
|
|
3555
3661
|
// or a shutting-down proxy gets no retry.
|
|
3556
3662
|
if (!state.shutdownSignal.aborted &&
|
|
3557
|
-
opts.memtree.paymentRequiredDetail === null
|
|
3663
|
+
opts.memtree.paymentRequiredDetail === null &&
|
|
3664
|
+
!opts.memtree.keyInvalid // a refused key refuses the index too
|
|
3665
|
+
) {
|
|
3558
3666
|
opts.memtree.indexInBackground(hash, msgsForMemtree, modelContextLimit, requestSessionId(req), args.clientMeta, transcriptTimesFor(opts, requestSessionId(req), agentAttributionId(req)));
|
|
3559
3667
|
}
|
|
3560
3668
|
return forwardOriginal();
|
|
@@ -3918,6 +4026,8 @@ async function selectForwardCandidate(args) {
|
|
|
3918
4026
|
target = policy.sizing.register(replacement, "replacement");
|
|
3919
4027
|
}
|
|
3920
4028
|
const selection = await policy.sizing.select(target, args.limits);
|
|
4029
|
+
const quietDegrade = quietDegrades.get(args.rec);
|
|
4030
|
+
quietDegrades.delete(args.rec);
|
|
3921
4031
|
if (!selection) {
|
|
3922
4032
|
if (args.rec.lateCountRecovery)
|
|
3923
4033
|
args.rec.lateCountRecovery.outcome = "refused";
|
|
@@ -3943,6 +4053,7 @@ async function selectForwardCandidate(args) {
|
|
|
3943
4053
|
? "exact-count-fits-window" : measurement.source === "bytes"
|
|
3944
4054
|
? "bytes-fallback-fits-window" : "estimate-fits-window" };
|
|
3945
4055
|
warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
|
|
4056
|
+
quietDegrade?.();
|
|
3946
4057
|
}
|
|
3947
4058
|
return changed ? candidate.body : intended;
|
|
3948
4059
|
}
|
|
@@ -3962,9 +4073,12 @@ async function wholeRequestRefused(sizing, candidate, limits) {
|
|
|
3962
4073
|
}
|
|
3963
4074
|
/** Write the bounded fallback refusal (route-fallback.ts); always false. */
|
|
3964
4075
|
function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
3965
|
-
|
|
3966
|
-
const
|
|
3967
|
-
|
|
4076
|
+
// A refused key or a paused one: a retry cannot fix either, so it is final at once.
|
|
4077
|
+
const keyInvalid = args.state.keyInvalid();
|
|
4078
|
+
const unpaid = keyInvalid ? null : args.state.pausedDetail();
|
|
4079
|
+
const failure = keyInvalid ? pausedRefusal(keyInvalidRefusalMessage(args.state.keyPageUrl))
|
|
4080
|
+
: unpaid !== null ? pausedRefusal(pausedRefusalMessage(unpaid))
|
|
4081
|
+
: args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
3968
4082
|
args.rec.forwardedBytes = 0;
|
|
3969
4083
|
args.rec.approxInputTokens = 0;
|
|
3970
4084
|
args.state.diagnostics?.record("refusal", {
|
|
@@ -3977,8 +4091,9 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
3977
4091
|
inputTokens, outputTokens: args.body.max_tokens ?? 0,
|
|
3978
4092
|
outputReservation: bodyOutputReservation(args.body), indexBehind: args.rec.indexBehind === true,
|
|
3979
4093
|
budgetTokens, modelContextLimit: args.modelContextLimit, ...(unpaid !== null ? { paused: true } : {}),
|
|
4094
|
+
...(keyInvalid ? { keyInvalid: true } : {}),
|
|
3980
4095
|
};
|
|
3981
|
-
if (unpaid !== null)
|
|
4096
|
+
if (unpaid !== null || keyInvalid)
|
|
3982
4097
|
cancelPausedNotice(args.state, args.rec);
|
|
3983
4098
|
if (args.res.headersSent || args.res.destroyed)
|
|
3984
4099
|
return false;
|
|
@@ -3987,6 +4102,9 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
3987
4102
|
if (unpaid !== null) {
|
|
3988
4103
|
notePausedRefusal(args.state, args.rec, args.routeKey, requestSessionId(args.req));
|
|
3989
4104
|
}
|
|
4105
|
+
// The refusal named the key: it counts as the key notice on the user's screen.
|
|
4106
|
+
if (keyInvalid && args.rec.routeLane === "main")
|
|
4107
|
+
args.state.keyNotice.markShown();
|
|
3990
4108
|
return false;
|
|
3991
4109
|
}
|
|
3992
4110
|
/**
|
|
@@ -4025,6 +4143,8 @@ function cancelPausedNotice(state, rec) {
|
|
|
4025
4143
|
* already queued the paused notice carries the line itself.
|
|
4026
4144
|
*/
|
|
4027
4145
|
function warnPausedNearWindow(state, rec, sessionId) {
|
|
4146
|
+
if (state.keyInvalid())
|
|
4147
|
+
return; // a refused key, not a pause: the key notice says why
|
|
4028
4148
|
const detail = state.unpaidDetail();
|
|
4029
4149
|
// Paid again: a later pause (next month's limit) warns afresh.
|
|
4030
4150
|
if (detail === null)
|
|
@@ -4057,6 +4177,25 @@ function nearWindowWarned(state, sessionId) {
|
|
|
4057
4177
|
function reachedBudget(compaction) {
|
|
4058
4178
|
return compaction !== undefined && (compaction.estimatedTokens ?? 0) >= compaction.budgetTokens;
|
|
4059
4179
|
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Whether the planned size says a turn needed MemTree to compress it: a
|
|
4182
|
+
* forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
|
|
4183
|
+
* size at or over the budget, or a size nobody measured. A planned fit is
|
|
4184
|
+
* provisional: delivery's own sizing can still find the turn over the budget
|
|
4185
|
+
* (quietDegrades), since a plan can undercount.
|
|
4186
|
+
*/
|
|
4187
|
+
function compressionNeeded(compaction) {
|
|
4188
|
+
if (compaction === undefined || compaction.reason !== undefined)
|
|
4189
|
+
return true;
|
|
4190
|
+
if (compaction.estimatedTokens === undefined)
|
|
4191
|
+
return true;
|
|
4192
|
+
return compaction.estimatedTokens >= compaction.budgetTokens;
|
|
4193
|
+
}
|
|
4194
|
+
/**
|
|
4195
|
+
* A degraded turn whose plan fit the budget, so it queued no notice: the
|
|
4196
|
+
* notice to queue if delivery's sizing finds it over the budget after all.
|
|
4197
|
+
*/
|
|
4198
|
+
const quietDegrades = new WeakMap();
|
|
4060
4199
|
/** The paused notice a degraded request queued, until it is refused or settles. */
|
|
4061
4200
|
const pausedNotices = new WeakMap();
|
|
4062
4201
|
const changedDeliveryRecords = new WeakSet();
|
|
@@ -4267,6 +4406,9 @@ function forwardAccepted(req, res, bodyBuffer, opts, upstream, shutdownSignal, r
|
|
|
4267
4406
|
};
|
|
4268
4407
|
if (rec) {
|
|
4269
4408
|
rec.upstreamStatus = upstreamRes.statusCode ?? 502;
|
|
4409
|
+
const rateLimit = rateLimitFromResponse(rec.upstreamStatus, upstreamRes.headers);
|
|
4410
|
+
if (rateLimit)
|
|
4411
|
+
rec.rateLimit = rateLimit;
|
|
4270
4412
|
}
|
|
4271
4413
|
successfulStatus =
|
|
4272
4414
|
typeof upstreamRes.statusCode === "number" &&
|