@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830
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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DPl4nBMA.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DNdRKXK9.js +0 -112
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
package/src/server/index.ts
CHANGED
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
getConfigDir,
|
|
21
21
|
websocketsEnabled,
|
|
22
22
|
} from "../config";
|
|
23
|
+
import { grokDefaultReasoningEffort } from "../grok/effort";
|
|
23
24
|
import { reconcileOAuthProviders } from "../oauth";
|
|
24
25
|
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
25
26
|
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
26
27
|
import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
|
|
27
28
|
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
28
29
|
import {
|
|
30
|
+
createWindowsTaskListingCache,
|
|
29
31
|
inspectNativeCodexOwnership,
|
|
30
32
|
type NativeCodexOwnership,
|
|
31
33
|
type OwnershipInspection,
|
|
@@ -442,6 +444,37 @@ function attachLiveSidebandUpstream(
|
|
|
442
444
|
// trackSseForRequestLog(
|
|
443
445
|
// export function relaySseWithHeartbeat
|
|
444
446
|
|
|
447
|
+
const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id";
|
|
448
|
+
|
|
449
|
+
function withRequestLogId(response: Response, requestId: string): Response {
|
|
450
|
+
const headers = new Headers(response.headers);
|
|
451
|
+
headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId);
|
|
452
|
+
// A custom `x-` header is not CORS-safelisted, so cross-origin JavaScript gets null from
|
|
453
|
+
// `response.headers.get()` even though the header is on the wire. Naming it here is what
|
|
454
|
+
// makes the id readable by a browser client — the only caller that needs a correlation id
|
|
455
|
+
// it did not send itself.
|
|
456
|
+
//
|
|
457
|
+
// Appending to whatever `withCors` already set, rather than overwriting, keeps this
|
|
458
|
+
// independent of the CORS layer: if the data plane later exposes another header, both
|
|
459
|
+
// survive. Duplicate names are harmless, and the header stays absent from responses that
|
|
460
|
+
// never reach this wrapper, so no management or rejected-origin response is widened.
|
|
461
|
+
const exposed = headers.get("Access-Control-Expose-Headers");
|
|
462
|
+
const already = (exposed ?? "")
|
|
463
|
+
.split(",")
|
|
464
|
+
.some(name => name.trim().toLowerCase() === REQUEST_LOG_ID_RESPONSE_HEADER);
|
|
465
|
+
if (!already) {
|
|
466
|
+
headers.set(
|
|
467
|
+
"Access-Control-Expose-Headers",
|
|
468
|
+
exposed ? `${exposed}, ${REQUEST_LOG_ID_RESPONSE_HEADER}` : REQUEST_LOG_ID_RESPONSE_HEADER,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return new Response(response.body, {
|
|
472
|
+
status: response.status,
|
|
473
|
+
statusText: response.statusText,
|
|
474
|
+
headers,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
445
478
|
export interface StartServerDeps {
|
|
446
479
|
/** Test-only seam; production always initializes its own management credential state. */
|
|
447
480
|
managementAuthState?: ManagementAuthState;
|
|
@@ -467,6 +500,7 @@ function inspectStartupOwnership(
|
|
|
467
500
|
deps: StartServerDeps,
|
|
468
501
|
currentHomes: ReturnType<typeof currentServiceHomes> | null,
|
|
469
502
|
statePaths: readonly string[] | null,
|
|
503
|
+
windowsTaskListingCache?: ReturnType<typeof createWindowsTaskListingCache>,
|
|
470
504
|
): OwnershipInspection {
|
|
471
505
|
try {
|
|
472
506
|
if (currentHomes === null || statePaths === null) {
|
|
@@ -476,9 +510,9 @@ function inspectStartupOwnership(
|
|
|
476
510
|
};
|
|
477
511
|
}
|
|
478
512
|
if (deps.inspectNativeCodexOwnership) {
|
|
479
|
-
return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
513
|
+
return deps.inspectNativeCodexOwnership({ currentHomes, statePaths, windowsTaskListingCache });
|
|
480
514
|
}
|
|
481
|
-
return inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
515
|
+
return inspectNativeCodexOwnership({ currentHomes, statePaths, windowsTaskListingCache });
|
|
482
516
|
} catch {
|
|
483
517
|
return {
|
|
484
518
|
ownership: "unknown",
|
|
@@ -568,6 +602,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
568
602
|
const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
|
|
569
603
|
let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
|
|
570
604
|
let startupOwnershipStatePaths: readonly string[] | null = null;
|
|
605
|
+
// #2923: both synchronous startup ownership decisions keep their fresh,
|
|
606
|
+
// race-sensitive targeted task query. Only the expensive fallback listing is
|
|
607
|
+
// shared, and only while that targeted result stays byte-for-byte unchanged.
|
|
608
|
+
// Runtime ownership retries below intentionally omit this startup-local memo.
|
|
609
|
+
const startupWindowsTaskListingCache = createWindowsTaskListingCache();
|
|
571
610
|
try {
|
|
572
611
|
const homes = resolveServiceHomes();
|
|
573
612
|
const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
|
|
@@ -578,6 +617,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
578
617
|
deps,
|
|
579
618
|
startupOwnershipHomes,
|
|
580
619
|
startupOwnershipStatePaths,
|
|
620
|
+
startupWindowsTaskListingCache,
|
|
581
621
|
);
|
|
582
622
|
// Startup cache invalidation is best-effort and must never block the server from
|
|
583
623
|
// serving. It now takes K so it cannot race a convergence commit. Use the home
|
|
@@ -753,7 +793,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
753
793
|
// clients; no Codex request can use this lifecycle in that state.
|
|
754
794
|
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
755
795
|
// between the two sites must not widen the service-install race.
|
|
756
|
-
const nativeOwnership = inspectStartupOwnership(
|
|
796
|
+
const nativeOwnership = inspectStartupOwnership(
|
|
797
|
+
deps,
|
|
798
|
+
startupOwnershipHomes,
|
|
799
|
+
startupOwnershipStatePaths,
|
|
800
|
+
startupWindowsTaskListingCache,
|
|
801
|
+
);
|
|
757
802
|
const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
|
|
758
803
|
&& startupOwnershipHomes !== null
|
|
759
804
|
? prepareNativeMainStartupLifecycle(
|
|
@@ -1024,7 +1069,81 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1024
1069
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
1025
1070
|
}
|
|
1026
1071
|
|
|
1072
|
+
if (url.pathname === "/v1/catalog" && (req.method === "GET" || req.method === "HEAD")) {
|
|
1073
|
+
// #809: remote Codex clients need the model catalog, and the only prior source was
|
|
1074
|
+
// GET /api/catalog behind management auth — so operators had to hand out an admin
|
|
1075
|
+
// token to read a list of models. This route fixes that on the data plane instead of
|
|
1076
|
+
// widening /api/*, which stays exactly as restricted as before.
|
|
1077
|
+
//
|
|
1078
|
+
// resolveApiAuth (not resolveResponsesApiAuth) for the same reason /v1/models uses
|
|
1079
|
+
// it: nothing here forwards a caller credential upstream, so accepting the dedicated
|
|
1080
|
+
// header, a recognized bearer, or x-api-key is safe — and rejecting x-api-key would
|
|
1081
|
+
// 401 Anthropic-SDK clients holding a perfectly valid data credential.
|
|
1082
|
+
const admission = resolveApiAuth(req, policy);
|
|
1083
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1084
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1085
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1086
|
+
}
|
|
1087
|
+
const { serializePersistedCatalog, persistedCodexVersion, MAX_REMOTE_CATALOG_BYTES } = await import("./catalog-download");
|
|
1088
|
+
const serialized = await serializePersistedCatalog();
|
|
1089
|
+
if (serialized.body === null) {
|
|
1090
|
+
// Built directly rather than through formatErrorResponse: that helper derives
|
|
1091
|
+
// `code` from the status and message via classifyError, and these two need stable,
|
|
1092
|
+
// specific codes. `catalog_not_found` in particular is what lets a caller — and
|
|
1093
|
+
// tests/api-key-attribution.test.ts — tell "this route exists and has no catalog"
|
|
1094
|
+
// apart from "this route is gone", which is the difference between admission proof
|
|
1095
|
+
// and a vacuous pass.
|
|
1096
|
+
return withCors(
|
|
1097
|
+
new Response(JSON.stringify({
|
|
1098
|
+
error: { type: "invalid_request_error", code: "catalog_not_found", message: "no materialized catalog is available" },
|
|
1099
|
+
}), {
|
|
1100
|
+
status: 404,
|
|
1101
|
+
headers: { "content-type": "application/json" },
|
|
1102
|
+
}),
|
|
1103
|
+
req,
|
|
1104
|
+
policy,
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
// Size policy belongs to this route, not the shared serializer: the management route
|
|
1108
|
+
// must keep its existing behavior for a catalog of any supported size.
|
|
1109
|
+
if (serialized.bytes !== undefined && serialized.bytes > MAX_REMOTE_CATALOG_BYTES) {
|
|
1110
|
+
return withCors(
|
|
1111
|
+
new Response(JSON.stringify({
|
|
1112
|
+
error: { type: "server_error", code: "catalog_too_large", message: "catalog exceeds the maximum served size" },
|
|
1113
|
+
}), {
|
|
1114
|
+
status: 507,
|
|
1115
|
+
headers: { "content-type": "application/json" },
|
|
1116
|
+
}),
|
|
1117
|
+
req,
|
|
1118
|
+
policy,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const headers: Record<string, string> = {
|
|
1122
|
+
"content-type": "application/json",
|
|
1123
|
+
// Identity-varying content behind a credential: never let a shared cache keep it.
|
|
1124
|
+
"cache-control": "private, no-cache",
|
|
1125
|
+
};
|
|
1126
|
+
if (serialized.etag) headers.ETag = serialized.etag;
|
|
1127
|
+
const version = await persistedCodexVersion();
|
|
1128
|
+
if (version) headers["x-opencodex-codex-version"] = version;
|
|
1129
|
+
// Conditional GET: a client that already holds these bytes re-validates cheaply.
|
|
1130
|
+
const ifNoneMatch = req.headers.get("if-none-match")?.trim();
|
|
1131
|
+
if (serialized.etag && ifNoneMatch && ifNoneMatch === serialized.etag) {
|
|
1132
|
+
return withCors(new Response(null, { status: 304, headers }), req, policy);
|
|
1133
|
+
}
|
|
1134
|
+
if (serialized.bytes !== undefined) headers["content-length"] = String(serialized.bytes);
|
|
1135
|
+
// HEAD returns identical status and headers with no body.
|
|
1136
|
+
return withCors(
|
|
1137
|
+
new Response(req.method === "HEAD" ? null : serialized.body, { status: 200, headers }),
|
|
1138
|
+
req,
|
|
1139
|
+
policy,
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1027
1143
|
if (url.pathname === "/v1/models" && req.method === "GET") {
|
|
1144
|
+
// #809: the catalog read sits immediately before model discovery because it shares
|
|
1145
|
+
// that route's admission rationale exactly. Keep them adjacent so a future change to
|
|
1146
|
+
// one is made in sight of the other.
|
|
1028
1147
|
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
1029
1148
|
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
1030
1149
|
// remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
|
|
@@ -1038,7 +1157,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1038
1157
|
try {
|
|
1039
1158
|
[goModels, modelEntitlements] = await Promise.all([
|
|
1040
1159
|
fetchAllModels(config),
|
|
1041
|
-
|
|
1160
|
+
// Codex sends its own client_version on this request, and upstream filters the
|
|
1161
|
+
// entitlement roster by it. Passing it through is what stops an entitled account
|
|
1162
|
+
// being told it cannot use models a newer client can (#2886).
|
|
1163
|
+
resolveCodexModelEntitlements(config, { clientVersion: url.searchParams.get("client_version") }),
|
|
1042
1164
|
]);
|
|
1043
1165
|
} catch (error) {
|
|
1044
1166
|
if (error instanceof CatalogGatherBusyError) {
|
|
@@ -1195,10 +1317,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1195
1317
|
...(isDefault ? { default: true } : {}),
|
|
1196
1318
|
});
|
|
1197
1319
|
const grokEffortFields = (efforts: string[], configuredDefault?: string) => {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
? configuredDefault
|
|
1201
|
-
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
|
|
1320
|
+
const defaultEffort = grokDefaultReasoningEffort(efforts, configuredDefault);
|
|
1321
|
+
if (defaultEffort === undefined) return {};
|
|
1202
1322
|
return {
|
|
1203
1323
|
supports_reasoning_effort: true,
|
|
1204
1324
|
reasoning_effort: defaultEffort,
|
|
@@ -1426,7 +1546,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1426
1546
|
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
1427
1547
|
},
|
|
1428
1548
|
});
|
|
1429
|
-
return
|
|
1549
|
+
return withRequestLogId(
|
|
1550
|
+
withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy),
|
|
1551
|
+
requestId,
|
|
1552
|
+
);
|
|
1430
1553
|
});
|
|
1431
1554
|
}
|
|
1432
1555
|
|
|
@@ -925,6 +925,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
925
925
|
nativeContextLimits(latest),
|
|
926
926
|
);
|
|
927
927
|
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
928
|
+
const { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
929
|
+
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
|
|
930
|
+
platform: deps.platform ?? process.platform,
|
|
931
|
+
});
|
|
932
|
+
const policyWarning = claudeDesktopPolicyWarning(policyState);
|
|
928
933
|
// Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
|
|
929
934
|
if (result.fingerprint) {
|
|
930
935
|
// The Desktop write already landed, so a failed bookkeeping save is not
|
|
@@ -941,11 +946,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
941
946
|
saved: false,
|
|
942
947
|
path: result.path,
|
|
943
948
|
fingerprint: result.fingerprint,
|
|
944
|
-
warning:
|
|
949
|
+
warning: [
|
|
950
|
+
`Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
|
|
951
|
+
policyWarning,
|
|
952
|
+
].filter(Boolean).join(" "),
|
|
945
953
|
});
|
|
946
954
|
}
|
|
947
955
|
}
|
|
948
|
-
return jsonResponse({
|
|
956
|
+
return jsonResponse({
|
|
957
|
+
ok: true,
|
|
958
|
+
saved: true,
|
|
959
|
+
applied: true,
|
|
960
|
+
path: result.path,
|
|
961
|
+
fingerprint: result.fingerprint,
|
|
962
|
+
policyState,
|
|
963
|
+
...(policyWarning ? { warning: policyWarning } : {}),
|
|
964
|
+
});
|
|
949
965
|
} catch (error) {
|
|
950
966
|
rethrowManagementBodyTooLarge(error);
|
|
951
967
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -967,7 +983,24 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
967
983
|
// a stale apply the operator should refresh.
|
|
968
984
|
const stale = desiredEnabled && observed.kind === "gateway_drifted";
|
|
969
985
|
const { getDesktopHealth } = await import("../../claude/desktop-health");
|
|
970
|
-
const
|
|
986
|
+
const { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
987
|
+
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
|
|
988
|
+
platform: deps.platform ?? process.platform,
|
|
989
|
+
});
|
|
990
|
+
const policy = claudeDesktopPolicyHealth(policyState);
|
|
991
|
+
const health = {
|
|
992
|
+
...getDesktopHealth(),
|
|
993
|
+
ok: policy.ok,
|
|
994
|
+
status: policy.status,
|
|
995
|
+
policy,
|
|
996
|
+
};
|
|
997
|
+
const policyConflict = desiredEnabled && !policy.ok;
|
|
998
|
+
const baseDrift = desiredEnabled ? !applied || stale : applied || observed.kind === "unsafe";
|
|
999
|
+
const driftReason = policyConflict
|
|
1000
|
+
? policy.state === "present" ? "managed_policy_present" : "managed_policy_unknown"
|
|
1001
|
+
: desiredEnabled
|
|
1002
|
+
? !applied ? "desired_on_not_current" : stale ? "profile_drift" : null
|
|
1003
|
+
: applied ? "desired_off_gateway_selected" : null;
|
|
971
1004
|
return jsonResponse({
|
|
972
1005
|
desiredEnabled,
|
|
973
1006
|
installed: observed.kind !== "not_installed",
|
|
@@ -982,8 +1015,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
982
1015
|
// undeterminable (no/unreadable metadata or no appliedId); a readable
|
|
983
1016
|
// appliedId with no owned entry is a KNOWN false. Predates the inspector.
|
|
984
1017
|
activeProfile: observed.ownedProfileActive,
|
|
985
|
-
drift:
|
|
986
|
-
driftReason
|
|
1018
|
+
drift: baseDrift || policyConflict,
|
|
1019
|
+
driftReason,
|
|
987
1020
|
health,
|
|
988
1021
|
});
|
|
989
1022
|
} catch (error) {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
MAX_BASE_VARIANTS,
|
|
29
29
|
adoptDeveloperInstructions,
|
|
30
30
|
composeProjection,
|
|
31
|
+
computePromptProbeStateFingerprint,
|
|
31
32
|
findInvalidCharacter,
|
|
32
33
|
inspectOwnership,
|
|
33
34
|
normalizeBody,
|
|
@@ -320,7 +321,12 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
|
|
|
320
321
|
// A `cwd` parameter would have let any authenticated request read an arbitrary
|
|
321
322
|
// folder's AGENTS.md through this endpoint.
|
|
322
323
|
const { probePromptText } = await import("../../codex/prompt-text-probe");
|
|
323
|
-
|
|
324
|
+
// A write can complete while an older probe is still running. Admission uses
|
|
325
|
+
// a prompt-state fingerprint rather than the transaction revision: editing
|
|
326
|
+
// the selected base variant changes its separate .md file without moving the
|
|
327
|
+
// optimistic-concurrency revision.
|
|
328
|
+
const promptStateFingerprint = computePromptProbeStateFingerprint(paths(ctx));
|
|
329
|
+
return jsonResponse(await probePromptText(15_000, req.signal, promptStateFingerprint), 200, req, ctx.config);
|
|
324
330
|
}
|
|
325
331
|
|
|
326
332
|
if (url.pathname === "/api/codex-prompt/toggle" && req.method === "PUT") {
|
|
@@ -65,6 +65,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
65
65
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
66
66
|
import type { ManagementContext } from "./context";
|
|
67
67
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
68
|
+
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
/** Management wire shape: omit default imageInput "auto" (persist/response sparse). */
|
|
@@ -169,7 +170,6 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
169
170
|
const nextCombos = { ...(config.combos ?? {}) };
|
|
170
171
|
if (renameFrom) delete nextCombos[renameFrom];
|
|
171
172
|
nextCombos[id] = stored;
|
|
172
|
-
config.combos = nextCombos;
|
|
173
173
|
let shouldSyncClaudeAgentDefs = false;
|
|
174
174
|
const migratedModels = new Map<string, string>();
|
|
175
175
|
if (oldPublicModel && oldPublicModel !== newPublicModel && previous?.nativeAlias !== true) {
|
|
@@ -183,6 +183,15 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
183
183
|
previous?.nativeAlias === true ? comboModelId(id) : newPublicModel,
|
|
184
184
|
);
|
|
185
185
|
}
|
|
186
|
+
const currentShadowTarget = config.shadowCallIntercept?.model;
|
|
187
|
+
const migratedShadowTarget = currentShadowTarget
|
|
188
|
+
? migratedModels.get(currentShadowTarget)
|
|
189
|
+
: undefined;
|
|
190
|
+
if (migratedShadowTarget) {
|
|
191
|
+
const targetError = shadowCallTargetError({ ...config, combos: nextCombos }, migratedShadowTarget);
|
|
192
|
+
if (targetError) return jsonResponse({ error: targetError }, 400);
|
|
193
|
+
}
|
|
194
|
+
config.combos = nextCombos;
|
|
186
195
|
if (migratedModels.size > 0) {
|
|
187
196
|
const migrateReference = (model: string): string => migratedModels.get(model) ?? model;
|
|
188
197
|
const migrateAgentReference = (model: string): string => {
|
|
@@ -38,7 +38,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
38
38
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
39
39
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
40
40
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
41
|
-
import { isCanonicalOpenAiForwardProvider
|
|
41
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
42
42
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
43
43
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
44
44
|
import {
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
type DebugFlag,
|
|
89
89
|
} from "../../lib/debug-settings";
|
|
90
90
|
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
91
|
+
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
91
92
|
import { drainAndShutdown } from "../lifecycle";
|
|
92
93
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
93
94
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
@@ -863,6 +864,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
863
864
|
if (body.model !== undefined && typeof body.model !== "string") {
|
|
864
865
|
return jsonResponse({ error: "model must be a string" }, 400);
|
|
865
866
|
}
|
|
867
|
+
const candidateModel = typeof body.model === "string"
|
|
868
|
+
? body.model
|
|
869
|
+
: body.enabled === true
|
|
870
|
+
? config.shadowCallIntercept?.model
|
|
871
|
+
: undefined;
|
|
872
|
+
const targetError = shadowCallTargetError(config, candidateModel);
|
|
873
|
+
if (targetError) return jsonResponse({ error: targetError }, 400);
|
|
866
874
|
config.shadowCallIntercept = { ...config.shadowCallIntercept };
|
|
867
875
|
if (typeof body.enabled === "boolean") config.shadowCallIntercept.enabled = body.enabled;
|
|
868
876
|
if (typeof body.model === "string") {
|
|
@@ -9,6 +9,7 @@ import type { CatalogModel } from "../../codex/catalog";
|
|
|
9
9
|
import type { Paths as CodexPromptPaths } from "../../codex/prompt-layers";
|
|
10
10
|
import type { injectGrokConfig } from "../../grok/inject";
|
|
11
11
|
import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
12
|
+
import type { probeClaudeDesktopPolicy } from "../../claude/desktop-policy";
|
|
12
13
|
import type { RuntimePortState } from "../../config/process-state";
|
|
13
14
|
import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
|
|
14
15
|
import type {
|
|
@@ -22,6 +23,8 @@ export interface ManagementApiDeps {
|
|
|
22
23
|
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
23
24
|
toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
|
|
24
25
|
createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
26
|
+
/** Test-only destination for best-effort Claude agent-definition sync. */
|
|
27
|
+
claudeAgentConfigDir?: string;
|
|
25
28
|
/** Startup-health seam keeps route tests from launching platform probes. */
|
|
26
29
|
getCachedStartupHealth?: (config: Pick<OcxConfig, "codexAutoStart">) => Promise<StartupHealth>;
|
|
27
30
|
/**
|
|
@@ -47,6 +50,8 @@ export interface ManagementApiDeps {
|
|
|
47
50
|
/** Desktop mutation seams keep route tests inside temporary config libraries. */
|
|
48
51
|
removeDesktop3pStandardPivot?: typeof removeDesktop3pStandardPivot;
|
|
49
52
|
writeDesktop3pConfig?: typeof writeDesktop3pConfig;
|
|
53
|
+
/** Read-only Windows MDM policy seam for status/apply tests. */
|
|
54
|
+
probeClaudeDesktopPolicy?: typeof probeClaudeDesktopPolicy;
|
|
50
55
|
/**
|
|
51
56
|
* Runtime-state seam: the fence must name the host/port the RUNNING process
|
|
52
57
|
* bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend
|
|
@@ -72,6 +72,7 @@ function readDefaultReasoningEffort(raw: unknown, efforts: string[] | undefined)
|
|
|
72
72
|
import type { CatalogModel } from "../../codex/catalog";
|
|
73
73
|
import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
74
74
|
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
75
|
+
import { NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
75
76
|
import { getProviderLiveModelCount } from "../../codex/model-cache";
|
|
76
77
|
import {
|
|
77
78
|
DEFAULT_SUBAGENT_MODELS,
|
|
@@ -332,17 +333,20 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
336
|
+
// Shared with GET|HEAD /v1/catalog (#809) so both planes emit identical bytes.
|
|
337
|
+
const { serializePersistedCatalog, persistedCodexVersion } = await import("../catalog-download");
|
|
338
|
+
const serialized = await serializePersistedCatalog();
|
|
339
|
+
// No size ceiling here on purpose: this route's behavior predates the shared serializer
|
|
340
|
+
// and a 2,000-model catalog (~92 MB, within the supported bound) must keep working.
|
|
341
|
+
if (serialized.body === null) return jsonResponse({ error: "catalog not found" }, 404, req, config);
|
|
338
342
|
const headers: Record<string, string> = {
|
|
339
343
|
"Content-Type": "application/json",
|
|
340
344
|
...corsHeaders(req, config),
|
|
341
345
|
};
|
|
342
|
-
|
|
343
|
-
const version =
|
|
346
|
+
if (serialized.etag) headers.ETag = serialized.etag;
|
|
347
|
+
const version = await persistedCodexVersion();
|
|
344
348
|
if (version) headers["x-opencodex-codex-version"] = version;
|
|
345
|
-
return new Response(
|
|
349
|
+
return new Response(serialized.body, { status: 200, headers });
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
@@ -465,6 +469,12 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
465
469
|
const supportedNative = new Set([
|
|
466
470
|
...nativeModelRows(config).map(row => row.slug),
|
|
467
471
|
...accountNativeQualified,
|
|
472
|
+
// A model suppressed by an unconfirmed entitlement roster is absent from
|
|
473
|
+
// nativeModelRows, so validating against those rows alone rejected a model this build
|
|
474
|
+
// knows perfectly well and left the operator with no way to clear its disable key
|
|
475
|
+
// (#2886). Accepting the target says "this build knows this model", not "this account
|
|
476
|
+
// may use it" — visibility only writes disabledModels and routing stays gated.
|
|
477
|
+
...NATIVE_OPENAI_MODELS,
|
|
468
478
|
]);
|
|
469
479
|
const targets: Array<{ id: string; native: boolean }> = [];
|
|
470
480
|
const seen = new Set<string>();
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
21
|
import { readRuntimePort } from "../../config/process-state";
|
|
22
|
-
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits
|
|
22
|
+
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
|
|
23
23
|
import { providerContextCap } from "../../providers/context-cap";
|
|
24
24
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
25
25
|
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
26
|
-
import {
|
|
26
|
+
import { projectGrokCatalog } from "../../grok/catalog";
|
|
27
|
+
import { injectGrokConfig, stripGrokConfig } from "../../grok/inject";
|
|
27
28
|
import { inspectGrokConfig } from "../../grok/inspect";
|
|
28
29
|
import { grokConfigPath } from "../../grok/status";
|
|
29
30
|
import { assertNativeTeardownOwned } from "../../integrations/native/ownership-preflight";
|
|
@@ -502,21 +503,10 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
502
503
|
* synchronous from entry (012 §One preflight is not enough).
|
|
503
504
|
*/
|
|
504
505
|
const fetchModels = deps.fetchAllModels ?? defaultFetchAllModels;
|
|
505
|
-
let
|
|
506
|
+
let projection: ReturnType<typeof projectGrokCatalog>;
|
|
506
507
|
try {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
// Native slugs carry their context window: without it Grok falls back
|
|
510
|
-
// to its own 200k default and understates a 372k model.
|
|
511
|
-
...visibleNativeSlugs(config).map(id => {
|
|
512
|
-
const contextWindow = nativeOpenAiContextWindow(id, nativeContextLimits(config));
|
|
513
|
-
return { id, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
514
|
-
}),
|
|
515
|
-
...routed.map(m => ({
|
|
516
|
-
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
517
|
-
...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}),
|
|
518
|
-
})),
|
|
519
|
-
];
|
|
508
|
+
const allRouted = await fetchModels(config);
|
|
509
|
+
projection = projectGrokCatalog(allRouted, config);
|
|
520
510
|
} catch (error) {
|
|
521
511
|
// A catalog failure must never write an empty fence (syncGrokConfig
|
|
522
512
|
// guards this; the route inherits the rule). Nothing was written.
|
|
@@ -529,12 +519,17 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
529
519
|
if (recheck.kind === "orphaned_marker") return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
|
|
530
520
|
|
|
531
521
|
const inject = deps.injectGrokConfig ?? injectGrokConfig;
|
|
532
|
-
const result = inject(port, models, {
|
|
522
|
+
const result = inject(port, projection.models, {
|
|
533
523
|
...(hostname !== undefined ? { hostname } : {}),
|
|
534
524
|
// The FULL list plus the exclusion set, never a pre-filtered list: the
|
|
535
525
|
// writer allocates aliases over everything, so a model's alias never
|
|
536
526
|
// depends on its neighbours' switches.
|
|
537
527
|
excluded: new Set(config.grokExcludedModels ?? []),
|
|
528
|
+
// Visibility filters decide what to emit, not whether an owned pre-fence table is still
|
|
529
|
+
// current. Otherwise a hidden model is mistaken for retired state and survives outside.
|
|
530
|
+
catalogModelIds: projection.catalogModelIds,
|
|
531
|
+
disabledProviderNamespaces: projection.disabledProviderNamespaces,
|
|
532
|
+
comboPublicModelIds: projection.comboPublicModelIds,
|
|
538
533
|
});
|
|
539
534
|
|
|
540
535
|
if (result.skippedReason === "non-loopback") {
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
parseAccountPoolStickyLimit,
|
|
40
40
|
parseAccountPoolStrategy,
|
|
41
41
|
} from "../../codex/pool-rotation";
|
|
42
|
+
import { normalizeAccountPoolQuotaWindow, parseAccountPoolQuotaWindow } from "../../oauth/anthropic-routing";
|
|
42
43
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
43
44
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
44
45
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
@@ -326,6 +327,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
326
327
|
autoSwitchThreshold: typeof pool.autoSwitchThreshold === "number" ? pool.autoSwitchThreshold : 80,
|
|
327
328
|
strategy: normalizeAccountPoolStrategy(pool.strategy),
|
|
328
329
|
stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
|
|
330
|
+
quotaWindow: normalizeAccountPoolQuotaWindow(pool.quotaWindow),
|
|
329
331
|
experimental: true,
|
|
330
332
|
});
|
|
331
333
|
}
|
|
@@ -340,6 +342,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
340
342
|
autoSwitchThreshold?: unknown;
|
|
341
343
|
strategy?: unknown;
|
|
342
344
|
stickyLimit?: unknown;
|
|
345
|
+
quotaWindow?: unknown;
|
|
343
346
|
};
|
|
344
347
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
345
348
|
if (provider !== "anthropic") return jsonResponse({ error: "pool config is only supported for anthropic" }, 400);
|
|
@@ -376,11 +379,20 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
376
379
|
}
|
|
377
380
|
stickyLimit = parsed;
|
|
378
381
|
}
|
|
382
|
+
let quotaWindow = config.anthropicAccountPool?.quotaWindow;
|
|
383
|
+
if (body.quotaWindow !== undefined) {
|
|
384
|
+
const parsed = parseAccountPoolQuotaWindow(body.quotaWindow);
|
|
385
|
+
if (parsed === null) {
|
|
386
|
+
return jsonResponse({ error: "quotaWindow must be one of: five-hour, weekly, max-utilization" }, 400);
|
|
387
|
+
}
|
|
388
|
+
quotaWindow = parsed;
|
|
389
|
+
}
|
|
379
390
|
config.anthropicAccountPool = {
|
|
380
391
|
enabled,
|
|
381
392
|
autoSwitchThreshold: threshold,
|
|
382
393
|
...(strategy !== undefined ? { strategy } : {}),
|
|
383
394
|
...(stickyLimit !== undefined ? { stickyLimit } : {}),
|
|
395
|
+
...(quotaWindow !== undefined ? { quotaWindow } : {}),
|
|
384
396
|
};
|
|
385
397
|
saveConfigPreservingClaudeCode(config);
|
|
386
398
|
reconcileLiveStateStores();
|
|
@@ -391,6 +403,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
391
403
|
autoSwitchThreshold: threshold,
|
|
392
404
|
strategy: normalizeAccountPoolStrategy(strategy),
|
|
393
405
|
stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
|
|
406
|
+
quotaWindow: normalizeAccountPoolQuotaWindow(quotaWindow),
|
|
394
407
|
experimental: true,
|
|
395
408
|
});
|
|
396
409
|
}
|