@bitkyc08/opencodex 2.7.36 → 2.7.37
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.ja.md +8 -1
- package/README.ko.md +7 -1
- package/README.md +7 -1
- package/README.ru.md +7 -1
- package/README.zh-CN.md +7 -1
- package/gui/dist/assets/index-BhUTxmCy.js +52 -0
- package/gui/dist/assets/index-oOZcqVmj.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +7 -0
- package/src/adapters/cursor/message-mapper.ts +3 -0
- package/src/adapters/cursor/protobuf-request.ts +223 -27
- package/src/adapters/cursor/request-builder.ts +41 -15
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +3 -1
- package/src/adapters/cursor.ts +44 -9
- package/src/adapters/google.ts +115 -62
- package/src/adapters/kiro.ts +3 -17
- package/src/adapters/openai-chat.ts +16 -5
- package/src/adapters/openai-responses.ts +56 -1
- package/src/adapters/run-turn-queue.ts +11 -1
- package/src/bridge.ts +139 -69
- package/src/chat/outbound.ts +135 -73
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/index.ts +17 -3
- package/src/cli/status.ts +80 -0
- package/src/cli/v2.ts +14 -2
- package/src/codex/auth-context.ts +18 -2
- package/src/codex/catalog/bundled.ts +83 -27
- package/src/codex/catalog/effort.ts +95 -3
- package/src/codex/catalog/parsing.ts +17 -0
- package/src/codex/catalog/provider-fetch.ts +31 -8
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/model-cache.ts +44 -0
- package/src/codex/runtime.ts +529 -0
- package/src/codex/shim.ts +608 -10
- package/src/combos/resolve.ts +7 -2
- package/src/config.ts +32 -1
- package/src/lib/bun-stream-caps.ts +88 -0
- package/src/lib/crash-guard.ts +3 -1
- package/src/lib/sse-decoder.ts +25 -6
- package/src/responses/parser.ts +2 -1
- package/src/responses/state.ts +10 -2
- package/src/server/auth-cors.ts +4 -1
- package/src/server/index.ts +191 -1
- package/src/server/live.ts +491 -0
- package/src/server/management/config-routes.ts +79 -3
- package/src/server/management/provider-routes.ts +2 -0
- package/src/server/management/shared.ts +6 -6
- package/src/server/management/system-routes.ts +65 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/memory-watchdog.ts +112 -0
- package/src/server/relay-eager.ts +199 -0
- package/src/server/relay.ts +131 -81
- package/src/server/responses/collaboration.ts +20 -3
- package/src/server/responses/core.ts +236 -21
- package/src/server/responses/encrypted-payload.ts +118 -41
- package/src/server/ws-bridge.ts +7 -0
- package/src/types.ts +25 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +19 -0
- package/src/usage/summary.ts +11 -8
- package/gui/dist/assets/index-BpX-hoSd.css +0 -1
- package/gui/dist/assets/index-ZmFopEYw.js +0 -52
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "../../combos";
|
|
28
28
|
import type { NormalizedComboConfig } from "../../combos/types";
|
|
29
29
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
30
|
-
import { redactSecretString } from "../../lib/redact";
|
|
30
|
+
import { redactSecretString, redactUserPath } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
32
|
|
|
33
33
|
|
|
@@ -37,6 +37,14 @@ import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
|
|
|
37
37
|
import { loadBundledCodexCatalog } from "./bundled";
|
|
38
38
|
import type { BundledCatalogDeps } from "./bundled";
|
|
39
39
|
import { deriveEntry } from "./sync";
|
|
40
|
+
import {
|
|
41
|
+
formatClampLogLines,
|
|
42
|
+
formatRuntimeLogLine,
|
|
43
|
+
displayCodexRuntimePath,
|
|
44
|
+
persistEffortClamp,
|
|
45
|
+
resolveAndPersistCodexRuntime,
|
|
46
|
+
type EffortClampDiagnostic,
|
|
47
|
+
} from "../runtime";
|
|
40
48
|
|
|
41
49
|
export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
|
|
42
50
|
if (!effort || (effort !== "max" && effort !== "ultra")) return null;
|
|
@@ -257,7 +265,91 @@ export function clampEntryToCodexSupportedEfforts(entry: RawEntry, supported: Se
|
|
|
257
265
|
|
|
258
266
|
export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: BundledCatalogDeps = {}): RawEntry[] {
|
|
259
267
|
const supported = codexSupportedReasoningEfforts(deps);
|
|
260
|
-
if (!supported)
|
|
261
|
-
|
|
268
|
+
if (!supported) {
|
|
269
|
+
if (!deps.commandCandidates) persistEffortClamp(null, { configDir: deps.configDir });
|
|
270
|
+
return models;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const removed = new Set<string>();
|
|
274
|
+
const affected: string[] = [];
|
|
275
|
+
for (const entry of models) {
|
|
276
|
+
const before = new Set(
|
|
277
|
+
(Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
|
|
278
|
+
.flatMap(level => typeof (level as { effort?: string })?.effort === "string"
|
|
279
|
+
? [(level as { effort: string }).effort]
|
|
280
|
+
: []),
|
|
281
|
+
);
|
|
282
|
+
const beforeDefault = typeof entry.default_reasoning_level === "string"
|
|
283
|
+
? entry.default_reasoning_level
|
|
284
|
+
: null;
|
|
285
|
+
clampEntryToCodexSupportedEfforts(entry, supported);
|
|
286
|
+
const after = new Set(
|
|
287
|
+
(Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
|
|
288
|
+
.flatMap(level => typeof (level as { effort?: string })?.effort === "string"
|
|
289
|
+
? [(level as { effort: string }).effort]
|
|
290
|
+
: []),
|
|
291
|
+
);
|
|
292
|
+
const afterDefault = typeof entry.default_reasoning_level === "string"
|
|
293
|
+
? entry.default_reasoning_level
|
|
294
|
+
: null;
|
|
295
|
+
const lost = [...before].filter(effort => !after.has(effort));
|
|
296
|
+
const defaultClamped = Boolean(beforeDefault && beforeDefault !== afterDefault);
|
|
297
|
+
if (lost.length > 0 || defaultClamped) {
|
|
298
|
+
for (const effort of lost) removed.add(effort);
|
|
299
|
+
if (defaultClamped && beforeDefault) removed.add(beforeDefault);
|
|
300
|
+
if (typeof entry.slug === "string") affected.push(entry.slug);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let runtimePath = "codex";
|
|
305
|
+
let runtimeVersion: string | null = null;
|
|
306
|
+
if (!deps.commandCandidates) {
|
|
307
|
+
try {
|
|
308
|
+
const resolved = resolveAndPersistCodexRuntime({
|
|
309
|
+
execFileSync: deps.execFileSync,
|
|
310
|
+
configDir: deps.configDir,
|
|
311
|
+
env: deps.env,
|
|
312
|
+
platform: deps.platform,
|
|
313
|
+
existsSync: deps.existsSync,
|
|
314
|
+
readFileSync: deps.readFileSync,
|
|
315
|
+
now: deps.now,
|
|
316
|
+
discoverAlternatives: deps.discoverAlternatives,
|
|
317
|
+
});
|
|
318
|
+
runtimePath = resolved.runtime.command;
|
|
319
|
+
runtimeVersion = resolved.runtime.version;
|
|
320
|
+
process.stderr.write(`${formatRuntimeLogLine(resolved.runtime)}\n`);
|
|
321
|
+
if (resolved.persistError) {
|
|
322
|
+
console.warn(`[opencodex] Codex runtime selection could not be persisted; a later sync may pick a different binary.`);
|
|
323
|
+
}
|
|
324
|
+
if (
|
|
325
|
+
resolved.replacedConfigured
|
|
326
|
+
&& resolved.replacedConfigured.from.command !== resolved.runtime.command
|
|
327
|
+
) {
|
|
328
|
+
console.warn(`[opencodex] Preferred Codex runtime is unavailable.`);
|
|
329
|
+
console.warn(
|
|
330
|
+
`[opencodex] Falling back from ${displayCodexRuntimePath(resolved.replacedConfigured.from.command)} to ${displayCodexRuntimePath(runtimePath)}.`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
335
|
+
const redacted = redactUserPath(redactSecretString(message)).slice(0, 200);
|
|
336
|
+
console.warn(`[opencodex] Codex runtime resolve failed during catalog clamp: ${redacted}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (removed.size > 0) {
|
|
341
|
+
const diagnostic: EffortClampDiagnostic = {
|
|
342
|
+
runtimePath,
|
|
343
|
+
runtimeVersion,
|
|
344
|
+
removedEfforts: [...removed].sort(),
|
|
345
|
+
affectedModels: affected,
|
|
346
|
+
};
|
|
347
|
+
for (const line of formatClampLogLines(diagnostic)) console.warn(line);
|
|
348
|
+
if (!deps.commandCandidates) persistEffortClamp(diagnostic, { configDir: deps.configDir });
|
|
349
|
+
deps.onEffortClamp?.(diagnostic);
|
|
350
|
+
} else if (!deps.commandCandidates) {
|
|
351
|
+
persistEffortClamp(null, { configDir: deps.configDir });
|
|
352
|
+
}
|
|
353
|
+
|
|
262
354
|
return models;
|
|
263
355
|
}
|
|
@@ -179,7 +179,24 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
|
|
|
179
179
|
) ?? null;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Native OpenAI slugs that do NOT support the Fast (priority) service tier.
|
|
184
|
+
* Upstream may advertise service_tiers for these models, but the tier is not
|
|
185
|
+
* actually available — strip it so the Codex UI does not offer a dead toggle.
|
|
186
|
+
*/
|
|
187
|
+
const NO_FAST_TIER_NATIVE_SLUGS = new Set([
|
|
188
|
+
"gpt-5.3-codex-spark",
|
|
189
|
+
]);
|
|
190
|
+
|
|
182
191
|
export function normalizeServiceTiers(entry: RawEntry): RawEntry {
|
|
192
|
+
// Strip service tiers for models that do not actually support the Fast tier.
|
|
193
|
+
if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) {
|
|
194
|
+
delete entry.service_tier;
|
|
195
|
+
delete entry.service_tiers;
|
|
196
|
+
delete entry.default_service_tier;
|
|
197
|
+
delete entry.additional_speed_tiers;
|
|
198
|
+
return entry;
|
|
199
|
+
}
|
|
183
200
|
// Codex stores the user-facing config spelling as "fast", but the catalog/request
|
|
184
201
|
// service tier id is "priority" in current codex-rs. Keep legacy catalogs working.
|
|
185
202
|
if (entry.service_tier === "fast") entry.service_tier = "priority";
|
|
@@ -4,7 +4,19 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from
|
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
6
6
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearModelCache,
|
|
9
|
+
clearProviderDiscoveryStatus,
|
|
10
|
+
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
11
|
+
getFreshCached,
|
|
12
|
+
getStaleCached,
|
|
13
|
+
isModelsFetchCoolingDown,
|
|
14
|
+
markModelsFetchFailure,
|
|
15
|
+
markProviderDiscoveryFailed,
|
|
16
|
+
markProviderDiscoveryOk,
|
|
17
|
+
setCached,
|
|
18
|
+
type ProviderModelDiscoveryFailure,
|
|
19
|
+
} from "../model-cache";
|
|
8
20
|
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
9
21
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
22
|
import { modelInList } from "../../types";
|
|
@@ -235,7 +247,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
235
247
|
: models
|
|
236
248
|
);
|
|
237
249
|
if (prov.adapter === "cursor") {
|
|
238
|
-
if (prov.liveModels === false
|
|
250
|
+
if (prov.liveModels === false) {
|
|
251
|
+
clearProviderDiscoveryStatus(name);
|
|
252
|
+
return configured;
|
|
253
|
+
}
|
|
254
|
+
if (!apiKey) return configured;
|
|
239
255
|
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
240
256
|
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
|
|
241
257
|
// suffix) but filter the static seed to the bases the account actually has — so models not on the
|
|
@@ -250,10 +266,12 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
250
266
|
if (liveResult.ok) {
|
|
251
267
|
const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
|
|
252
268
|
const result = available.length > 0 ? available : configured;
|
|
269
|
+
markProviderDiscoveryOk(name);
|
|
253
270
|
setCached(name, result);
|
|
254
271
|
return result;
|
|
255
272
|
}
|
|
256
273
|
markModelsFetchFailure(name);
|
|
274
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
257
275
|
console.warn(
|
|
258
276
|
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
259
277
|
);
|
|
@@ -267,6 +285,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
267
285
|
return configured;
|
|
268
286
|
}
|
|
269
287
|
if (prov.liveModels === false) {
|
|
288
|
+
clearProviderDiscoveryStatus(name);
|
|
270
289
|
return configured;
|
|
271
290
|
}
|
|
272
291
|
const fresh = getFreshCached(name, ttlMs);
|
|
@@ -281,8 +300,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
281
300
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
282
301
|
? "vertex-aiplatform"
|
|
283
302
|
: "provider-models";
|
|
284
|
-
const failedDiscoveryFallback = (
|
|
303
|
+
const failedDiscoveryFallback = (
|
|
304
|
+
failure: ProviderModelDiscoveryFailure,
|
|
305
|
+
): { models: CatalogModel[]; fallback: "stale" | "configured" } => {
|
|
285
306
|
markModelsFetchFailure(name);
|
|
307
|
+
markProviderDiscoveryFailed(name, failure);
|
|
286
308
|
const stale = getStaleCached(name);
|
|
287
309
|
return {
|
|
288
310
|
models: stale
|
|
@@ -297,7 +319,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
297
319
|
allowPrivateNetwork: prov.allowPrivateNetwork,
|
|
298
320
|
});
|
|
299
321
|
if (destinationError) {
|
|
300
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
322
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "blocked" });
|
|
301
323
|
console.warn(
|
|
302
324
|
`[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${destinationError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
303
325
|
);
|
|
@@ -306,7 +328,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
306
328
|
|
|
307
329
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
308
330
|
if (!res.ok) {
|
|
309
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
331
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
310
332
|
console.warn(
|
|
311
333
|
`[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
312
334
|
);
|
|
@@ -321,7 +343,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
321
343
|
try {
|
|
322
344
|
json = JSON.parse(body) as unknown;
|
|
323
345
|
} catch {
|
|
324
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
346
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
325
347
|
const diagnostic = contentType === "application/json" || contentType.endsWith("+json")
|
|
326
348
|
? "returned invalid JSON in a 2xx response"
|
|
327
349
|
: "returned a non-JSON 2xx response";
|
|
@@ -334,7 +356,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
334
356
|
? (json as { data?: unknown }).data
|
|
335
357
|
: undefined;
|
|
336
358
|
if (!isProviderModelsApiItems(data)) {
|
|
337
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
359
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
338
360
|
console.warn(
|
|
339
361
|
`[opencodex] Provider model discovery for "${name}" returned malformed 2xx data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
340
362
|
);
|
|
@@ -376,10 +398,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
376
398
|
&& !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
|
|
377
399
|
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
378
400
|
}
|
|
401
|
+
markProviderDiscoveryOk(name);
|
|
379
402
|
setCached(name, live);
|
|
380
403
|
return live;
|
|
381
404
|
} catch (error) {
|
|
382
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
405
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "network" });
|
|
383
406
|
console.warn(
|
|
384
407
|
`[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
385
408
|
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { commandInvocation, type ResolveDeps, type SpawnInvocation } from "../lib/win-exec";
|
|
2
|
+
|
|
3
|
+
export function isSpawnableCodexCandidate(path: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
4
|
+
if (platform !== "win32") return true;
|
|
5
|
+
return /\.(cmd|bat|exe|com)$/i.test(path);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Platform-safe Codex launcher invocation.
|
|
10
|
+
*
|
|
11
|
+
* Windows `.cmd`/`.bat` must go through `cmd.exe`, but never via `shell: true`
|
|
12
|
+
* (Node does not escape cmd metacharacters there). Reuse the shared
|
|
13
|
+
* `commandInvocation` helper (`ComSpec /d /s /c` + cross-spawn escaping).
|
|
14
|
+
*/
|
|
15
|
+
export function codexExecInvocation(
|
|
16
|
+
command: string,
|
|
17
|
+
args: readonly string[],
|
|
18
|
+
platform: NodeJS.Platform = process.platform,
|
|
19
|
+
deps: ResolveDeps = {},
|
|
20
|
+
): SpawnInvocation {
|
|
21
|
+
return commandInvocation(command, args, platform, deps);
|
|
22
|
+
}
|
package/src/codex/model-cache.ts
CHANGED
|
@@ -17,6 +17,28 @@ interface CacheEntry {
|
|
|
17
17
|
fetchedAt: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export type ProviderModelDiscoveryFailureReason =
|
|
21
|
+
| "http"
|
|
22
|
+
| "blocked"
|
|
23
|
+
| "invalid_response"
|
|
24
|
+
| "network"
|
|
25
|
+
| "provider";
|
|
26
|
+
|
|
27
|
+
export type ProviderModelDiscoveryStatus =
|
|
28
|
+
| { status: "ok" }
|
|
29
|
+
| { status: "failed"; reason: "http"; httpStatus: number }
|
|
30
|
+
| {
|
|
31
|
+
status: "failed";
|
|
32
|
+
reason: Exclude<ProviderModelDiscoveryFailureReason, "http">;
|
|
33
|
+
httpStatus?: never;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ProviderModelDiscoveryFailure = ProviderModelDiscoveryStatus extends infer Status
|
|
37
|
+
? Status extends { status: "failed" }
|
|
38
|
+
? Omit<Status, "status">
|
|
39
|
+
: never
|
|
40
|
+
: never;
|
|
41
|
+
|
|
20
42
|
const cache = new Map<string, CacheEntry>();
|
|
21
43
|
|
|
22
44
|
/** Cooldown after a failed live `/models` fetch, so a dead/unreachable provider doesn't re-pay
|
|
@@ -24,11 +46,31 @@ const cache = new Map<string, CacheEntry>();
|
|
|
24
46
|
export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
|
|
25
47
|
|
|
26
48
|
const failureAt = new Map<string, number>();
|
|
49
|
+
const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
27
50
|
|
|
28
51
|
export function markModelsFetchFailure(provider: string, now = Date.now()): void {
|
|
29
52
|
failureAt.set(provider, now);
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
export function markProviderDiscoveryOk(provider: string): void {
|
|
56
|
+
discoveryStatus.set(provider, { status: "ok" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function markProviderDiscoveryFailed(
|
|
60
|
+
provider: string,
|
|
61
|
+
failure: ProviderModelDiscoveryFailure,
|
|
62
|
+
): void {
|
|
63
|
+
discoveryStatus.set(provider, { status: "failed", ...failure });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clearProviderDiscoveryStatus(provider: string): void {
|
|
67
|
+
discoveryStatus.delete(provider);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getProviderDiscoveryStatus(provider: string): ProviderModelDiscoveryStatus | undefined {
|
|
71
|
+
return discoveryStatus.get(provider);
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
export function isModelsFetchCoolingDown(provider: string, cooldownMs = MODELS_FETCH_FAILURE_COOLDOWN_MS, now = Date.now()): boolean {
|
|
33
75
|
const at = failureAt.get(provider);
|
|
34
76
|
return at !== undefined && now - at < cooldownMs;
|
|
@@ -55,8 +97,10 @@ export function clearModelCache(provider?: string): void {
|
|
|
55
97
|
if (provider) {
|
|
56
98
|
cache.delete(provider);
|
|
57
99
|
failureAt.delete(provider);
|
|
100
|
+
discoveryStatus.delete(provider);
|
|
58
101
|
} else {
|
|
59
102
|
cache.clear();
|
|
60
103
|
failureAt.clear();
|
|
104
|
+
discoveryStatus.clear();
|
|
61
105
|
}
|
|
62
106
|
}
|