@bitkyc08/opencodex 2.7.41 → 2.7.42
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-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
package/src/grok/inject.ts
CHANGED
|
@@ -77,6 +77,14 @@ const MODEL_TABLE_HEADER = new RegExp(
|
|
|
77
77
|
"gm",
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* ANY table header, capturing its full dotted key. Used to compute table SPANS: a table
|
|
82
|
+
* body runs from its own header to the next header of any kind, so the orphan sweep can
|
|
83
|
+
* remove a whole table instead of a guessed line range (a partial removal would re-parent
|
|
84
|
+
* the leftover keys onto the preceding table).
|
|
85
|
+
*/
|
|
86
|
+
const ANY_TABLE_HEADER = /^[ \t]*\[\[?[ \t]*([^\]\r\n]*?)[ \t]*\]\]?[ \t]*(?:#.*)?$/gm;
|
|
87
|
+
|
|
80
88
|
/** Resolve a header key segment (bare / basic / literal) to the key it actually addresses. */
|
|
81
89
|
function canonicalKeySegment(raw: string): string {
|
|
82
90
|
if (raw.startsWith('"')) return decodeTomlBasicString(raw.slice(1, -1));
|
|
@@ -102,6 +110,143 @@ function userModelAliases(content: string, region: ManagedRegion | null): Set<st
|
|
|
102
110
|
return aliases;
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The api_key literal every generated entry carries. It is the STRONG ownership signal:
|
|
115
|
+
* a value we mint, that a human has no reason to type by hand.
|
|
116
|
+
*/
|
|
117
|
+
const OPENCODEX_API_KEY = "opencodex-loopback";
|
|
118
|
+
|
|
119
|
+
/** A plain `[model.<alias>]` table outside the fence that opencodex itself wrote. */
|
|
120
|
+
interface OrphanTable {
|
|
121
|
+
alias: string;
|
|
122
|
+
/** The model id this entry routes to — used to find its replacement alias. */
|
|
123
|
+
modelId: string | undefined;
|
|
124
|
+
/** Offsets into the NORMALIZED content: header start .. next header start (or EOF). */
|
|
125
|
+
start: number;
|
|
126
|
+
end: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `key = "value"` / `key = value` pairs at the top level of one table body. */
|
|
130
|
+
function tableBodyKeys(body: string): Map<string, string> {
|
|
131
|
+
const keys = new Map<string, string>();
|
|
132
|
+
for (const line of body.split("\n")) {
|
|
133
|
+
const match = /^[ \t]*([A-Za-z0-9_-]+)[ \t]*=[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
134
|
+
if (!match) continue;
|
|
135
|
+
const raw = match[2]!;
|
|
136
|
+
const value = raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2
|
|
137
|
+
? decodeTomlBasicString(raw.slice(1, -1))
|
|
138
|
+
: raw;
|
|
139
|
+
if (!keys.has(match[1]!)) keys.set(match[1]!, value);
|
|
140
|
+
}
|
|
141
|
+
return keys;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Is this base_url pointed at the local machine? */
|
|
145
|
+
function isLoopbackBaseUrl(value: string | undefined): boolean {
|
|
146
|
+
if (!value) return false;
|
|
147
|
+
try {
|
|
148
|
+
return isLoopbackHostname(new URL(value).hostname);
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Model tables OUTSIDE the fence that opencodex itself wrote (#511).
|
|
156
|
+
*
|
|
157
|
+
* These are entries from a version that predates the markers (or predates
|
|
158
|
+
* `context_window`). Because they sit outside the fence, `userModelAliases` reserves
|
|
159
|
+
* their aliases as user-owned and the writer routes around them with a `-2` suffix — so
|
|
160
|
+
* every sync adds a CORRECT duplicate and never removes the stale original. Grok then
|
|
161
|
+
* resolves the original, finds no `context_window`, and falls back to its own 200k.
|
|
162
|
+
*
|
|
163
|
+
* Ownership is CONJUNCTIVE and deliberately strict, because a false positive deletes a
|
|
164
|
+
* hand-written user model:
|
|
165
|
+
* - a plain `[model.x]` header (never `[[model.x]]` / `[model.x.sub]` — those spellings
|
|
166
|
+
* mark human authorship and stay reserved);
|
|
167
|
+
* - `api_key` equal to our own literal;
|
|
168
|
+
* - a loopback `base_url`, so an entry that merely copied our key while pointing at a
|
|
169
|
+
* remote host is left alone.
|
|
170
|
+
* A loopback base_url ALONE is not enough: aiming your own model at the local proxy is a
|
|
171
|
+
* legitimate thing to do.
|
|
172
|
+
*/
|
|
173
|
+
function findOpencodexOrphans(content: string, region: ManagedRegion | null): OrphanTable[] {
|
|
174
|
+
const orphans: OrphanTable[] = [];
|
|
175
|
+
// A pre-fence orphan's body must stop AT the fence. The managed block opens with a
|
|
176
|
+
// COMMENT, not a table header, so a span that runs to "the next table header" swallows
|
|
177
|
+
// the BEGIN marker whenever no other table separates them — and removing the orphan then
|
|
178
|
+
// deletes the fence opener itself, which strands the END marker and makes every later
|
|
179
|
+
// sync re-append a block (the #511 duplicate loop, one layer down).
|
|
180
|
+
// `region` is null ONLY when BEGIN_MARKER is absent (see findManagedRegion), so -1
|
|
181
|
+
// disables the clamp for marker-less files without a redundant scan.
|
|
182
|
+
const fenceStart = region ? region.start : -1;
|
|
183
|
+
const clampEnd = (start: number, end: number): number =>
|
|
184
|
+
fenceStart >= 0 && start < fenceStart ? Math.min(end, fenceStart) : end;
|
|
185
|
+
// Collect every table header first: a table body runs to the NEXT header, whatever it is.
|
|
186
|
+
const headers: Array<{ index: number; length: number; segments: string[]; array: boolean }> = [];
|
|
187
|
+
for (const match of content.matchAll(ANY_TABLE_HEADER)) {
|
|
188
|
+
headers.push({
|
|
189
|
+
index: match.index!,
|
|
190
|
+
length: match[0].length,
|
|
191
|
+
segments: match[1]!.split(".").map(part => canonicalKeySegment(part.trim())),
|
|
192
|
+
array: match[0].trimStart().startsWith("[["),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (const [position, header] of headers.entries()) {
|
|
196
|
+
if (header.array || header.segments.length !== 2 || header.segments[0] !== "model") continue;
|
|
197
|
+
// Inside the fence the regular splice already owns it.
|
|
198
|
+
if (region && header.index >= region.start && header.index < region.end) continue;
|
|
199
|
+
const bodyEnd = clampEnd(header.index, headers[position + 1]?.index ?? content.length);
|
|
200
|
+
const keys = tableBodyKeys(content.slice(header.index + header.length, bodyEnd));
|
|
201
|
+
if (keys.get("api_key") !== OPENCODEX_API_KEY) continue;
|
|
202
|
+
if (!isLoopbackBaseUrl(keys.get("base_url"))) continue;
|
|
203
|
+
// Swallow the entry's OWN sub-tables (`[model.<alias>.extra_headers]`). Grok writes
|
|
204
|
+
// them when it re-serializes the file, and leaving one behind keeps the alias
|
|
205
|
+
// reserved by `userModelAliases` — so the sweep would remove the parent and STILL
|
|
206
|
+
// allocate a suffixed duplicate, which is the exact #511 loop we came to close.
|
|
207
|
+
let end = bodyEnd;
|
|
208
|
+
for (let next = position + 1; next < headers.length; next += 1) {
|
|
209
|
+
const child = headers[next]!;
|
|
210
|
+
// Only a PRE-fence parent may be cut short by the fence. Without the parent test a
|
|
211
|
+
// below-fence orphan would break on its first child (every index is past the fence),
|
|
212
|
+
// leaving the sub-table behind to keep the alias reserved — the -2 loop again.
|
|
213
|
+
if (fenceStart >= 0 && header.index < fenceStart && child.index >= fenceStart) break;
|
|
214
|
+
if (child.segments.length <= 2) break;
|
|
215
|
+
if (child.segments[0] !== "model" || child.segments[1] !== header.segments[1]) break;
|
|
216
|
+
end = clampEnd(header.index, headers[next + 1]?.index ?? content.length);
|
|
217
|
+
}
|
|
218
|
+
orphans.push({ alias: header.segments[1]!, modelId: keys.get("model"), start: header.index, end });
|
|
219
|
+
}
|
|
220
|
+
return orphans;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Remove whole tables, back to front so earlier offsets stay valid. */
|
|
224
|
+
function removeOrphanTables(content: string, orphans: OrphanTable[]): string {
|
|
225
|
+
let next = content;
|
|
226
|
+
for (const orphan of [...orphans].sort((a, b) => b.start - a.start)) {
|
|
227
|
+
next = next.slice(0, orphan.start) + next.slice(orphan.end);
|
|
228
|
+
}
|
|
229
|
+
return next;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Repoint `default` / `fork_secondary_model` at the alias that survived.
|
|
234
|
+
*
|
|
235
|
+
* Removing an adopted orphan that `[models] default` names would leave Grok pointing at
|
|
236
|
+
* a model that no longer exists — and on a real machine `default` DOES name one, so this
|
|
237
|
+
* is the common path rather than an edge case.
|
|
238
|
+
*/
|
|
239
|
+
function rewriteAliasReferences(content: string, renames: Map<string, string>): string {
|
|
240
|
+
if (renames.size === 0) return content;
|
|
241
|
+
return content.replace(
|
|
242
|
+
/^([ \t]*(?:default|fork_secondary_model)[ \t]*=[ \t]*")([^"]*)(")/gm,
|
|
243
|
+
(whole, prefix: string, value: string, suffix: string) => {
|
|
244
|
+
const replacement = renames.get(value);
|
|
245
|
+
return replacement ? `${prefix}${replacement}${suffix}` : whole;
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
105
250
|
function orphanedMarkerResult(action: string): GrokInjectResult {
|
|
106
251
|
return {
|
|
107
252
|
ok: false,
|
|
@@ -230,9 +375,20 @@ export function injectGrokConfig(
|
|
|
230
375
|
const configExisted = existsSync(configPath);
|
|
231
376
|
const rawContent = configExisted ? readFileSync(configPath, "utf8") : "";
|
|
232
377
|
const eol = dominantEol(rawContent);
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
378
|
+
const originalContent = applyEol(rawContent, "\n");
|
|
379
|
+
const originalRegion = findManagedRegion(originalContent);
|
|
380
|
+
// Ambiguous fence: refuse before the sweep, or "outside the region" could mean the
|
|
381
|
+
// entire file.
|
|
382
|
+
if (originalRegion?.orphaned) return orphanedMarkerResult("injection");
|
|
383
|
+
|
|
384
|
+
// Adopt our own pre-fence entries (#511) BEFORE reserving user aliases, so the stale
|
|
385
|
+
// duplicate is replaced instead of routed around forever. Runs inside the normalized
|
|
386
|
+
// window so the user's dominant EOL is still restored below.
|
|
387
|
+
const orphans = findOpencodexOrphans(originalContent, originalRegion);
|
|
388
|
+
const content = removeOrphanTables(originalContent, orphans);
|
|
389
|
+
// Removing bytes above the fence MOVES it: recompute rather than adjust arithmetic,
|
|
390
|
+
// so the splice below cannot cut the file in the wrong place.
|
|
391
|
+
const region = orphans.length > 0 ? findManagedRegion(content) : originalRegion;
|
|
236
392
|
|
|
237
393
|
const block = buildGrokManagedBlock(port, models, opts.hostname, userModelAliases(content, region), opts.excluded);
|
|
238
394
|
let nextContent: string;
|
|
@@ -248,11 +404,35 @@ export function injectGrokConfig(
|
|
|
248
404
|
nextContent = `${content}\n${block}\n`;
|
|
249
405
|
}
|
|
250
406
|
|
|
407
|
+
// Repoint `default` / `fork_secondary_model` at whichever alias survived. A removed
|
|
408
|
+
// model with no replacement keeps its reference untouched — a stale name in a working
|
|
409
|
+
// file beats a dangling one.
|
|
410
|
+
if (orphans.length > 0) {
|
|
411
|
+
const survivors = new Map<string, string>();
|
|
412
|
+
for (const match of nextContent.matchAll(MODEL_TABLE_HEADER)) {
|
|
413
|
+
if (canonicalKeySegment(match[1]!) !== "model") continue;
|
|
414
|
+
const alias = canonicalKeySegment(match[2]!);
|
|
415
|
+
const body = nextContent.slice(match.index! + match[0].length);
|
|
416
|
+
const modelId = tableBodyKeys(body.slice(0, body.search(/^[ \t]*\[/m) + 1 || body.length)).get("model");
|
|
417
|
+
if (modelId !== undefined && !survivors.has(modelId)) survivors.set(modelId, alias);
|
|
418
|
+
}
|
|
419
|
+
const renames = new Map<string, string>();
|
|
420
|
+
for (const orphan of orphans) {
|
|
421
|
+
const replacement = orphan.modelId === undefined ? undefined : survivors.get(orphan.modelId);
|
|
422
|
+
if (replacement && replacement !== orphan.alias) renames.set(orphan.alias, replacement);
|
|
423
|
+
}
|
|
424
|
+
nextContent = rewriteAliasReferences(nextContent, renames);
|
|
425
|
+
}
|
|
426
|
+
|
|
251
427
|
const output = applyEol(nextContent, eol);
|
|
252
428
|
if (output === rawContent) {
|
|
253
429
|
return { ok: true, changed: false, message: "Grok config already contains the current opencodex managed block." };
|
|
254
430
|
}
|
|
255
|
-
|
|
431
|
+
// Back up before a first-time fence write AND before any sweep, since adopting an orphan
|
|
432
|
+
// deletes a table the user has in their file. Previously the adjacent-orphan layout got a
|
|
433
|
+
// backup only as a side effect of the fence being destroyed (which made `region` falsy);
|
|
434
|
+
// preserving the fence must not silently drop that safety net.
|
|
435
|
+
if (configExisted && (!region || orphans.length > 0)) copyBackupOnce(configPath, backupPath);
|
|
256
436
|
atomicWriteFile(configPath, output);
|
|
257
437
|
return {
|
|
258
438
|
ok: true,
|
package/src/grok/status.ts
CHANGED
|
@@ -86,3 +86,36 @@ export function readGrokStatus(opts: { grokHome?: string } = {}): GrokStatus {
|
|
|
86
86
|
|
|
87
87
|
return { configPath, present: true, baseUrl, models: models.filter(model => model.id) };
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Does the fence point somewhere the running proxy is NOT?
|
|
92
|
+
*
|
|
93
|
+
* Grok resolves its model at request time and retries a refused connection up to 15
|
|
94
|
+
* times, and those retries are invisible to us: nothing reaches the proxy, so nothing
|
|
95
|
+
* lands in our log. The user sees a correct context window (the stale entry carries it)
|
|
96
|
+
* and an endless "Retrying (attempt N/15)". That is a silent failure unless someone
|
|
97
|
+
* compares the fence's port against the port we actually bound — which is exactly what
|
|
98
|
+
* `ocx status` is for.
|
|
99
|
+
*
|
|
100
|
+
* Returns null when there is nothing to say: no fence, an unparsable endpoint, or a
|
|
101
|
+
* fence that already agrees with the live listener.
|
|
102
|
+
*/
|
|
103
|
+
export function grokFenceEndpointDrift(
|
|
104
|
+
status: Pick<GrokStatus, "present" | "baseUrl">,
|
|
105
|
+
livePort: number | undefined,
|
|
106
|
+
): { fencePort: number; livePort: number } | null {
|
|
107
|
+
if (!status.present || !status.baseUrl) return null;
|
|
108
|
+
if (typeof livePort !== "number" || !Number.isFinite(livePort) || livePort <= 0) return null;
|
|
109
|
+
let fencePort: number;
|
|
110
|
+
try {
|
|
111
|
+
const url = new URL(status.baseUrl);
|
|
112
|
+
// An explicit port is the only thing we write, so an empty one means a shape we did
|
|
113
|
+
// not produce; stay quiet rather than guess a default.
|
|
114
|
+
if (!url.port) return null;
|
|
115
|
+
fencePort = Number(url.port);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isFinite(fencePort) || fencePort === livePort) return null;
|
|
120
|
+
return { fencePort, livePort };
|
|
121
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseRetryAfterMs } from "../combos";
|
|
2
|
+
import { classifyError, parseRetryAfterFromMessage } from "./errors";
|
|
3
|
+
|
|
4
|
+
/** Small default when a retryable 429 has no upstream Retry-After (#507). */
|
|
5
|
+
export const DEFAULT_RETRYABLE_429_RETRY_AFTER_SEC = "2";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate a raw upstream Retry-After for client exposure.
|
|
9
|
+
* Instant-retry `"0"` is kept even though cooldown parsers reject it.
|
|
10
|
+
*/
|
|
11
|
+
export function validateClientRetryAfterHeader(
|
|
12
|
+
value: string | null | undefined,
|
|
13
|
+
now: number,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const trimmed = value?.trim();
|
|
16
|
+
if (!trimmed || trimmed.length > 128) return undefined;
|
|
17
|
+
// Instant-retry "0" is a valid client directive but rejected by cooldown parsers
|
|
18
|
+
// (parseRetryAfterMs requires seconds > 0). Preserve it for client headers.
|
|
19
|
+
if (trimmed === "0") return trimmed;
|
|
20
|
+
return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pick a Retry-After value that clients (especially Codex) can back off on.
|
|
25
|
+
*
|
|
26
|
+
* Order: validated upstream header → delay embedded in the error message →
|
|
27
|
+
* small default for retryable rate-limit 429s. Quota-exhausted 429s get no
|
|
28
|
+
* default — hammering a spent quota is not a backoff problem.
|
|
29
|
+
*
|
|
30
|
+
* Pass `includeDefault: false` when the value feeds cooldown/failover metadata:
|
|
31
|
+
* the synthetic client fallback must not shorten combo cooldowns from 60s to 2s.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveClientRetryAfter(opts: {
|
|
34
|
+
status: number;
|
|
35
|
+
message?: string;
|
|
36
|
+
upstreamRetryAfter?: string | null;
|
|
37
|
+
now?: number;
|
|
38
|
+
includeDefault?: boolean;
|
|
39
|
+
}): string | undefined {
|
|
40
|
+
const now = opts.now ?? Date.now();
|
|
41
|
+
const fromHeader = validateClientRetryAfterHeader(opts.upstreamRetryAfter, now);
|
|
42
|
+
if (fromHeader) return fromHeader;
|
|
43
|
+
|
|
44
|
+
const message = opts.message ?? "";
|
|
45
|
+
const fromMessage = parseRetryAfterFromMessage(message);
|
|
46
|
+
if (fromMessage !== undefined) return String(fromMessage);
|
|
47
|
+
|
|
48
|
+
if (opts.includeDefault === false) return undefined;
|
|
49
|
+
if (opts.status !== 429) return undefined;
|
|
50
|
+
const classified = classifyError(429, "rate_limit_error", message || "Too Many Requests");
|
|
51
|
+
if (classified.type === "insufficient_quota" || classified.code === "insufficient_quota") {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return DEFAULT_RETRYABLE_429_RETRY_AFTER_SEC;
|
|
55
|
+
}
|