@bitkyc08/opencodex 2.40.0 → 2.41.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/README.md +4 -0
- package/gui/dist/assets/index-B2YjLA-i.css +1 -0
- package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/google.ts +39 -2
- package/src/adapters/openai-responses.ts +14 -1
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/metadata.ts +5 -0
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-fetch.ts +163 -26
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +85 -61
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +11 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +67 -38
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +61 -7
- package/src/types/config.ts +10 -1
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/src/combos/failover.ts
CHANGED
|
@@ -13,6 +13,28 @@ interface TargetCooldown {
|
|
|
13
13
|
|
|
14
14
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
15
15
|
const MAX_COOLDOWN_MS = 10 * 60_000;
|
|
16
|
+
/** Short cooldown for request-rate 429s (for example provider code 1302) that omit Retry-After. */
|
|
17
|
+
export const COMBO_REQUEST_RATE_COOLDOWN_MS = 5_000;
|
|
18
|
+
|
|
19
|
+
const QUOTA_LIMIT_CODES = new Set([
|
|
20
|
+
"1308",
|
|
21
|
+
"1310",
|
|
22
|
+
"1316",
|
|
23
|
+
"1317",
|
|
24
|
+
"1318",
|
|
25
|
+
"1319",
|
|
26
|
+
"1320",
|
|
27
|
+
"1321",
|
|
28
|
+
"insufficient_quota",
|
|
29
|
+
]);
|
|
30
|
+
const TRANSIENT_REQUEST_RATE_CODES = new Set(["1302", "1305"]);
|
|
31
|
+
const IMF_FIXDATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
|
|
32
|
+
const RFC850_DATE_RE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
|
|
33
|
+
const ASCTIME_DATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/i;
|
|
34
|
+
const HTTP_MONTH_INDEX: Record<string, number> = {
|
|
35
|
+
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
|
|
36
|
+
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
|
|
37
|
+
};
|
|
16
38
|
|
|
17
39
|
/** Map<`${comboId}\0${provider/model}`, TargetCooldown> */
|
|
18
40
|
const targetCooldowns = new Map<string, TargetCooldown>();
|
|
@@ -26,22 +48,92 @@ function cooldownMapKey(
|
|
|
26
48
|
return `${comboId}\0${targetKey(target)}`;
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
function parseUtcDateParts(
|
|
52
|
+
year: number,
|
|
53
|
+
monthName: string,
|
|
54
|
+
day: number,
|
|
55
|
+
hour: number,
|
|
56
|
+
minute: number,
|
|
57
|
+
second: number,
|
|
58
|
+
): number | undefined {
|
|
59
|
+
const month = HTTP_MONTH_INDEX[monthName.toLowerCase()];
|
|
60
|
+
if (month === undefined) return undefined;
|
|
61
|
+
const timestamp = Date.UTC(year, month, day, hour, minute, second);
|
|
62
|
+
const parsed = new Date(timestamp);
|
|
63
|
+
return parsed.getUTCFullYear() === year
|
|
64
|
+
&& parsed.getUTCMonth() === month
|
|
65
|
+
&& parsed.getUTCDate() === day
|
|
66
|
+
&& parsed.getUTCHours() === hour
|
|
67
|
+
&& parsed.getUTCMinutes() === minute
|
|
68
|
+
&& parsed.getUTCSeconds() === second
|
|
69
|
+
? timestamp
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseHttpDate(value: string, now: number): number | undefined {
|
|
74
|
+
const imf = IMF_FIXDATE_RE.exec(value);
|
|
75
|
+
if (imf) {
|
|
76
|
+
return parseUtcDateParts(
|
|
77
|
+
Number(imf[3]), imf[2]!, Number(imf[1]),
|
|
78
|
+
Number(imf[4]), Number(imf[5]), Number(imf[6]),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const rfc850 = RFC850_DATE_RE.exec(value);
|
|
82
|
+
if (rfc850) {
|
|
83
|
+
const current = new Date(now);
|
|
84
|
+
const currentYear = current.getUTCFullYear();
|
|
85
|
+
const month = HTTP_MONTH_INDEX[rfc850[2]!.toLowerCase()];
|
|
86
|
+
if (month === undefined) return undefined;
|
|
87
|
+
let year = Math.floor(currentYear / 100) * 100 + Number(rfc850[3]);
|
|
88
|
+
const yearDelta = year - currentYear;
|
|
89
|
+
const candidateTimeOfYear = Date.UTC(
|
|
90
|
+
2000, month, Number(rfc850[1]),
|
|
91
|
+
Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
|
|
92
|
+
);
|
|
93
|
+
const currentTimeOfYear = Date.UTC(
|
|
94
|
+
2000, current.getUTCMonth(), current.getUTCDate(),
|
|
95
|
+
current.getUTCHours(), current.getUTCMinutes(), current.getUTCSeconds(),
|
|
96
|
+
current.getUTCMilliseconds(),
|
|
97
|
+
);
|
|
98
|
+
if (yearDelta < -50 || (yearDelta === -50 && candidateTimeOfYear < currentTimeOfYear)) {
|
|
99
|
+
year += 100;
|
|
100
|
+
} else if (yearDelta > 50 || (yearDelta === 50 && candidateTimeOfYear > currentTimeOfYear)) {
|
|
101
|
+
year -= 100;
|
|
102
|
+
}
|
|
103
|
+
return parseUtcDateParts(
|
|
104
|
+
year, rfc850[2]!, Number(rfc850[1]),
|
|
105
|
+
Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const asctime = ASCTIME_DATE_RE.exec(value);
|
|
109
|
+
if (!asctime) return undefined;
|
|
110
|
+
return parseUtcDateParts(
|
|
111
|
+
Number(asctime[6]), asctime[1]!, Number(asctime[2]),
|
|
112
|
+
Number(asctime[3]), Number(asctime[4]), Number(asctime[5]),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
29
116
|
export function parseRetryAfterMs(
|
|
30
117
|
value: string | null | undefined,
|
|
31
118
|
now = Date.now(),
|
|
119
|
+
options?: { preserveImmediate?: boolean },
|
|
32
120
|
): number | undefined {
|
|
33
121
|
const text = value?.trim();
|
|
34
122
|
if (!text) return undefined;
|
|
35
123
|
if (/^\d+(?:\.\d+)?$/.test(text)) {
|
|
36
124
|
const seconds = Number(text);
|
|
37
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
Number.isFinite(seconds)
|
|
127
|
+
&& (seconds > 0 || (options?.preserveImmediate && seconds === 0))
|
|
128
|
+
) {
|
|
38
129
|
return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
|
|
39
130
|
}
|
|
40
131
|
}
|
|
41
|
-
const timestamp =
|
|
42
|
-
if (
|
|
132
|
+
const timestamp = parseHttpDate(text, now);
|
|
133
|
+
if (timestamp === undefined) return undefined;
|
|
43
134
|
const delay = timestamp - now;
|
|
44
|
-
|
|
135
|
+
if (delay > 0) return Math.min(delay, MAX_COOLDOWN_MS);
|
|
136
|
+
return options?.preserveImmediate ? 1 : undefined;
|
|
45
137
|
}
|
|
46
138
|
|
|
47
139
|
export function isComboTargetInCooldown(
|
|
@@ -59,10 +151,59 @@ export function isComboTargetInCooldown(
|
|
|
59
151
|
return true;
|
|
60
152
|
}
|
|
61
153
|
|
|
154
|
+
export function isTransientRequestRateLimit(input: {
|
|
155
|
+
status?: number;
|
|
156
|
+
code?: string | null;
|
|
157
|
+
message?: string;
|
|
158
|
+
}): boolean {
|
|
159
|
+
if (isProviderScopedQuotaCap(input.status, input.message ?? "", input.code)) return false;
|
|
160
|
+
const code = (input.code ?? "").trim().toLowerCase().replaceAll("-", "_");
|
|
161
|
+
if (QUOTA_LIMIT_CODES.has(code)) return false;
|
|
162
|
+
if (TRANSIENT_REQUEST_RATE_CODES.has(code)) return true;
|
|
163
|
+
const text = (input.message ?? "").toLowerCase();
|
|
164
|
+
if (
|
|
165
|
+
text.includes("usage limit reached")
|
|
166
|
+
|| text.includes("insufficient_quota")
|
|
167
|
+
|| text.includes("quota exhausted")
|
|
168
|
+
) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return text.includes("rate limit reached for requests");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function remainingComboCooldownMs(comboId: string, now = Date.now()): number | undefined {
|
|
175
|
+
const prefix = `${comboId}\0`;
|
|
176
|
+
let soonest: number | undefined;
|
|
177
|
+
for (const [key, cooldown] of targetCooldowns) {
|
|
178
|
+
if (!key.startsWith(prefix)) continue;
|
|
179
|
+
const remaining = cooldown.cooldownUntil - now;
|
|
180
|
+
if (remaining <= 0) {
|
|
181
|
+
targetCooldowns.delete(key);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (soonest === undefined || remaining < soonest) soonest = remaining;
|
|
185
|
+
}
|
|
186
|
+
return soonest;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now()): string | undefined {
|
|
190
|
+
const remainingMs = remainingComboCooldownMs(comboId, now);
|
|
191
|
+
if (remainingMs === undefined) return undefined;
|
|
192
|
+
return String(Math.max(1, Math.ceil(remainingMs / 1000)));
|
|
193
|
+
}
|
|
194
|
+
|
|
62
195
|
export function coolComboTarget(
|
|
63
196
|
comboId: string,
|
|
64
197
|
target: Pick<OcxComboTarget, "provider" | "model">,
|
|
65
|
-
options?: {
|
|
198
|
+
options?: {
|
|
199
|
+
retryAfter?: string | null;
|
|
200
|
+
now?: number;
|
|
201
|
+
cooldownMs?: number;
|
|
202
|
+
writerGeneration?: number;
|
|
203
|
+
status?: number;
|
|
204
|
+
code?: string | null;
|
|
205
|
+
message?: string;
|
|
206
|
+
},
|
|
66
207
|
): void {
|
|
67
208
|
const now = options?.now ?? Date.now();
|
|
68
209
|
const writerGeneration = options?.writerGeneration ?? captureConfigGeneration();
|
|
@@ -70,7 +211,11 @@ export function coolComboTarget(
|
|
|
70
211
|
if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return;
|
|
71
212
|
const cooldownMs = options?.cooldownMs
|
|
72
213
|
?? parseRetryAfterMs(options?.retryAfter, now)
|
|
73
|
-
??
|
|
214
|
+
?? (isTransientRequestRateLimit({
|
|
215
|
+
status: options?.status,
|
|
216
|
+
code: options?.code,
|
|
217
|
+
message: options?.message,
|
|
218
|
+
}) ? COMBO_REQUEST_RATE_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
|
|
74
219
|
targetCooldowns.set(cooldownMapKey(comboId, target), {
|
|
75
220
|
cooldownUntil: now + Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS),
|
|
76
221
|
});
|
|
@@ -108,6 +253,37 @@ export function clearComboTargetCooldowns(comboId?: string): void {
|
|
|
108
253
|
}
|
|
109
254
|
|
|
110
255
|
export type ComboFailureDecision = "hop" | "stop";
|
|
256
|
+
export type ComboFailureCooldownScope = "target" | "provider";
|
|
257
|
+
|
|
258
|
+
function normalizedFailureCode(code?: string | null): string {
|
|
259
|
+
return code?.trim().toLowerCase().replaceAll("-", "_") ?? "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isProviderScopedQuotaCap(
|
|
263
|
+
status: number | undefined,
|
|
264
|
+
message: string,
|
|
265
|
+
code?: string | null,
|
|
266
|
+
): boolean {
|
|
267
|
+
const normalizedCode = normalizedFailureCode(code);
|
|
268
|
+
const text = message.toLowerCase();
|
|
269
|
+
if (
|
|
270
|
+
status === 429
|
|
271
|
+
&& (normalizedCode === "gousagelimiterror" || text.includes("monthly usage limit reached"))
|
|
272
|
+
) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return normalizedCode === "free_rate_limited"
|
|
276
|
+
|| text.includes("err_free_prompt_cap")
|
|
277
|
+
|| (text.includes("free tier") && text.includes("single request"));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function comboFailureCooldownScope(
|
|
281
|
+
status: number,
|
|
282
|
+
message: string,
|
|
283
|
+
options?: { code?: string | null },
|
|
284
|
+
): ComboFailureCooldownScope {
|
|
285
|
+
return isProviderScopedQuotaCap(status, message, options?.code) ? "provider" : "target";
|
|
286
|
+
}
|
|
111
287
|
|
|
112
288
|
function isModelLifecycleGone(
|
|
113
289
|
status: number,
|
|
@@ -168,6 +344,9 @@ export function comboFailureDecision(
|
|
|
168
344
|
if (options?.code === "input_admission_refused" || error.code === "input_admission_refused") {
|
|
169
345
|
return "hop";
|
|
170
346
|
}
|
|
347
|
+
if (isProviderScopedQuotaCap(status, message, options?.code || error.code)) {
|
|
348
|
+
return "hop";
|
|
349
|
+
}
|
|
171
350
|
if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
|
|
172
351
|
return "stop";
|
|
173
352
|
}
|
package/src/combos/index.ts
CHANGED
|
@@ -31,11 +31,17 @@ export {
|
|
|
31
31
|
} from "./resolve";
|
|
32
32
|
export {
|
|
33
33
|
clearComboTargetCooldowns,
|
|
34
|
+
comboCooldownRetryAfterSeconds,
|
|
35
|
+
COMBO_REQUEST_RATE_COOLDOWN_MS,
|
|
34
36
|
coolComboTarget,
|
|
35
37
|
isComboTargetInCooldown,
|
|
38
|
+
isTransientRequestRateLimit,
|
|
36
39
|
parseRetryAfterMs,
|
|
40
|
+
remainingComboCooldownMs,
|
|
37
41
|
comboFailureDecision,
|
|
42
|
+
comboFailureCooldownScope,
|
|
38
43
|
type ComboFailureDecision,
|
|
44
|
+
type ComboFailureCooldownScope,
|
|
39
45
|
} from "./failover";
|
|
40
46
|
export {
|
|
41
47
|
comboIdFromRawBody,
|
package/src/combos/resolve.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxComboTarget, OcxConfig } from "../types";
|
|
2
2
|
import { getCachedProviderQuota } from "../providers/quota-routing-cache";
|
|
3
|
-
import {
|
|
3
|
+
import type { ProviderQuota } from "../providers/quota-types";
|
|
4
|
+
import { coolComboTarget, isComboTargetInCooldown, type ComboFailureCooldownScope } from "./failover";
|
|
4
5
|
import { quotaResetRemainingMs } from "./reset-window";
|
|
5
6
|
import { getCombo, resolveComboId, targetKey } from "./types";
|
|
6
7
|
import type { NormalizedComboConfig } from "./types";
|
|
@@ -58,6 +59,28 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget): bool
|
|
|
58
59
|
&& config.providers[target.provider]?.disabled !== true;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
|
|
63
|
+
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 100) return false;
|
|
64
|
+
return typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt > now;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function cachedProviderQuotaIsExhausted(
|
|
68
|
+
quota: ProviderQuota | null,
|
|
69
|
+
now = Date.now(),
|
|
70
|
+
): boolean {
|
|
71
|
+
if (!quota) return false;
|
|
72
|
+
if (quotaWindowExhausted(quota.fiveHourPercent, quota.fiveHourResetAt, now)) return true;
|
|
73
|
+
if (quotaWindowExhausted(quota.weeklyPercent, quota.weeklyResetAt, now)) return true;
|
|
74
|
+
if (quotaWindowExhausted(quota.monthlyPercent, quota.monthlyResetAt, now)) return true;
|
|
75
|
+
if (quota.customWindows?.some(window => quotaWindowExhausted(window.percent, window.resetAt, now))) return true;
|
|
76
|
+
if (quota.creditsUsd?.unlimited !== true
|
|
77
|
+
&& typeof quota.creditsUsd?.percent === "number"
|
|
78
|
+
&& Number.isFinite(quota.creditsUsd.percent)
|
|
79
|
+
&& quota.creditsUsd.percent >= 100
|
|
80
|
+
&& quota.creditsUsd.remaining <= 0) return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
function smoothWeightedIndex(
|
|
62
85
|
targets: Required<OcxComboTarget>[],
|
|
63
86
|
state: SelectionState,
|
|
@@ -121,14 +144,17 @@ export function pickComboTarget(
|
|
|
121
144
|
options: {
|
|
122
145
|
exclude?: Iterable<string>;
|
|
123
146
|
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
147
|
+
now?: number;
|
|
124
148
|
} = {},
|
|
125
149
|
): ComboPick | null {
|
|
126
150
|
const writerGeneration = captureConfigGeneration();
|
|
127
151
|
const combo = getCombo(config, comboId);
|
|
128
152
|
if (!combo) throw new UnknownComboError(comboId);
|
|
129
153
|
const excluded = new Set(options.exclude ?? []);
|
|
154
|
+
const now = options.now ?? Date.now();
|
|
130
155
|
const eligible = (target: Required<OcxComboTarget>): boolean =>
|
|
131
156
|
targetProviderIsUsable(config, target)
|
|
157
|
+
&& !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
|
|
132
158
|
&& !excluded.has(targetKey(target))
|
|
133
159
|
&& (options.eligible?.(target) ?? true);
|
|
134
160
|
|
|
@@ -187,7 +213,7 @@ export function pickComboTarget(
|
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
215
|
} else if (combo.strategy === "reset-window") {
|
|
190
|
-
targetIndex = resetWindowIndex(combo.targets, eligible);
|
|
216
|
+
targetIndex = resetWindowIndex(combo.targets, eligible, now);
|
|
191
217
|
} else {
|
|
192
218
|
targetIndex = combo.targets.findIndex(eligible);
|
|
193
219
|
}
|
|
@@ -250,15 +276,26 @@ export function advanceComboAfterFailure(
|
|
|
250
276
|
retryAfter?: string | null;
|
|
251
277
|
now?: number;
|
|
252
278
|
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
279
|
+
cooldownScope?: ComboFailureCooldownScope;
|
|
280
|
+
status?: number;
|
|
281
|
+
code?: string | null;
|
|
282
|
+
message?: string;
|
|
253
283
|
} = {},
|
|
254
284
|
): ComboPick | null {
|
|
255
285
|
noteComboFailure(pick.comboId, pick.target, pick.writerGeneration);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
const combo = getCombo(config, pick.comboId);
|
|
287
|
+
const cooldownTargets = options.cooldownScope === "provider" && combo
|
|
288
|
+
? combo.targets.filter(target => target.provider === pick.target.provider)
|
|
289
|
+
: [pick.target];
|
|
290
|
+
for (const target of cooldownTargets) {
|
|
291
|
+
coolComboTarget(pick.comboId, target, {
|
|
292
|
+
...options,
|
|
293
|
+
writerGeneration: pick.writerGeneration,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
260
296
|
return pickComboTarget(config, pick.comboId, {
|
|
261
297
|
exclude: pick.attempted,
|
|
298
|
+
now: options.now,
|
|
262
299
|
eligible: target => !isComboTargetInCooldown(pick.comboId, target, options.now)
|
|
263
300
|
&& (options.eligible?.(target) ?? true),
|
|
264
301
|
});
|
package/src/config.ts
CHANGED
|
@@ -1010,7 +1010,9 @@ const configSchema = z.object({
|
|
|
1010
1010
|
// A malformed present client block must remain diagnosable from raw config and
|
|
1011
1011
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
1012
1012
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
1013
|
-
managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024)
|
|
1013
|
+
managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024).describe(
|
|
1014
|
+
"Deprecated compatibility limit for bounded legacy usage readers; GET /api/usage always aggregates the complete ledger",
|
|
1015
|
+
),
|
|
1014
1016
|
// Invalid hand edits disable only this opt-in circuit. Live writes remain strict.
|
|
1015
1017
|
upstreamHostCircuitThreshold: z.number().int()
|
|
1016
1018
|
.min(0)
|
|
@@ -1046,6 +1048,8 @@ const configSchema = z.object({
|
|
|
1046
1048
|
providers: z.record(z.string(), providerConfigSchema),
|
|
1047
1049
|
defaultProvider: z.string().min(1).default("openai"),
|
|
1048
1050
|
defaultModelAliases: z.boolean().optional(),
|
|
1051
|
+
// Malformed hand edits disable this opt-in projection without rejecting providers.
|
|
1052
|
+
cursorEffortRows: z.boolean().optional().catch(false),
|
|
1049
1053
|
// Future versions remain opaque through passthrough-compatible whole-config saves.
|
|
1050
1054
|
// Only version 1 grants deletion authority in the rebase path.
|
|
1051
1055
|
configRebaseProvenance: z.unknown().optional(),
|