codex-multi-auth 2.9.1 → 2.10.0
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/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -1
- package/config/schema/config.schema.json +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -2
- package/dist/lib/accounts.d.ts +25 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +26 -6
- package/dist/lib/codex-manager/backend-settings-helpers.d.ts.map +1 -1
- package/dist/lib/codex-manager/backend-settings-helpers.js +10 -1
- package/dist/lib/codex-manager/backend-settings-schema.d.ts +18 -0
- package/dist/lib/codex-manager/backend-settings-schema.d.ts.map +1 -1
- package/dist/lib/codex-manager/backend-settings-schema.js +19 -0
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts +1 -0
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.js +12 -0
- package/dist/lib/codex-manager/experimental-settings-schema.d.ts +2 -0
- package/dist/lib/codex-manager/experimental-settings-schema.d.ts.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-schema.js +2 -0
- package/dist/lib/config.d.ts +29 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +88 -0
- package/dist/lib/context-budget/model-context-windows.d.ts +25 -0
- package/dist/lib/context-budget/model-context-windows.d.ts.map +1 -0
- package/dist/lib/context-budget/model-context-windows.js +118 -0
- package/dist/lib/context-budget-guard.d.ts +100 -0
- package/dist/lib/context-budget-guard.d.ts.map +1 -0
- package/dist/lib/context-budget-guard.js +183 -0
- package/dist/lib/context-budget-response.d.ts +25 -0
- package/dist/lib/context-budget-response.d.ts.map +1 -0
- package/dist/lib/context-budget-response.js +57 -0
- package/dist/lib/context-overflow.d.ts.map +1 -1
- package/dist/lib/context-overflow.js +5 -60
- package/dist/lib/request/rate-limit-decision.d.ts +4 -5
- package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
- package/dist/lib/request/rate-limit-decision.js +33 -4
- package/dist/lib/request/response-metadata.d.ts.map +1 -1
- package/dist/lib/request/response-metadata.js +1 -0
- package/dist/lib/request/stream-failover-runtime.d.ts +3 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +7 -2
- package/dist/lib/runtime/context-budget-settings.d.ts +13 -0
- package/dist/lib/runtime/context-budget-settings.d.ts.map +1 -0
- package/dist/lib/runtime/context-budget-settings.js +8 -0
- package/dist/lib/runtime/loader-setup.d.ts +1 -0
- package/dist/lib/runtime/loader-setup.d.ts.map +1 -1
- package/dist/lib/runtime/loader-setup.js +1 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts +23 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-account-selection.js +15 -6
- package/dist/lib/runtime/rotation-proxy-state.d.ts +2 -0
- package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +11 -0
- package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +251 -43
- package/dist/lib/schemas.d.ts +4 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +10 -0
- package/dist/lib/synthetic-response.d.ts +31 -0
- package/dist/lib/synthetic-response.d.ts.map +1 -0
- package/dist/lib/synthetic-response.js +79 -0
- package/dist/lib/ui/ui-copy.d.ts +2 -1
- package/dist/lib/ui/ui-copy.d.ts.map +1 -1
- package/dist/lib/ui/ui-copy.js +2 -1
- package/package.json +2 -2
package/dist/lib/config.js
CHANGED
|
@@ -171,6 +171,10 @@ export const DEFAULT_PLUGIN_CONFIG = {
|
|
|
171
171
|
preemptiveQuotaRemainingPercent5h: 5,
|
|
172
172
|
preemptiveQuotaRemainingPercent7d: 5,
|
|
173
173
|
preemptiveQuotaMaxDeferralMs: 2 * 60 * 60_000,
|
|
174
|
+
contextBudgetGuardEnabled: false,
|
|
175
|
+
contextBudgetGuardSoftPercent: 65,
|
|
176
|
+
contextBudgetGuardHardPercent: 69,
|
|
177
|
+
contextBudgetGuardModelWindowOverrides: {},
|
|
174
178
|
routingMutex: "legacy",
|
|
175
179
|
schedulingStrategy: "hybrid",
|
|
176
180
|
};
|
|
@@ -834,6 +838,14 @@ function resolveNumberSetting(envName, configValue, defaultValue, options) {
|
|
|
834
838
|
logConfigWarnOnce(`Ignoring invalid numeric env ${envName}. Expected a finite number.`);
|
|
835
839
|
}
|
|
836
840
|
const candidate = envValue ?? configValue ?? defaultValue;
|
|
841
|
+
// Math.max/Math.min propagate NaN, so a non-finite candidate would be
|
|
842
|
+
// returned as-is and every threshold comparison against it reads false --
|
|
843
|
+
// a setting that silently disables whatever it gates. parseNumberEnv already
|
|
844
|
+
// rejects non-finite env values and zod rejects NaN on the config field, so
|
|
845
|
+
// this is a boundary guard for the ~20 settings that share this helper
|
|
846
|
+
// rather than a reachable path through loadPluginConfig.
|
|
847
|
+
if (!Number.isFinite(candidate))
|
|
848
|
+
return defaultValue;
|
|
837
849
|
const min = options?.min ?? Number.NEGATIVE_INFINITY;
|
|
838
850
|
const max = options?.max ?? Number.POSITIVE_INFINITY;
|
|
839
851
|
return Math.max(min, Math.min(max, candidate));
|
|
@@ -1295,6 +1307,62 @@ export function getPreemptiveQuotaRemainingPercent7d(pluginConfig) {
|
|
|
1295
1307
|
export function getPreemptiveQuotaMaxDeferralMs(pluginConfig) {
|
|
1296
1308
|
return resolveNumberSetting("CODEX_AUTH_PREEMPTIVE_QUOTA_MAX_DEFERRAL_MS", pluginConfig.preemptiveQuotaMaxDeferralMs, 2 * 60 * 60_000, { min: 1_000 });
|
|
1297
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Whether the (experimental, opt-in) context budget guard is enabled.
|
|
1312
|
+
*
|
|
1313
|
+
* Ships disabled: pausing a session before it hits the context window is a
|
|
1314
|
+
* staged behavior change, not a default-on safety net like preemptive quota
|
|
1315
|
+
* deferral, so an explicit opt-in is required.
|
|
1316
|
+
*/
|
|
1317
|
+
export function getContextBudgetGuardEnabled(pluginConfig) {
|
|
1318
|
+
return resolveBooleanSetting("CODEX_AUTH_CONTEXT_BUDGET_GUARD_ENABLED", pluginConfig.contextBudgetGuardEnabled, false);
|
|
1319
|
+
}
|
|
1320
|
+
/** Percent of the effective context window at which the guard's non-blocking advisory fires. */
|
|
1321
|
+
export function getContextBudgetGuardSoftPercent(pluginConfig) {
|
|
1322
|
+
return resolveNumberSetting("CODEX_AUTH_CONTEXT_BUDGET_SOFT_PCT", pluginConfig.contextBudgetGuardSoftPercent, 65, { min: 0, max: 100 });
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Percent of the effective context window at which the guard pauses the next
|
|
1326
|
+
* request.
|
|
1327
|
+
*
|
|
1328
|
+
* Floored at 10, not 0. `resolveNumberSetting` clamps out-of-range values
|
|
1329
|
+
* instead of rejecting them, so a configured (or env-supplied) `0` would be a
|
|
1330
|
+
* legal threshold that every recorded snapshot clears — pausing every session
|
|
1331
|
+
* from its first turn on. The guard applies the same floor in `configure()`
|
|
1332
|
+
* so a directly constructed instance cannot be set below it either.
|
|
1333
|
+
*/
|
|
1334
|
+
export function getContextBudgetGuardHardPercent(pluginConfig) {
|
|
1335
|
+
return resolveNumberSetting("CODEX_AUTH_CONTEXT_BUDGET_HARD_PCT", pluginConfig.contextBudgetGuardHardPercent, 69, { min: 10, max: 100 });
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* User-supplied context-window sizes, keyed by normalized model id, that
|
|
1339
|
+
* always take priority over this package's own best-effort estimates (see
|
|
1340
|
+
* `lib/context-budget/model-context-windows.ts`). Settings-only: there is no
|
|
1341
|
+
* environment override, the same as `unsupportedCodexFallbackChain`, since a
|
|
1342
|
+
* per-model token count is not a reasonable single env var.
|
|
1343
|
+
*/
|
|
1344
|
+
export function getContextBudgetGuardModelWindowOverrides(pluginConfig) {
|
|
1345
|
+
const overrides = pluginConfig.contextBudgetGuardModelWindowOverrides;
|
|
1346
|
+
if (!overrides || typeof overrides !== "object") {
|
|
1347
|
+
return {};
|
|
1348
|
+
}
|
|
1349
|
+
const normalized = {};
|
|
1350
|
+
for (const [rawModel, rawValue] of Object.entries(overrides)) {
|
|
1351
|
+
const model = rawModel.trim().toLowerCase();
|
|
1352
|
+
if (!model)
|
|
1353
|
+
continue;
|
|
1354
|
+
if (typeof rawValue !== "number" || !Number.isFinite(rawValue))
|
|
1355
|
+
continue;
|
|
1356
|
+
// Floor first: the schema accepts any positive number, so a sub-1 value
|
|
1357
|
+
// passes a `rawValue > 0` check and then floors to 0, which is stored as
|
|
1358
|
+
// a window of zero tokens and silently ignored downstream.
|
|
1359
|
+
const tokens = Math.floor(rawValue);
|
|
1360
|
+
if (tokens <= 0)
|
|
1361
|
+
continue;
|
|
1362
|
+
normalized[model] = tokens;
|
|
1363
|
+
}
|
|
1364
|
+
return normalized;
|
|
1365
|
+
}
|
|
1298
1366
|
const ROUTING_MUTEX_MODES = new Set(["enabled", "legacy"]);
|
|
1299
1367
|
/**
|
|
1300
1368
|
* Resolve the routing-mutex mode (PR-N / R4).
|
|
@@ -1649,6 +1717,26 @@ const CONFIG_EXPLAIN_ENTRIES = [
|
|
|
1649
1717
|
envNames: ["CODEX_AUTH_PREEMPTIVE_QUOTA_MAX_DEFERRAL_MS"],
|
|
1650
1718
|
getValue: getPreemptiveQuotaMaxDeferralMs,
|
|
1651
1719
|
},
|
|
1720
|
+
{
|
|
1721
|
+
key: "contextBudgetGuardEnabled",
|
|
1722
|
+
envNames: ["CODEX_AUTH_CONTEXT_BUDGET_GUARD_ENABLED"],
|
|
1723
|
+
getValue: getContextBudgetGuardEnabled,
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
key: "contextBudgetGuardSoftPercent",
|
|
1727
|
+
envNames: ["CODEX_AUTH_CONTEXT_BUDGET_SOFT_PCT"],
|
|
1728
|
+
getValue: getContextBudgetGuardSoftPercent,
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
key: "contextBudgetGuardHardPercent",
|
|
1732
|
+
envNames: ["CODEX_AUTH_CONTEXT_BUDGET_HARD_PCT"],
|
|
1733
|
+
getValue: getContextBudgetGuardHardPercent,
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
key: "contextBudgetGuardModelWindowOverrides",
|
|
1737
|
+
envNames: [],
|
|
1738
|
+
getValue: getContextBudgetGuardModelWindowOverrides,
|
|
1739
|
+
},
|
|
1652
1740
|
// config-01/config-07: these three live settings were missing from the explain
|
|
1653
1741
|
// report, so `config explain` silently under-reported the effective config. A
|
|
1654
1742
|
// parity test (test/config-explain.test.ts) now guards this class of drift.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routable models this guard consciously does not estimate a window for.
|
|
3
|
+
* `getEffectiveContextWindow` returns `null` for these absent an override,
|
|
4
|
+
* and the guard treats `null` as "cannot evaluate; skip" — never a guessed
|
|
5
|
+
* percentage. The 5.6 family ships with materially different per-tier
|
|
6
|
+
* behavior (see the v2.5.0 release notes) closely enough to launch that no
|
|
7
|
+
* estimate here has been reviewed for it yet.
|
|
8
|
+
*/
|
|
9
|
+
export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
10
|
+
export interface EffectiveContextWindow {
|
|
11
|
+
tokens: number;
|
|
12
|
+
source: "override" | "estimate";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the context-window size to evaluate a session's budget against.
|
|
16
|
+
*
|
|
17
|
+
* Precedence: an explicit override always wins, even for a model this file
|
|
18
|
+
* has never heard of — the whole point of the override setting is to let a
|
|
19
|
+
* user correct or extend past this file's guesses. Failing that, fall back
|
|
20
|
+
* to the estimate table. Returns `null` when neither source has a value, so
|
|
21
|
+
* the guard can no-op rather than evaluate against a fabricated number.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getEffectiveContextWindow(model: string | null | undefined, overrides: Record<string, number> | undefined): EffectiveContextWindow | null;
|
|
24
|
+
export declare function listEstimatedModelContextWindows(): Record<string, number>;
|
|
25
|
+
//# sourceMappingURL=model-context-windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-context-windows.d.ts","sourceRoot":"","sources":["../../../lib/context-budget/model-context-windows.ts"],"names":[],"mappings":"AAyCA;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,2DAI9B,CAAC;AAgCX,MAAM,WAAW,sBAAsB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAC3C,sBAAsB,GAAG,IAAI,CA0B/B;AAED,wBAAgB,gCAAgC,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEzE"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getNormalizedModel } from "../request/helpers/model-map.js";
|
|
2
|
+
/**
|
|
3
|
+
* Context-window size estimates for the Context Budget Guard.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `lib/usage/pricing.ts`, the numbers below are NOT published,
|
|
6
|
+
* contractual facts. Per `docs/releases/v2.5.0.md` ("Where the model facts
|
|
7
|
+
* came from"), the context window OpenAI's published API docs advertise is
|
|
8
|
+
* for the API surface, not the ChatGPT Codex backend this wrapper actually
|
|
9
|
+
* talks to — and the docs pages don't even agree with each other. Treating a
|
|
10
|
+
* hardcoded number here as ground truth would repeat that exact mistake for
|
|
11
|
+
* a safety feature, which is worse than not shipping the feature at all.
|
|
12
|
+
*
|
|
13
|
+
* So this table is a fallback starting point ONLY. `getEffectiveContextWindow`
|
|
14
|
+
* always prefers a caller-supplied override (from
|
|
15
|
+
* `contextBudgetGuardModelWindowOverrides`, the setting a user fills in once
|
|
16
|
+
* they know their real ceiling) over anything listed here.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort estimates, in tokens. Deliberately conservative: an estimate
|
|
20
|
+
* that is too LOW makes the guard fire early (annoying, safe); an estimate
|
|
21
|
+
* that is too HIGH makes the guard fire late or never (silent, unsafe). When
|
|
22
|
+
* in doubt about a model's real ceiling, leave it out of this table and add
|
|
23
|
+
* it to UNESTIMATED_ROUTABLE_MODELS instead.
|
|
24
|
+
*/
|
|
25
|
+
const ESTIMATED_MODEL_CONTEXT_WINDOWS = {
|
|
26
|
+
"gpt-5.1": 260_000,
|
|
27
|
+
"gpt-5.2": 260_000,
|
|
28
|
+
"gpt-5.2-pro": 260_000,
|
|
29
|
+
"gpt-5.3-codex": 260_000,
|
|
30
|
+
"gpt-5.4": 260_000,
|
|
31
|
+
"gpt-5.4-mini": 260_000,
|
|
32
|
+
"gpt-5.4-nano": 260_000,
|
|
33
|
+
"gpt-5.4-pro": 260_000,
|
|
34
|
+
"gpt-5.5": 260_000,
|
|
35
|
+
"gpt-5.5-pro": 260_000,
|
|
36
|
+
"gpt-5-mini": 260_000,
|
|
37
|
+
"gpt-5-nano": 260_000,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Routable models this guard consciously does not estimate a window for.
|
|
41
|
+
* `getEffectiveContextWindow` returns `null` for these absent an override,
|
|
42
|
+
* and the guard treats `null` as "cannot evaluate; skip" — never a guessed
|
|
43
|
+
* percentage. The 5.6 family ships with materially different per-tier
|
|
44
|
+
* behavior (see the v2.5.0 release notes) closely enough to launch that no
|
|
45
|
+
* estimate here has been reviewed for it yet.
|
|
46
|
+
*/
|
|
47
|
+
export const UNESTIMATED_ROUTABLE_MODELS = [
|
|
48
|
+
"gpt-5.6-sol",
|
|
49
|
+
"gpt-5.6-terra",
|
|
50
|
+
"gpt-5.6-luna",
|
|
51
|
+
];
|
|
52
|
+
function normalizeModelName(model) {
|
|
53
|
+
const trimmed = model?.trim().toLowerCase();
|
|
54
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Model ids to try, in order, against the override map and the estimate table.
|
|
58
|
+
*
|
|
59
|
+
* The guard is fed the model string exactly as the client sent it: the
|
|
60
|
+
* rotation proxy is a pass-through and `buildResponsesRequestContext` copies
|
|
61
|
+
* `body.model` verbatim. That raw string is very often NOT a key of this
|
|
62
|
+
* file's table — Codex CLI's own default is `gpt-5-codex`, and every
|
|
63
|
+
* reasoning-suffixed alias (`gpt-5.3-codex-high`, …) is an alias-map entry
|
|
64
|
+
* rather than a catalog model. Looking up only the raw string made the guard
|
|
65
|
+
* silently return "no window, skip" for the most common model in the product.
|
|
66
|
+
*
|
|
67
|
+
* So: try the raw (lowercased) string first, so a user override keyed exactly
|
|
68
|
+
* the way they type the model still wins, then the catalog's canonical id for
|
|
69
|
+
* it. `getNormalizedModel` is deliberately the exact/alias-only resolver —
|
|
70
|
+
* `resolveNormalizedModel` falls back to `DEFAULT_MODEL` for anything it does
|
|
71
|
+
* not recognize, which would hand an unknown model gpt-5.5's window and
|
|
72
|
+
* evaluate a real session against a fabricated number.
|
|
73
|
+
*/
|
|
74
|
+
function windowLookupCandidates(model) {
|
|
75
|
+
const raw = normalizeModelName(model);
|
|
76
|
+
if (!raw)
|
|
77
|
+
return [];
|
|
78
|
+
const canonical = normalizeModelName(getNormalizedModel(raw));
|
|
79
|
+
return canonical && canonical !== raw ? [raw, canonical] : [raw];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the context-window size to evaluate a session's budget against.
|
|
83
|
+
*
|
|
84
|
+
* Precedence: an explicit override always wins, even for a model this file
|
|
85
|
+
* has never heard of — the whole point of the override setting is to let a
|
|
86
|
+
* user correct or extend past this file's guesses. Failing that, fall back
|
|
87
|
+
* to the estimate table. Returns `null` when neither source has a value, so
|
|
88
|
+
* the guard can no-op rather than evaluate against a fabricated number.
|
|
89
|
+
*/
|
|
90
|
+
export function getEffectiveContextWindow(model, overrides) {
|
|
91
|
+
const candidates = windowLookupCandidates(model);
|
|
92
|
+
if (candidates.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
const override = overrides?.[candidate];
|
|
96
|
+
if (typeof override !== "number" || !Number.isFinite(override))
|
|
97
|
+
continue;
|
|
98
|
+
// Floor BEFORE the positivity check, not after. Checking `override > 0`
|
|
99
|
+
// on the raw value admits anything in (0, 1), which then floors to 0 and
|
|
100
|
+
// returns a zero-token window -- breaking this function's contract that a
|
|
101
|
+
// window it cannot resolve comes back as null, and handing any caller
|
|
102
|
+
// that trusts it a division by zero.
|
|
103
|
+
const tokens = Math.floor(override);
|
|
104
|
+
if (tokens > 0) {
|
|
105
|
+
return { tokens, source: "override" };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
const estimate = ESTIMATED_MODEL_CONTEXT_WINDOWS[candidate];
|
|
110
|
+
if (typeof estimate === "number") {
|
|
111
|
+
return { tokens: estimate, source: "estimate" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
export function listEstimatedModelContextWindows() {
|
|
117
|
+
return { ...ESTIMATED_MODEL_CONTEXT_WINDOWS };
|
|
118
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface ContextBudgetSnapshot {
|
|
2
|
+
model: string;
|
|
3
|
+
/**
|
|
4
|
+
* Tokens the NEXT turn on this session is expected to resend as context:
|
|
5
|
+
* this turn's `input_tokens` plus the part of its output that becomes
|
|
6
|
+
* conversation history. Deliberately NOT `total_tokens`, which also counts
|
|
7
|
+
* `reasoning_tokens` that the Responses API drops rather than resends —
|
|
8
|
+
* charging those against the window inflates every measurement by this
|
|
9
|
+
* turn's thinking budget and fires the guard early on long-reasoning turns.
|
|
10
|
+
*/
|
|
11
|
+
contextTokens: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ContextBudgetOptions {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
softPercent?: number;
|
|
17
|
+
hardPercent?: number;
|
|
18
|
+
modelWindowOverrides?: Record<string, number>;
|
|
19
|
+
}
|
|
20
|
+
interface ContextBudgetPressure {
|
|
21
|
+
percent: number;
|
|
22
|
+
contextTokens: number;
|
|
23
|
+
windowTokens: number;
|
|
24
|
+
windowSource: "override" | "estimate";
|
|
25
|
+
model: string;
|
|
26
|
+
}
|
|
27
|
+
export type ContextBudgetAdvisory = {
|
|
28
|
+
level: "ok";
|
|
29
|
+
} | ({
|
|
30
|
+
level: "soft";
|
|
31
|
+
} & ContextBudgetPressure) | ({
|
|
32
|
+
level: "hard";
|
|
33
|
+
} & ContextBudgetPressure);
|
|
34
|
+
/**
|
|
35
|
+
* Tracks how full each session's context window is and decides, before the
|
|
36
|
+
* NEXT request on that session goes out, whether to nudge (soft) or pause
|
|
37
|
+
* (hard) the caller.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately shaped like `PreemptiveQuotaScheduler`
|
|
40
|
+
* (`lib/preemptive-quota-scheduler.ts`): a per-key `Map` snapshot, a
|
|
41
|
+
* `configure()`/decision-getter split, and a lazy `prune()` on the same
|
|
42
|
+
* cadence. Same reason in both places — the decision needs to survive
|
|
43
|
+
* across the request that observed the usage and the next request that
|
|
44
|
+
* asks "are we still OK," and a scheduled sweep keeps memory bounded
|
|
45
|
+
* without a dedicated timer.
|
|
46
|
+
*/
|
|
47
|
+
export declare class ContextBudgetGuard {
|
|
48
|
+
private readonly snapshots;
|
|
49
|
+
private enabled;
|
|
50
|
+
private softPercent;
|
|
51
|
+
private hardPercent;
|
|
52
|
+
private modelWindowOverrides;
|
|
53
|
+
private lastPruneAt;
|
|
54
|
+
constructor(options?: ContextBudgetOptions);
|
|
55
|
+
configure(options?: ContextBudgetOptions): void;
|
|
56
|
+
private maybePrune;
|
|
57
|
+
/**
|
|
58
|
+
* Record the latest known context size for a session, from that turn's usage.
|
|
59
|
+
*
|
|
60
|
+
* No-ops while disabled. The call sites sit on the hot path of every
|
|
61
|
+
* forwarded Responses turn, so a default-off install must not pay for a
|
|
62
|
+
* per-session map it will never read.
|
|
63
|
+
*/
|
|
64
|
+
update(key: string, snapshot: ContextBudgetSnapshot): void;
|
|
65
|
+
/**
|
|
66
|
+
* Evaluate before forwarding the NEXT request on this session key.
|
|
67
|
+
*
|
|
68
|
+
* `requestModel` is the model that request will use, and it is what the
|
|
69
|
+
* window is resolved from -- NOT the model of the last recorded turn. The
|
|
70
|
+
* two differ whenever a session switches models, and then the snapshot's
|
|
71
|
+
* model is the wrong denominator: a session carrying 205k tokens is 79% of
|
|
72
|
+
* a 260k window and a fifth of a 1M one. Using the stale model also let
|
|
73
|
+
* the guard pause a request for a model it deliberately refuses to
|
|
74
|
+
* estimate, and print that stale model's name in the notice. Falls back to
|
|
75
|
+
* the snapshot's model only when the request declares none.
|
|
76
|
+
*/
|
|
77
|
+
getAdvisory(key: string, now?: number, requestModel?: string | null): ContextBudgetAdvisory;
|
|
78
|
+
/** Drop sessions untouched for a while so the map does not grow unbounded. */
|
|
79
|
+
prune(now?: number): number;
|
|
80
|
+
/** Forget a single session's tracked usage, e.g. once it has visibly compacted. */
|
|
81
|
+
forget(key: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Call immediately after emitting a hard pause for `key`.
|
|
84
|
+
*
|
|
85
|
+
* The pause is synthetic: it short-circuits before the request is forwarded,
|
|
86
|
+
* so `update()` never runs for it and the recorded usage can never fall on
|
|
87
|
+
* its own. Left in place, the first crossing of the hard threshold would
|
|
88
|
+
* block that session forever — including the `/compact` turn the pause
|
|
89
|
+
* message tells the user to run, which travels on the same session key.
|
|
90
|
+
*
|
|
91
|
+
* Dropping the snapshot makes the pause one-shot per measurement: the next
|
|
92
|
+
* request is forwarded and re-measures. A session that really did compact
|
|
93
|
+
* comes back under the threshold and stays quiet; one that did not gets
|
|
94
|
+
* paused again on the turn after, so the warning still repeats while the
|
|
95
|
+
* session is over budget, without ever becoming a dead end.
|
|
96
|
+
*/
|
|
97
|
+
noteHardPauseEmitted(key: string): void;
|
|
98
|
+
}
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=context-budget-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-budget-guard.d.ts","sourceRoot":"","sources":["../../lib/context-budget-guard.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C;AAED,UAAU,qBAAqB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,qBAAqB,CAAC,GAC3C,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,qBAAqB,CAAC,CAAC;AAuB/C;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,oBAAoB,CAAqC;IACjE,OAAO,CAAC,WAAW,CAAK;gBAEZ,OAAO,GAAE,oBAAyB;IAI9C,SAAS,CAAC,OAAO,GAAE,oBAAyB,GAAG,IAAI;IAgCnD,OAAO,CAAC,UAAU;IAMlB;;;;;;OAMG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAmB1D;;;;;;;;;;;OAWG;IACH,WAAW,CACV,GAAG,EAAE,MAAM,EACX,GAAG,SAAa,EAChB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB;IAuCxB,8EAA8E;IAC9E,KAAK,CAAC,GAAG,SAAa,GAAG,MAAM;IAW/B,mFAAmF;IACnF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAGvC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getEffectiveContextWindow } from "./context-budget/model-context-windows.js";
|
|
2
|
+
const DEFAULT_SOFT_PERCENT = 65;
|
|
3
|
+
const DEFAULT_HARD_PERCENT = 69;
|
|
4
|
+
/**
|
|
5
|
+
* Lowest hard threshold that can describe a real budget.
|
|
6
|
+
*
|
|
7
|
+
* `getContextBudgetGuardHardPercent` clamps rather than rejects (that is what
|
|
8
|
+
* every other numeric setting does), so without a floor a configured `0` is a
|
|
9
|
+
* legal value that makes `percent >= hardPercent` true for every snapshot —
|
|
10
|
+
* every session pauses from its first recorded turn onward. Anything under
|
|
11
|
+
* 10% of a window is that same failure in slower motion, so clamp up to a
|
|
12
|
+
* value that at least can't fire on turn one of a normal session.
|
|
13
|
+
*/
|
|
14
|
+
const MIN_HARD_PERCENT = 10;
|
|
15
|
+
/** Sessions idle longer than this are dropped on the next prune sweep. */
|
|
16
|
+
const SESSION_TTL_MS = 6 * 60 * 60_000;
|
|
17
|
+
const PRUNE_INTERVAL_MS = 60_000;
|
|
18
|
+
function clampInt(value, min, max) {
|
|
19
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Tracks how full each session's context window is and decides, before the
|
|
23
|
+
* NEXT request on that session goes out, whether to nudge (soft) or pause
|
|
24
|
+
* (hard) the caller.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately shaped like `PreemptiveQuotaScheduler`
|
|
27
|
+
* (`lib/preemptive-quota-scheduler.ts`): a per-key `Map` snapshot, a
|
|
28
|
+
* `configure()`/decision-getter split, and a lazy `prune()` on the same
|
|
29
|
+
* cadence. Same reason in both places — the decision needs to survive
|
|
30
|
+
* across the request that observed the usage and the next request that
|
|
31
|
+
* asks "are we still OK," and a scheduled sweep keeps memory bounded
|
|
32
|
+
* without a dedicated timer.
|
|
33
|
+
*/
|
|
34
|
+
export class ContextBudgetGuard {
|
|
35
|
+
snapshots = new Map();
|
|
36
|
+
enabled = false;
|
|
37
|
+
softPercent = DEFAULT_SOFT_PERCENT;
|
|
38
|
+
hardPercent = DEFAULT_HARD_PERCENT;
|
|
39
|
+
modelWindowOverrides;
|
|
40
|
+
lastPruneAt = 0;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.configure(options);
|
|
43
|
+
}
|
|
44
|
+
configure(options = {}) {
|
|
45
|
+
if (typeof options.enabled === "boolean") {
|
|
46
|
+
// Turning the guard off must also drop what it already tracked, or a
|
|
47
|
+
// disable/re-enable cycle re-evaluates against usage measured under
|
|
48
|
+
// the old settings and can pause a session on its first request back.
|
|
49
|
+
if (!options.enabled && this.enabled) {
|
|
50
|
+
this.snapshots.clear();
|
|
51
|
+
}
|
|
52
|
+
this.enabled = options.enabled;
|
|
53
|
+
}
|
|
54
|
+
if (typeof options.softPercent === "number" && Number.isFinite(options.softPercent)) {
|
|
55
|
+
this.softPercent = clampInt(options.softPercent, 0, 100);
|
|
56
|
+
}
|
|
57
|
+
if (typeof options.hardPercent === "number" && Number.isFinite(options.hardPercent)) {
|
|
58
|
+
this.hardPercent = clampInt(options.hardPercent, MIN_HARD_PERCENT, 100);
|
|
59
|
+
}
|
|
60
|
+
// A misconfigured soft >= hard must degrade safely (guard fires later
|
|
61
|
+
// than intended) rather than throw or produce a nonsensical "hard but
|
|
62
|
+
// not soft" state. Never silently widen hard to fix it — narrowing soft
|
|
63
|
+
// is the change least likely to surprise whoever set hard deliberately.
|
|
64
|
+
if (this.softPercent >= this.hardPercent) {
|
|
65
|
+
this.softPercent = Math.max(0, this.hardPercent - 1);
|
|
66
|
+
}
|
|
67
|
+
if (options.modelWindowOverrides) {
|
|
68
|
+
this.modelWindowOverrides = options.modelWindowOverrides;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
maybePrune(now) {
|
|
72
|
+
if (now - this.lastPruneAt < PRUNE_INTERVAL_MS)
|
|
73
|
+
return;
|
|
74
|
+
this.prune(now);
|
|
75
|
+
this.lastPruneAt = now;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record the latest known context size for a session, from that turn's usage.
|
|
79
|
+
*
|
|
80
|
+
* No-ops while disabled. The call sites sit on the hot path of every
|
|
81
|
+
* forwarded Responses turn, so a default-off install must not pay for a
|
|
82
|
+
* per-session map it will never read.
|
|
83
|
+
*/
|
|
84
|
+
update(key, snapshot) {
|
|
85
|
+
if (!this.enabled || !key)
|
|
86
|
+
return;
|
|
87
|
+
// Both production call sites source contextTokens from
|
|
88
|
+
// lib/usage/usage-extraction.ts, whose nonNegativeInteger already returns
|
|
89
|
+
// 0 for anything non-finite, so this is a boundary guard rather than a
|
|
90
|
+
// reachable path today. It is here because percent is a division by the
|
|
91
|
+
// window: a non-finite count would put `Infinity%` in the pause notice a
|
|
92
|
+
// user reads, and every threshold comparison against NaN is false, which
|
|
93
|
+
// disables the guard silently.
|
|
94
|
+
if (!Number.isFinite(snapshot.contextTokens) ||
|
|
95
|
+
snapshot.contextTokens < 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.maybePrune(snapshot.updatedAt || Date.now());
|
|
99
|
+
this.snapshots.set(key, snapshot);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Evaluate before forwarding the NEXT request on this session key.
|
|
103
|
+
*
|
|
104
|
+
* `requestModel` is the model that request will use, and it is what the
|
|
105
|
+
* window is resolved from -- NOT the model of the last recorded turn. The
|
|
106
|
+
* two differ whenever a session switches models, and then the snapshot's
|
|
107
|
+
* model is the wrong denominator: a session carrying 205k tokens is 79% of
|
|
108
|
+
* a 260k window and a fifth of a 1M one. Using the stale model also let
|
|
109
|
+
* the guard pause a request for a model it deliberately refuses to
|
|
110
|
+
* estimate, and print that stale model's name in the notice. Falls back to
|
|
111
|
+
* the snapshot's model only when the request declares none.
|
|
112
|
+
*/
|
|
113
|
+
getAdvisory(key, now = Date.now(), requestModel) {
|
|
114
|
+
this.maybePrune(now);
|
|
115
|
+
if (!this.enabled || !key)
|
|
116
|
+
return { level: "ok" };
|
|
117
|
+
const snapshot = this.snapshots.get(key);
|
|
118
|
+
if (!snapshot)
|
|
119
|
+
return { level: "ok" };
|
|
120
|
+
const trimmedRequestModel = requestModel?.trim();
|
|
121
|
+
const evaluatedModel = trimmedRequestModel && trimmedRequestModel.length > 0
|
|
122
|
+
? trimmedRequestModel
|
|
123
|
+
: snapshot.model;
|
|
124
|
+
const window = getEffectiveContextWindow(evaluatedModel, this.modelWindowOverrides);
|
|
125
|
+
if (!window || window.tokens <= 0)
|
|
126
|
+
return { level: "ok" };
|
|
127
|
+
const percent = (snapshot.contextTokens / window.tokens) * 100;
|
|
128
|
+
if (percent >= this.hardPercent) {
|
|
129
|
+
return {
|
|
130
|
+
level: "hard",
|
|
131
|
+
percent,
|
|
132
|
+
contextTokens: snapshot.contextTokens,
|
|
133
|
+
windowTokens: window.tokens,
|
|
134
|
+
windowSource: window.source,
|
|
135
|
+
model: evaluatedModel,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (percent >= this.softPercent) {
|
|
139
|
+
return {
|
|
140
|
+
level: "soft",
|
|
141
|
+
percent,
|
|
142
|
+
contextTokens: snapshot.contextTokens,
|
|
143
|
+
windowTokens: window.tokens,
|
|
144
|
+
windowSource: window.source,
|
|
145
|
+
model: evaluatedModel,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { level: "ok" };
|
|
149
|
+
}
|
|
150
|
+
/** Drop sessions untouched for a while so the map does not grow unbounded. */
|
|
151
|
+
prune(now = Date.now()) {
|
|
152
|
+
let removed = 0;
|
|
153
|
+
for (const [key, snapshot] of this.snapshots.entries()) {
|
|
154
|
+
if (now - snapshot.updatedAt > SESSION_TTL_MS) {
|
|
155
|
+
this.snapshots.delete(key);
|
|
156
|
+
removed += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return removed;
|
|
160
|
+
}
|
|
161
|
+
/** Forget a single session's tracked usage, e.g. once it has visibly compacted. */
|
|
162
|
+
forget(key) {
|
|
163
|
+
this.snapshots.delete(key);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Call immediately after emitting a hard pause for `key`.
|
|
167
|
+
*
|
|
168
|
+
* The pause is synthetic: it short-circuits before the request is forwarded,
|
|
169
|
+
* so `update()` never runs for it and the recorded usage can never fall on
|
|
170
|
+
* its own. Left in place, the first crossing of the hard threshold would
|
|
171
|
+
* block that session forever — including the `/compact` turn the pause
|
|
172
|
+
* message tells the user to run, which travels on the same session key.
|
|
173
|
+
*
|
|
174
|
+
* Dropping the snapshot makes the pause one-shot per measurement: the next
|
|
175
|
+
* request is forwarded and re-measures. A session that really did compact
|
|
176
|
+
* comes back under the threshold and stays quiet; one that did not gets
|
|
177
|
+
* paused again on the turn after, so the warning still repeats while the
|
|
178
|
+
* session is over budget, without ever becoming a dead end.
|
|
179
|
+
*/
|
|
180
|
+
noteHardPauseEmitted(key) {
|
|
181
|
+
this.snapshots.delete(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ContextBudgetAdvisory } from "./context-budget-guard.js";
|
|
2
|
+
/** Response header carrying the soft-threshold advisory, non-blocking. */
|
|
3
|
+
export declare const CONTEXT_BUDGET_PERCENT_HEADER = "x-codex-context-budget-percent";
|
|
4
|
+
/**
|
|
5
|
+
* Build the synthetic response returned in place of forwarding to upstream
|
|
6
|
+
* when the context budget guard's hard threshold has been reached.
|
|
7
|
+
*
|
|
8
|
+
* Same technique as `createContextOverflowResponse`: a 200 OK synthetic SSE
|
|
9
|
+
* reply so the host session never locks on an error for a notice the plugin
|
|
10
|
+
* itself generated — except this fires pre-flight, before the request that
|
|
11
|
+
* would overflow is ever sent, instead of reacting to the 400 after the
|
|
12
|
+
* fact.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createContextBudgetPauseResponse(advisory: Extract<ContextBudgetAdvisory, {
|
|
15
|
+
level: "hard";
|
|
16
|
+
}>): Response;
|
|
17
|
+
/**
|
|
18
|
+
* Non-blocking header for the soft threshold, added to the real forwarded
|
|
19
|
+
* response next to the existing `x-codex-*-used-percent` quota headers.
|
|
20
|
+
* Consumed by `codex-multi-auth status` / the statusline, not by the model.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildContextBudgetHeaders(advisory: Extract<ContextBudgetAdvisory, {
|
|
23
|
+
level: "soft" | "hard";
|
|
24
|
+
}>): Record<string, string>;
|
|
25
|
+
//# sourceMappingURL=context-budget-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-budget-response.d.ts","sourceRoot":"","sources":["../../lib/context-budget-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAGvE,0EAA0E;AAC1E,eAAO,MAAM,6BAA6B,mCAAmC,CAAC;AAgC9E;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAC/C,QAAQ,EAAE,OAAO,CAAC,qBAAqB,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,QAAQ,CAOV;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,OAAO,CAAC,qBAAqB,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,GAClE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIxB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createSyntheticSseResponse } from "./synthetic-response.js";
|
|
2
|
+
/** Response header carrying the soft-threshold advisory, non-blocking. */
|
|
3
|
+
export const CONTEXT_BUDGET_PERCENT_HEADER = "x-codex-context-budget-percent";
|
|
4
|
+
function formatPercent(percent) {
|
|
5
|
+
return percent.toFixed(1);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The message shown to users when the context budget guard pauses a session
|
|
9
|
+
* at the hard threshold, before the request that would have pushed it over
|
|
10
|
+
* the model's window is even sent upstream.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately echoes `lib/context-overflow.ts`'s CONTEXT_OVERFLOW_MESSAGE
|
|
13
|
+
* copy so the two related notices read as one family in a transcript,
|
|
14
|
+
* rather than as two unexplained different warnings.
|
|
15
|
+
*/
|
|
16
|
+
function buildPauseMessage(advisory) {
|
|
17
|
+
const windowNote = advisory.windowSource === "override"
|
|
18
|
+
? "your configured window size"
|
|
19
|
+
: "an estimated window size (unverified — set `contextBudgetGuardModelWindowOverrides` if you know the real limit)";
|
|
20
|
+
return `[Plugin Notice] Context budget guard paused this session at ${formatPercent(advisory.percent)}% of ${windowNote} for \`${advisory.model}\`, before hitting the model's actual limit.
|
|
21
|
+
|
|
22
|
+
Please use one of these commands to reduce context size:
|
|
23
|
+
|
|
24
|
+
• **/compact** - Compress conversation history (recommended)
|
|
25
|
+
• **/clear** - Start fresh with empty context
|
|
26
|
+
• **/undo** - Remove recent messages
|
|
27
|
+
|
|
28
|
+
Then retry your request. This pause clears itself automatically once your context usage drops back below the threshold.`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the synthetic response returned in place of forwarding to upstream
|
|
32
|
+
* when the context budget guard's hard threshold has been reached.
|
|
33
|
+
*
|
|
34
|
+
* Same technique as `createContextOverflowResponse`: a 200 OK synthetic SSE
|
|
35
|
+
* reply so the host session never locks on an error for a notice the plugin
|
|
36
|
+
* itself generated — except this fires pre-flight, before the request that
|
|
37
|
+
* would overflow is ever sent, instead of reacting to the 400 after the
|
|
38
|
+
* fact.
|
|
39
|
+
*/
|
|
40
|
+
export function createContextBudgetPauseResponse(advisory) {
|
|
41
|
+
return createSyntheticSseResponse({
|
|
42
|
+
model: advisory.model,
|
|
43
|
+
message: buildPauseMessage(advisory),
|
|
44
|
+
idPrefix: "context_budget_pause",
|
|
45
|
+
errorType: "context_budget_pause",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Non-blocking header for the soft threshold, added to the real forwarded
|
|
50
|
+
* response next to the existing `x-codex-*-used-percent` quota headers.
|
|
51
|
+
* Consumed by `codex-multi-auth status` / the statusline, not by the model.
|
|
52
|
+
*/
|
|
53
|
+
export function buildContextBudgetHeaders(advisory) {
|
|
54
|
+
return {
|
|
55
|
+
[CONTEXT_BUDGET_PERCENT_HEADER]: formatPercent(advisory.percent),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-overflow.d.ts","sourceRoot":"","sources":["../../lib/context-overflow.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"context-overflow.d.ts","sourceRoot":"","sources":["../../lib/context-overflow.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkBH;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMhF;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,GAAE,MAAkB,GAAG,QAAQ,CAOjF;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAC,CAmBrE"}
|