@bitkyc08/opencodex 2.46.0 → 2.47.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 +22 -0
- package/SPONSORS.md +105 -0
- package/gui/dist/assets/index-B8ZZ0ivI.css +1 -0
- package/gui/dist/assets/{index-B72RPblC.js → index-BoXZzNaV.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/adapters/cursor/protobuf-request.ts +34 -21
- package/src/adapters/cursor/tool-guidance.ts +2 -2
- package/src/adapters/cursor/tool-result-normalize.ts +22 -2
- package/src/adapters/exec-tool-result-normalize.ts +87 -0
- package/src/adapters/kiro.ts +29 -18
- package/src/adapters/responses-code-mode.ts +14 -4
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/bridge.ts +40 -24
- package/src/claude/inbound.ts +21 -6
- package/src/claude/outbound.ts +43 -26
- package/src/cli/capabilities.ts +27 -0
- package/src/cli/integrations.ts +1 -1
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +108 -1
- package/src/cli/observe.ts +18 -3
- package/src/cli/usage-report.ts +6 -1
- package/src/clients/config-export.ts +5 -3
- package/src/codex/auth-api.ts +36 -0
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/quota-auto-refresh-state.ts +8 -0
- package/src/codex/quota-auto-refresh.ts +157 -27
- package/src/codex/warmup.ts +5 -0
- package/src/config/provider-validation.ts +70 -1
- package/src/config.ts +73 -1
- package/src/generated/compatibility-version.json +67 -55
- package/src/lib/json-byte-size.ts +61 -0
- package/src/lib/provider-outbound.ts +11 -10
- package/src/oauth/index.ts +42 -8
- package/src/oauth/orcarouter.ts +200 -0
- package/src/providers/key-store.ts +22 -0
- package/src/providers/registry.ts +78 -29
- package/src/responses/citation-markers.ts +56 -24
- package/src/responses/reasoning-envelope.ts +53 -19
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +7 -0
- package/src/server/chat-native.ts +65 -3
- package/src/server/claude-messages.ts +30 -10
- package/src/server/effort-policy.ts +183 -1
- package/src/server/management/agent-settings-routes.ts +50 -9
- package/src/server/management/config-routes.ts +2 -0
- package/src/server/management/logs-usage-routes.ts +11 -3
- package/src/server/management/model-routes.ts +65 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/provider-routes.ts +99 -6
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -7
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core.ts +16 -2
- package/src/server/startup-health-cache.ts +31 -9
- package/src/types/config.ts +5 -0
- package/src/types/provider.ts +4 -0
- package/src/usage/cost.ts +27 -31
- package/src/usage/summary.ts +52 -9
- package/src/usage/time-range.ts +48 -0
- package/src/usage/user-cost-overlays.ts +35 -3
- package/src/vision/anthropic-describe.ts +46 -2
- package/src/web-search/anthropic-executor.ts +49 -3
- package/src/web-search/parse.ts +1 -1
- package/gui/dist/assets/index-BFgUC17B.css +0 -1
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import type { CatalogModel } from "../../codex/catalog";
|
|
4
4
|
import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
|
+
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
|
|
5
6
|
import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_SUBAGENT_MODELS,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
providerHeadersConfigError,
|
|
17
18
|
saveConfigPreservingClaudeCode,
|
|
18
19
|
subagentDefaultSyncEffective,
|
|
20
|
+
validateConfigCandidate,
|
|
19
21
|
} from "../../config";
|
|
20
22
|
import {
|
|
21
23
|
clearLoginState,
|
|
@@ -603,24 +605,63 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
603
605
|
return jsonResponse({
|
|
604
606
|
effortCap: config.effortCap ?? null,
|
|
605
607
|
subagentEffortCap: config.subagentEffortCap ?? null,
|
|
608
|
+
modelPinnedEfforts: config.modelPinnedEfforts ?? {},
|
|
606
609
|
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
607
610
|
});
|
|
608
611
|
}
|
|
609
612
|
if (url.pathname === "/api/effort-caps" && req.method === "PUT") {
|
|
610
|
-
let body:
|
|
613
|
+
let body: unknown;
|
|
611
614
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
615
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
616
|
+
return jsonResponse({ error: "effort caps body must be a plain object" }, 400);
|
|
617
|
+
}
|
|
618
|
+
const patch = body as Record<string, unknown>;
|
|
612
619
|
const { isCodexReasoningEffort } = await import("../../reasoning-effort");
|
|
620
|
+
const draft = { ...projectConfigRebaseProvenance(config) };
|
|
621
|
+
const touched: (keyof OcxConfig)[] = [];
|
|
613
622
|
for (const key of ["effortCap", "subagentEffortCap"] as const) {
|
|
614
|
-
if (!(key
|
|
615
|
-
const value =
|
|
616
|
-
if (value === null || value === "")
|
|
617
|
-
if (typeof value
|
|
618
|
-
|
|
623
|
+
if (!Object.hasOwn(patch, key)) continue;
|
|
624
|
+
const value = patch[key];
|
|
625
|
+
if (value === null || value === "") deleteConfigTopLevelKey(draft, key);
|
|
626
|
+
else if (typeof value === "string" && isCodexReasoningEffort(value)) draft[key] = value;
|
|
627
|
+
else return jsonResponse({ error: "caps must be valid reasoning efforts or null" }, 400);
|
|
628
|
+
touched.push(key);
|
|
629
|
+
}
|
|
630
|
+
if (Object.hasOwn(patch, "modelPinnedEfforts")) {
|
|
631
|
+
const error = modelPinnedEffortsConfigError(patch.modelPinnedEfforts, "modelPinnedEfforts", true);
|
|
632
|
+
if (error) return jsonResponse({ error }, 400);
|
|
633
|
+
const pins = mergeModelPinnedEfforts(config.modelPinnedEfforts, patch.modelPinnedEfforts);
|
|
634
|
+
if (pins) draft.modelPinnedEfforts = pins;
|
|
635
|
+
else deleteConfigTopLevelKey(draft, "modelPinnedEfforts");
|
|
636
|
+
touched.push("modelPinnedEfforts");
|
|
637
|
+
}
|
|
638
|
+
const validation = validateConfigCandidate(draft);
|
|
639
|
+
if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
|
|
640
|
+
if (touched.some(key => !Object.hasOwn(draft, key)) && config.configRebaseProvenance !== undefined
|
|
641
|
+
&& parsedConfigRebaseDeletionKeys(config) === null) {
|
|
642
|
+
return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
|
|
643
|
+
}
|
|
644
|
+
const projected = projectConfigRebaseProvenance(draft);
|
|
645
|
+
touched.push("configRebaseProvenance");
|
|
646
|
+
const rollback = captureConfigTopLevelRollback(config, touched);
|
|
647
|
+
try {
|
|
648
|
+
for (const key of touched) {
|
|
649
|
+
if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
|
|
650
|
+
value: projected[key], writable: true, enumerable: true, configurable: true,
|
|
651
|
+
});
|
|
652
|
+
else deleteConfigTopLevelKey(config, key);
|
|
619
653
|
}
|
|
620
|
-
|
|
654
|
+
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
rollback();
|
|
657
|
+
throw error;
|
|
621
658
|
}
|
|
622
|
-
|
|
623
|
-
|
|
659
|
+
return jsonResponse({
|
|
660
|
+
ok: true,
|
|
661
|
+
effortCap: config.effortCap ?? null,
|
|
662
|
+
subagentEffortCap: config.subagentEffortCap ?? null,
|
|
663
|
+
...(config.modelPinnedEfforts ? { modelPinnedEfforts: config.modelPinnedEfforts } : {}),
|
|
664
|
+
});
|
|
624
665
|
}
|
|
625
666
|
|
|
626
667
|
// Featured roster and saved picker order are separate settings. Native Codex advertises
|
|
@@ -701,6 +701,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
701
701
|
const webSearchCandidates = await webSearchCandidateRows(config);
|
|
702
702
|
return jsonResponse({
|
|
703
703
|
webSearch: {
|
|
704
|
+
enabled: ws.enabled !== false,
|
|
704
705
|
model: ws.model ?? "gpt-5.6-luna",
|
|
705
706
|
backend: ws.backend,
|
|
706
707
|
streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
|
|
@@ -939,6 +940,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
939
940
|
return jsonResponse({
|
|
940
941
|
ok: true,
|
|
941
942
|
webSearch: {
|
|
943
|
+
enabled: ws.enabled !== false,
|
|
942
944
|
model: ws.model ?? "gpt-5.6-luna",
|
|
943
945
|
backend: ws.backend,
|
|
944
946
|
streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
usageLogRevisionKey,
|
|
52
52
|
} from "../../usage/log";
|
|
53
53
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
54
|
+
import { parseUsageTimeWindow, type UsageTimeWindow } from "../../usage/time-range";
|
|
54
55
|
import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, rangeWindow, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
55
56
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
56
57
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
@@ -177,6 +178,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
177
178
|
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
178
179
|
const range = parseRange(url.searchParams.get("range"));
|
|
179
180
|
const surface = parseUsageSurface(url.searchParams.get("surface"));
|
|
181
|
+
let window: UsageTimeWindow | undefined;
|
|
182
|
+
try {
|
|
183
|
+
window = parseUsageTimeWindow(url.searchParams.get("since"), url.searchParams.get("until"));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return jsonResponse({ error: error instanceof Error ? error.message : "invalid usage time window" }, 400);
|
|
186
|
+
}
|
|
180
187
|
// A filtered summary must never reach the cache or the warm loop below:
|
|
181
188
|
// the key is `range:surface`, so a filtered entry stored under it would be
|
|
182
189
|
// served to the next unfiltered caller, dashboard included.
|
|
@@ -185,7 +192,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
185
192
|
model: url.searchParams.get("model"),
|
|
186
193
|
apiKeyId: url.searchParams.get("apiKeyId"),
|
|
187
194
|
};
|
|
188
|
-
const filterRequested = [filter.provider, filter.model, filter.apiKeyId]
|
|
195
|
+
const filterRequested = window !== undefined || [filter.provider, filter.model, filter.apiKeyId]
|
|
189
196
|
.some(value => typeof value === "string" && value.trim() !== "");
|
|
190
197
|
const now = Date.now();
|
|
191
198
|
try {
|
|
@@ -211,7 +218,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
211
218
|
}
|
|
212
219
|
if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
|
|
213
220
|
if (filterRequested) {
|
|
214
|
-
const filteredAggregate = await getFilteredUsageAggregate(filter);
|
|
221
|
+
const filteredAggregate = await getFilteredUsageAggregate(filter, window);
|
|
215
222
|
const accumulator = filteredAggregate.accumulator;
|
|
216
223
|
return jsonResponse({
|
|
217
224
|
...accumulator.summarize(range, now, surface),
|
|
@@ -289,7 +296,8 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
289
296
|
return jsonResponse({
|
|
290
297
|
range,
|
|
291
298
|
surface,
|
|
292
|
-
since: null,
|
|
299
|
+
since: window?.since ?? null,
|
|
300
|
+
...(window ? { customWindow: true, until: window.until } : {}),
|
|
293
301
|
generatedAt: now,
|
|
294
302
|
summary: {
|
|
295
303
|
requests: 0,
|
|
@@ -85,6 +85,8 @@ import {
|
|
|
85
85
|
multiAgentGuidanceEnabled,
|
|
86
86
|
providerBaseUrlConfigError,
|
|
87
87
|
providerHeadersConfigError,
|
|
88
|
+
providerModelCostsConfigError,
|
|
89
|
+
sanitizeModelCostsForDisplay,
|
|
88
90
|
saveConfigPreservingClaudeCode,
|
|
89
91
|
} from "../../config";
|
|
90
92
|
import {
|
|
@@ -98,6 +100,7 @@ import {
|
|
|
98
100
|
} from "../../oauth";
|
|
99
101
|
import { removeCredential } from "../../oauth/store";
|
|
100
102
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
103
|
+
import { redactSecretString } from "../../lib/redact";
|
|
101
104
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
102
105
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
103
106
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
@@ -128,7 +131,7 @@ import {
|
|
|
128
131
|
setDebugSettings,
|
|
129
132
|
type DebugFlag,
|
|
130
133
|
} from "../../lib/debug-settings";
|
|
131
|
-
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
134
|
+
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, ProviderCostOverlay } from "../../types";
|
|
132
135
|
import { drainAndShutdown } from "../lifecycle";
|
|
133
136
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
134
137
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
@@ -369,6 +372,67 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
369
372
|
return jsonResponse(await listManagementModelRows(config));
|
|
370
373
|
}
|
|
371
374
|
|
|
375
|
+
const modelCostsMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-costs$/);
|
|
376
|
+
if (modelCostsMatch && (req.method === "GET" || req.method === "PUT")) {
|
|
377
|
+
let name: string;
|
|
378
|
+
try { name = decodeURIComponent(modelCostsMatch[1]!); } catch { return jsonResponse({ error: "invalid provider encoding" }, 400); }
|
|
379
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
380
|
+
return jsonResponse({ error: "provider not found" }, 404, req, config);
|
|
381
|
+
}
|
|
382
|
+
if (req.method === "GET") {
|
|
383
|
+
const provider = config.providers[name]!;
|
|
384
|
+
return jsonResponse({ provider: name, modelCosts: sanitizeModelCostsForDisplay(provider.modelCosts) ?? {} }, 200, req, config);
|
|
385
|
+
}
|
|
386
|
+
let body: unknown;
|
|
387
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
388
|
+
if (!isPlainRecord(body) || !isValidModelDiscoveryModelId(body.modelId)
|
|
389
|
+
|| !Object.hasOwn(body, "cost")
|
|
390
|
+
|| Object.keys(body).some(key => key !== "modelId" && key !== "cost")) {
|
|
391
|
+
return jsonResponse({ error: "only a valid modelId and cost object or null are allowed" }, 400, req, config);
|
|
392
|
+
}
|
|
393
|
+
const modelId = body.modelId;
|
|
394
|
+
if (redactSecretString(modelId) !== modelId) {
|
|
395
|
+
return jsonResponse({ error: "modelId cannot be displayed safely" }, 400, req, config);
|
|
396
|
+
}
|
|
397
|
+
const submitted = { [modelId]: body.cost };
|
|
398
|
+
const validationError = body.cost === null ? null : providerModelCostsConfigError(submitted);
|
|
399
|
+
if (validationError) return jsonResponse({ error: validationError }, 400, req, config);
|
|
400
|
+
// Copy only validated rate fields; never echo a secret-shaped model key that the
|
|
401
|
+
// shared display boundary suppresses. Model IDs remain exact, including slashes.
|
|
402
|
+
const cost = body.cost === null ? null : sanitizeModelCostsForDisplay(submitted)?.[modelId];
|
|
403
|
+
if (cost === undefined) return jsonResponse({ error: "modelId cannot be displayed safely" }, 400, req, config);
|
|
404
|
+
|
|
405
|
+
// Body parsing yields: a concurrent provider PATCH can replace the row or remove it.
|
|
406
|
+
// Resolve ownership again and keep the merge/save synchronous on the current row.
|
|
407
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
408
|
+
return jsonResponse({ error: "provider not found" }, 404, req, config);
|
|
409
|
+
}
|
|
410
|
+
const provider = config.providers[name]!;
|
|
411
|
+
const hadModelCosts = Object.hasOwn(provider, "modelCosts");
|
|
412
|
+
const previousModelCosts = provider.modelCosts;
|
|
413
|
+
const nextModelCosts = Object.assign(
|
|
414
|
+
Object.create(null) as Record<string, ProviderCostOverlay>,
|
|
415
|
+
previousModelCosts ?? {},
|
|
416
|
+
);
|
|
417
|
+
if (cost === null) delete nextModelCosts[modelId];
|
|
418
|
+
else nextModelCosts[modelId] = cost;
|
|
419
|
+
const mergedError = providerModelCostsConfigError(nextModelCosts);
|
|
420
|
+
if (mergedError) return jsonResponse({ error: mergedError }, 400, req, config);
|
|
421
|
+
// Keep even an empty map until persistence reconciles individual model keys.
|
|
422
|
+
// Deleting the property would also delete prices another writer added on disk.
|
|
423
|
+
provider.modelCosts = nextModelCosts;
|
|
424
|
+
try {
|
|
425
|
+
// The persistence owner refreshes usage overlays after its atomic write.
|
|
426
|
+
// Price-only edits do not change routing or require catalog convergence.
|
|
427
|
+
persistConfig(config);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (hadModelCosts) provider.modelCosts = previousModelCosts;
|
|
430
|
+
else delete provider.modelCosts;
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
return jsonResponse({ ok: true, provider: name, modelId, cost }, 200, req, config);
|
|
434
|
+
}
|
|
435
|
+
|
|
372
436
|
const displayNameMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-display-names$/);
|
|
373
437
|
if (displayNameMatch && req.method === "PUT") {
|
|
374
438
|
let name: string;
|
|
@@ -46,6 +46,7 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
46
46
|
native?: boolean;
|
|
47
47
|
custom?: boolean;
|
|
48
48
|
customId?: string;
|
|
49
|
+
manualPricing?: boolean;
|
|
49
50
|
fastRowAvailable?: boolean;
|
|
50
51
|
displayNameOverride?: string;
|
|
51
52
|
displayNameSource?: "operator" | "provider" | "fallback";
|
|
@@ -181,8 +182,12 @@ export async function listManagementModelRows(
|
|
|
181
182
|
for (const row of rows) knownIds.add(row.namespaced);
|
|
182
183
|
return rows.map(row => {
|
|
183
184
|
const pending = initialModelSelectionPending(config.providers[row.provider]);
|
|
185
|
+
const modelCosts = Object.hasOwn(config.providers, row.provider)
|
|
186
|
+
? config.providers[row.provider]?.modelCosts : undefined;
|
|
184
187
|
return {
|
|
185
188
|
...row,
|
|
189
|
+
...(!row.native && modelCosts !== undefined && Object.hasOwn(modelCosts, row.id)
|
|
190
|
+
? { manualPricing: true } : {}),
|
|
186
191
|
...(pending ? { disabled: true, initialSelectionPending: true } : {}),
|
|
187
192
|
fastRowAvailable: !row.disabled && !pending
|
|
188
193
|
&& !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
submitManualLoginCode,
|
|
34
34
|
upsertOAuthProvider,
|
|
35
35
|
} from "../../oauth";
|
|
36
|
+
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
37
|
+
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
|
|
36
38
|
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
37
39
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
38
40
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
@@ -274,6 +276,11 @@ function providerEditorCandidate(
|
|
|
274
276
|
if (!validated.ok) {
|
|
275
277
|
return { ok: false, status: 400, error: validated.error, code: "invalid_provider_editor_config" };
|
|
276
278
|
}
|
|
279
|
+
for (const [name, provider] of Object.entries(candidate.providers)) {
|
|
280
|
+
if (provider.modelPinnedReasoningEfforts !== undefined) {
|
|
281
|
+
provider.modelPinnedReasoningEfforts = validated.config.providers[name]!.modelPinnedReasoningEfforts;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
277
284
|
return { ok: true, config: candidate, removedProviders };
|
|
278
285
|
}
|
|
279
286
|
|
|
@@ -297,6 +304,27 @@ function adoptProviderEditorCandidate(live: OcxConfig, persisted: OcxConfig): vo
|
|
|
297
304
|
else live.modelDiscovery = structuredClone(persisted.modelDiscovery);
|
|
298
305
|
}
|
|
299
306
|
|
|
307
|
+
/** Share pin merge/clear semantics between POST and the PATCH mask. */
|
|
308
|
+
function applyProviderPinFields(
|
|
309
|
+
next: OcxProviderConfig,
|
|
310
|
+
patch: Record<string, unknown>,
|
|
311
|
+
current: OcxProviderConfig | undefined,
|
|
312
|
+
): string | null {
|
|
313
|
+
const scalarError = pinnedReasoningEffortConfigError(patch.pinnedReasoningEffort, true);
|
|
314
|
+
const mapError = modelPinnedEffortsConfigError(patch.modelPinnedReasoningEfforts, "modelPinnedReasoningEfforts", true);
|
|
315
|
+
if (scalarError || mapError) return scalarError ?? mapError;
|
|
316
|
+
const scalar = Object.hasOwn(patch, "pinnedReasoningEffort")
|
|
317
|
+
? patch.pinnedReasoningEffort : current?.pinnedReasoningEffort;
|
|
318
|
+
const map = Object.hasOwn(patch, "modelPinnedReasoningEfforts")
|
|
319
|
+
? mergeModelPinnedEfforts(current?.modelPinnedReasoningEfforts, patch.modelPinnedReasoningEfforts)
|
|
320
|
+
: current?.modelPinnedReasoningEfforts;
|
|
321
|
+
if (scalar === undefined || scalar === null || scalar === "") delete next.pinnedReasoningEffort;
|
|
322
|
+
else next.pinnedReasoningEffort = scalar as string;
|
|
323
|
+
if (map === undefined) delete next.modelPinnedReasoningEfforts;
|
|
324
|
+
else next.modelPinnedReasoningEfforts = { ...map };
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
300
328
|
/**
|
|
301
329
|
* Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
|
|
302
330
|
* for validation and again inside the config mutation lock against the newest provider,
|
|
@@ -478,6 +506,11 @@ function applyProviderPatchFields(
|
|
|
478
506
|
}
|
|
479
507
|
touched = true;
|
|
480
508
|
}
|
|
509
|
+
if (Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts")) {
|
|
510
|
+
const error = applyProviderPinFields(next, rawBody, provider);
|
|
511
|
+
if (error) return { error };
|
|
512
|
+
touched = true;
|
|
513
|
+
}
|
|
481
514
|
if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
|
|
482
515
|
const value = rawBody.modelAutoCompactTokenLimits;
|
|
483
516
|
const error = modelAutoCompactTokenLimitsConfigError(value, {
|
|
@@ -689,6 +722,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
689
722
|
models: p.models ?? [],
|
|
690
723
|
contextWindow: p.contextWindow,
|
|
691
724
|
modelContextWindows: p.modelContextWindows,
|
|
725
|
+
pinnedReasoningEffort: p.pinnedReasoningEffort,
|
|
726
|
+
modelPinnedReasoningEfforts: p.modelPinnedReasoningEfforts,
|
|
692
727
|
modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
|
|
693
728
|
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
694
729
|
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
@@ -895,6 +930,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
895
930
|
const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
|
|
896
931
|
if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
|
|
897
932
|
const transportCandidate = providerTransportValidationCandidate(body.provider);
|
|
933
|
+
const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
|
|
934
|
+
if (pinError) return jsonResponse({ error: pinError }, 400);
|
|
898
935
|
const providerError = providerManagementConfigError(name, transportCandidate)
|
|
899
936
|
?? providerEmptyToolOutputConfigError(name, transportCandidate);
|
|
900
937
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
@@ -1021,10 +1058,49 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1021
1058
|
prov.xaiResponsesDefaultVersion = latest.xaiResponsesDefaultVersion;
|
|
1022
1059
|
}
|
|
1023
1060
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1061
|
+
// Reapply pins to the latest live row after DNS/import awaits, then validate the
|
|
1062
|
+
// complete draft before adopting any provider/default state.
|
|
1063
|
+
const latest = config.providers[name];
|
|
1064
|
+
const latestPinError = applyProviderPinFields(prov, body.provider, latest);
|
|
1065
|
+
if (latestPinError) return jsonResponse({ error: latestPinError }, 400);
|
|
1066
|
+
const pinsOwned = Object.hasOwn(body.provider, "pinnedReasoningEffort")
|
|
1067
|
+
|| Object.hasOwn(body.provider, "modelPinnedReasoningEfforts")
|
|
1068
|
+
|| latest?.pinnedReasoningEffort !== undefined || latest?.modelPinnedReasoningEfforts !== undefined;
|
|
1069
|
+
// New registration also edits discovery/disabled-model state; stage those
|
|
1070
|
+
// side effects with the pin draft instead of mutating live state before validation.
|
|
1071
|
+
const registrationDraft = pinsOwned && !latest ? {
|
|
1072
|
+
...config,
|
|
1073
|
+
...(config.modelDiscovery === undefined ? {} : { modelDiscovery: structuredClone(config.modelDiscovery) }),
|
|
1074
|
+
} : undefined;
|
|
1075
|
+
initializeProviderModelSelection(name, prov, latest, registrationDraft ?? config);
|
|
1076
|
+
const candidate = stripRegistryOnlyStaticHeaders(name, prov);
|
|
1077
|
+
if (pinsOwned) {
|
|
1078
|
+
const draft = { ...(registrationDraft ?? config), providers: { ...config.providers, [name]: candidate },
|
|
1079
|
+
...(body.setDefault === true ? { defaultProvider: name } : {}) };
|
|
1080
|
+
const validation = validateConfigCandidate(draft);
|
|
1081
|
+
if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
|
|
1082
|
+
}
|
|
1083
|
+
const previous = Object.getOwnPropertyDescriptor(config.providers, name);
|
|
1084
|
+
const rollback = pinsOwned ? captureConfigTopLevelRollback(config, ["defaultProvider", "modelDiscovery", "disabledModels"]) : undefined;
|
|
1085
|
+
try {
|
|
1086
|
+
if (registrationDraft) {
|
|
1087
|
+
for (const key of ["modelDiscovery", "disabledModels"] as const) {
|
|
1088
|
+
if (Object.hasOwn(registrationDraft, key)) Object.defineProperty(config, key, {
|
|
1089
|
+
value: registrationDraft[key], writable: true, enumerable: true, configurable: true,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
config.providers[name] = candidate;
|
|
1094
|
+
if (body.setDefault === true) config.defaultProvider = name;
|
|
1095
|
+
(deps.saveConfigPreservingClaudeCode ?? save)(config);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (rollback) {
|
|
1098
|
+
if (previous) Object.defineProperty(config.providers, name, previous);
|
|
1099
|
+
else delete config.providers[name];
|
|
1100
|
+
rollback();
|
|
1101
|
+
}
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1028
1104
|
reconcileLiveStateStores();
|
|
1029
1105
|
if (prov.apiKey && prov.apiKeyPool) {
|
|
1030
1106
|
const { addProviderApiKey } = await import("../../providers/api-keys");
|
|
@@ -1179,8 +1255,25 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1179
1255
|
}
|
|
1180
1256
|
// A PATCH that managed headers owns the resulting block: the clear path restores
|
|
1181
1257
|
// registry static headers, so exact-match stripping must not erase them again.
|
|
1182
|
-
|
|
1183
|
-
|
|
1258
|
+
const candidate = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
|
|
1259
|
+
const pinsTouched = Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts");
|
|
1260
|
+
if (pinsTouched) {
|
|
1261
|
+
const validation = validateConfigCandidate({ ...config, providers: { ...config.providers, [name]: candidate } });
|
|
1262
|
+
if (!validation.ok) { replayError = validation.error; return; }
|
|
1263
|
+
}
|
|
1264
|
+
const previous = Object.getOwnPropertyDescriptor(config.providers, name);
|
|
1265
|
+
const rollback = pinsTouched ? captureConfigTopLevelRollback(config, []) : undefined;
|
|
1266
|
+
try {
|
|
1267
|
+
config.providers[name] = candidate;
|
|
1268
|
+
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
if (rollback) {
|
|
1271
|
+
if (previous) Object.defineProperty(config.providers, name, previous);
|
|
1272
|
+
else delete config.providers[name];
|
|
1273
|
+
rollback();
|
|
1274
|
+
}
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1184
1277
|
});
|
|
1185
1278
|
if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
|
|
1186
1279
|
reconcileLiveStateStores();
|
|
@@ -334,6 +334,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
334
334
|
{ method: "GET", path: "/api/request-history/{id}/route-decision", module: "server/management/request-history-routes", mutates: false, mechanism: "ends-with" },
|
|
335
335
|
{ method: "PUT", path: "/api/providers/{provider}/alias", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
336
336
|
{ method: "PUT", path: "/api/providers/{provider}/model-aliases", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
337
|
+
{ method: "GET", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: false, mechanism: "regex" },
|
|
338
|
+
{ method: "PUT", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
337
339
|
{ method: "PUT", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
338
340
|
{ method: "DELETE", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
339
341
|
{ method: "GET", path: "/api/lab/subjects/{id}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type UsageSummaryAccumulator,
|
|
15
15
|
} from "../../usage/summary";
|
|
16
16
|
import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
|
|
17
|
+
import type { UsageTimeWindow } from "../../usage/time-range";
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
cacheApiKeyUsageFromRollup,
|
|
@@ -263,7 +264,8 @@ export async function getFilteredUsageAggregate(filter: {
|
|
|
263
264
|
provider?: string | null;
|
|
264
265
|
model?: string | null;
|
|
265
266
|
apiKeyId?: string | null;
|
|
266
|
-
}): Promise<UsageAggregateResult> {
|
|
267
|
+
}, window?: UsageTimeWindow): Promise<UsageAggregateResult> {
|
|
268
|
+
const fixedWindow = window ? Object.freeze({ ...window }) : undefined;
|
|
267
269
|
const normalizedFilter = {
|
|
268
270
|
provider: normalizeFilterValue(filter.provider),
|
|
269
271
|
model: normalizeFilterValue(filter.model),
|
|
@@ -273,11 +275,13 @@ export async function getFilteredUsageAggregate(filter: {
|
|
|
273
275
|
normalizedFilter.provider,
|
|
274
276
|
normalizedFilter.model,
|
|
275
277
|
normalizedFilter.apiKeyId,
|
|
278
|
+
fixedWindow?.since ?? null,
|
|
279
|
+
fixedWindow?.until ?? null,
|
|
276
280
|
]);
|
|
277
281
|
const existing = filteredFlights.get(key);
|
|
278
282
|
if (existing) return existing;
|
|
279
283
|
|
|
280
|
-
const flight = refreshFilteredAggregate(key, normalizedFilter);
|
|
284
|
+
const flight = refreshFilteredAggregate(key, normalizedFilter, fixedWindow);
|
|
281
285
|
filteredFlights.set(key, flight);
|
|
282
286
|
try {
|
|
283
287
|
return await flight;
|
|
@@ -316,12 +320,13 @@ function publishFilteredAggregate(
|
|
|
316
320
|
async function rebuildFilteredAggregate(
|
|
317
321
|
key: string,
|
|
318
322
|
filter: NormalizedUsageFilter,
|
|
323
|
+
window?: UsageTimeWindow,
|
|
319
324
|
): Promise<UsageAggregateResult> {
|
|
320
325
|
let lastError: unknown;
|
|
321
326
|
for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
|
|
322
327
|
const overlayVersion = userCostOverlayVersion();
|
|
323
328
|
const timeZone = currentTimeZone();
|
|
324
|
-
const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique" });
|
|
329
|
+
const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique", window });
|
|
325
330
|
try {
|
|
326
331
|
const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
|
|
327
332
|
if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
|
|
@@ -345,6 +350,7 @@ async function appendFilteredAggregate(
|
|
|
345
350
|
key: string,
|
|
346
351
|
state: RetainedUsageAggregate,
|
|
347
352
|
filter: NormalizedUsageFilter,
|
|
353
|
+
window?: UsageTimeWindow,
|
|
348
354
|
): Promise<UsageAggregateResult> {
|
|
349
355
|
pinnedAggregates.add(state);
|
|
350
356
|
let rebuildAfterUnpin = false;
|
|
@@ -384,28 +390,29 @@ async function appendFilteredAggregate(
|
|
|
384
390
|
pinnedAggregates.delete(state);
|
|
385
391
|
trimRetainedFilteredAggregates();
|
|
386
392
|
}
|
|
387
|
-
if (rebuildAfterUnpin) return rebuildFilteredAggregate(key, filter);
|
|
393
|
+
if (rebuildAfterUnpin) return rebuildFilteredAggregate(key, filter, window);
|
|
388
394
|
throw new Error("filtered usage append did not settle");
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
async function refreshFilteredAggregate(
|
|
392
398
|
key: string,
|
|
393
399
|
filter: NormalizedUsageFilter,
|
|
400
|
+
window?: UsageTimeWindow,
|
|
394
401
|
): Promise<UsageAggregateResult> {
|
|
395
402
|
const state = retainedFilteredAggregates.get(key);
|
|
396
|
-
if (!state) return rebuildFilteredAggregate(key, filter);
|
|
403
|
+
if (!state) return rebuildFilteredAggregate(key, filter, window);
|
|
397
404
|
const observed = currentUsageLogRevision();
|
|
398
405
|
const overlayVersion = userCostOverlayVersion();
|
|
399
406
|
const timeZone = currentTimeZone();
|
|
400
407
|
if (requiresRebuild(state, observed, overlayVersion, timeZone)) {
|
|
401
408
|
retainedFilteredAggregates.delete(key);
|
|
402
|
-
return rebuildFilteredAggregate(key, filter);
|
|
409
|
+
return rebuildFilteredAggregate(key, filter, window);
|
|
403
410
|
}
|
|
404
411
|
if (state.revisionKey === usageLogRevisionKey(observed)) {
|
|
405
412
|
state.retainedAt = Date.now();
|
|
406
413
|
return resultFrom(state, "unchanged");
|
|
407
414
|
}
|
|
408
|
-
return appendFilteredAggregate(key, state, filter);
|
|
415
|
+
return appendFilteredAggregate(key, state, filter, window);
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
|
|
@@ -1094,7 +1094,8 @@ export async function handleResponsesCompact(
|
|
|
1094
1094
|
}
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
|
-
|
|
1097
|
+
// A native compact 404 falls back to a regular Responses compaction turn.
|
|
1098
|
+
if (buffered.status !== 404) return buffered;
|
|
1098
1099
|
} finally {
|
|
1099
1100
|
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
1100
1101
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
@@ -1111,7 +1112,7 @@ export async function handleResponsesCompact(
|
|
|
1111
1112
|
// the completed event back into the v1 compact JSON contract below. Combo-dispatched
|
|
1112
1113
|
// turns also go out as SSE: failover can land on a canonical child that rejects a
|
|
1113
1114
|
// non-streaming turn, and every combo-capable provider already serves streaming traffic.
|
|
1114
|
-
stream: accountGatedCompactWireModel || route.combo ? true : false,
|
|
1115
|
+
stream: isCanonicalOpenAiForwardProvider(route.provider) || accountGatedCompactWireModel || route.combo ? true : false,
|
|
1115
1116
|
input: [...inputItems, { type: "compaction_trigger" }],
|
|
1116
1117
|
};
|
|
1117
1118
|
const internalHeaders = new Headers({ "content-type": "application/json" });
|
|
@@ -270,7 +270,7 @@ import {
|
|
|
270
270
|
upstreamErrorMessageFromPayload,
|
|
271
271
|
} from "../../lib/errors";
|
|
272
272
|
import type { AdmissionLease } from "../../lib/admission";
|
|
273
|
-
import { supportedLadderFor } from "../effort-policy";
|
|
273
|
+
import { prepareEffortNormalization, supportedLadderFor } from "../effort-policy";
|
|
274
274
|
import { isThreadSpawnRequest } from "../effort-policy";
|
|
275
275
|
import {
|
|
276
276
|
applySubagentModelFallback,
|
|
@@ -2280,6 +2280,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2280
2280
|
inboundTransport?: "websocket";
|
|
2281
2281
|
}): Promise<void> {
|
|
2282
2282
|
const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
|
|
2283
|
+
const effortSelector = prepareEffortNormalization(parsed, route);
|
|
2283
2284
|
|
|
2284
2285
|
// Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
|
|
2285
2286
|
// their existing response.model contract even when their public and wire model ids differ.
|
|
@@ -2304,7 +2305,8 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2304
2305
|
|
|
2305
2306
|
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
2306
2307
|
// this request will actually use (#404).
|
|
2307
|
-
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
2308
|
+
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
2309
|
+
sessionLaneIdFromRequest(req.headers) ?? normalizeLogConversationId(req.headers.get("x-opencode-session")));
|
|
2308
2310
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2309
2311
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
2310
2312
|
logCtx.model = route.modelId;
|
|
@@ -2402,6 +2404,17 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2402
2404
|
}
|
|
2403
2405
|
}
|
|
2404
2406
|
|
|
2407
|
+
{
|
|
2408
|
+
const { applyPinnedEffort } = await import("../effort-policy");
|
|
2409
|
+
const pinned = applyPinnedEffort(parsed, route, config, effortSelector);
|
|
2410
|
+
if (pinned) {
|
|
2411
|
+
logCtx.requestedEffort = pinned.from ? `${pinned.from}->${pinned.to}` : pinned.to;
|
|
2412
|
+
if (isInjectionDebugEnabled()) {
|
|
2413
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: pinned reasoning effort applied (${pinned.from ?? "none"} -> ${pinned.to})`);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2405
2418
|
{
|
|
2406
2419
|
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
|
|
2407
2420
|
const surface = collabSurface(parsed);
|
|
@@ -3692,6 +3705,7 @@ async function handleResponsesInner(
|
|
|
3692
3705
|
|| route.providerName === "github-copilot"
|
|
3693
3706
|
|| route.providerName === "kiro"
|
|
3694
3707
|
|| route.providerName === "google-antigravity"
|
|
3708
|
+
|| route.providerName === "orcarouter-oauth"
|
|
3695
3709
|
) && route.provider.authMode === "oauth";
|
|
3696
3710
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
3697
3711
|
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
@@ -50,6 +50,23 @@ export interface StartupHealthCacheDeps {
|
|
|
50
50
|
) => Promise<StartupHealth | null>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Return the last completed probe immediately and refresh it in the background.
|
|
55
|
+
*
|
|
56
|
+
* Settings are consumed by several dashboard controls. They must not block on a
|
|
57
|
+
* Windows service-manager probe; the dedicated /api/startup-health route owns
|
|
58
|
+
* the fresh, bounded diagnostic read.
|
|
59
|
+
*/
|
|
60
|
+
export function getStartupHealthSnapshot(
|
|
61
|
+
config: Pick<OcxConfig, "codexAutoStart">,
|
|
62
|
+
deps: StartupHealthCacheDeps = {},
|
|
63
|
+
): StartupHealth {
|
|
64
|
+
const now = deps.now ?? Date.now;
|
|
65
|
+
if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
|
|
66
|
+
refreshInBackground(config, deps);
|
|
67
|
+
return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config);
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth {
|
|
54
71
|
if (!value.localRoutingDependency) return { ...value, diagnosticStale: true };
|
|
55
72
|
return {
|
|
@@ -134,15 +151,20 @@ function refreshInBackground(
|
|
|
134
151
|
): void {
|
|
135
152
|
if (inflight) return;
|
|
136
153
|
const startedGeneration = generation;
|
|
137
|
-
const probe =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
const probe: Promise<StartupHealth> = Promise.resolve()
|
|
155
|
+
.then(() => (deps.probe ?? runProbe)(config))
|
|
156
|
+
.then(value => {
|
|
157
|
+
if (startedGeneration === generation) {
|
|
158
|
+
cached = { timestamp: (deps.now ?? Date.now)(), value };
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
})
|
|
162
|
+
.catch(() => cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config))
|
|
163
|
+
.finally(() => {
|
|
164
|
+
// An invalidated probe must never clear the newer generation's flight.
|
|
165
|
+
if (inflight === probe) inflight = null;
|
|
166
|
+
});
|
|
167
|
+
inflight = probe;
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
/** Stale-while-revalidate: service-manager probes never hold open a model/UI request. */
|