@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
|
|
2
|
+
import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
|
|
3
|
+
import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
|
|
4
|
+
import type { CodexCapacityResult } from "../quota-capacity";
|
|
5
|
+
import { readUsageSnapshotForManagement } from "../../usage/log";
|
|
6
|
+
import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
|
|
7
|
+
import { deleteCodexAccount } from "../account-lifecycle";
|
|
8
|
+
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
9
|
+
import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
|
|
10
|
+
import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
|
|
11
|
+
import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
|
|
12
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
13
|
+
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
14
|
+
import type { OcxConfig } from "../../types";
|
|
15
|
+
import { CODEX_ACCOUNT_ID_RE, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
16
|
+
import { isCodexResetCreditOperationId } from "../reset-credit-recovery";
|
|
17
|
+
import { listCodexAuthAccounts, selectFallbackAfterPause, pauseExhaustedCodexAccounts } from "./account-list";
|
|
18
|
+
import { jsonResponse, manualImportDisabledResponse } from "./http";
|
|
19
|
+
import { convergeAccountNamespaceCatalog, handleCodexAuthLoginStart, handleCodexAuthLoginCode, handleCodexAuthLoginCancel, handleCodexAuthLoginStatus } from "./login-flow";
|
|
20
|
+
import type { CodexAuthCatalogConvergence } from "./login-flow";
|
|
21
|
+
import { PoolQuotaProbeBusyError } from "./pool-quota-probe";
|
|
22
|
+
import { inspectResetCredits, consumeResetCredits } from "./reset-credit-service";
|
|
23
|
+
import { getRuntimeConfig, saveRuntimeConfig, configuredPoolAccount } from "./runtime-config";
|
|
24
|
+
|
|
25
|
+
export async function handleCodexAuthAPI(
|
|
26
|
+
req: Request,
|
|
27
|
+
url: URL,
|
|
28
|
+
config: OcxConfig,
|
|
29
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
30
|
+
principal?: import("../../server/management-auth").ManagementPrincipal,
|
|
31
|
+
): Promise<Response | null> {
|
|
32
|
+
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
33
|
+
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
|
|
34
|
+
return jsonResponse({ accounts: await listCodexAuthAccounts(config, forceRefresh) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (url.pathname === "/api/codex-auth/accounts/refresh" && req.method === "POST") {
|
|
38
|
+
// Inference spends quota: only a dashboard session carries the consent
|
|
39
|
+
// required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
|
|
40
|
+
return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
|
|
41
|
+
validatePending: principal === "gui-session",
|
|
42
|
+
}) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|
|
46
|
+
return manualImportDisabledResponse();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
|
|
50
|
+
const id = url.searchParams.get("id");
|
|
51
|
+
if (!id) return jsonResponse({ error: "Missing id" }, 400);
|
|
52
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
53
|
+
const isLegacyPoolAccount = CODEX_ACCOUNT_ID_RE.test(id)
|
|
54
|
+
&& (runtimeConfig.codexAccounts ?? []).some(account => !account.isMain && account.id === id);
|
|
55
|
+
if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
|
|
56
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
57
|
+
}
|
|
58
|
+
const pickerVisibilityChanged = deleteCodexAccount(runtimeConfig, id);
|
|
59
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
60
|
+
reconcileLiveStateStores();
|
|
61
|
+
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
62
|
+
runtimeConfig,
|
|
63
|
+
pickerVisibilityChanged,
|
|
64
|
+
convergeCodexCatalog,
|
|
65
|
+
);
|
|
66
|
+
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
|
|
70
|
+
const body = await req.json().catch(() => ({})) as { id?: unknown; alias?: unknown };
|
|
71
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
72
|
+
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
73
|
+
if (id === MAIN_CODEX_ACCOUNT_ID) return jsonResponse({ error: "Main Codex account alias is not configurable" }, 400);
|
|
74
|
+
if (!isValidCodexAccountId(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
75
|
+
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
76
|
+
return jsonResponse({ error: "Alias must be a string of at most 80 printable characters" }, 400);
|
|
77
|
+
}
|
|
78
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
79
|
+
const account = (runtimeConfig.codexAccounts ?? []).find(candidate => candidate.id === id && !candidate.isMain);
|
|
80
|
+
if (!account) return jsonResponse({ error: "Account not found" }, 404);
|
|
81
|
+
if (alias) account.alias = alias;
|
|
82
|
+
else delete account.alias;
|
|
83
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
84
|
+
return jsonResponse({ ok: true, id, alias: alias || null });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (url.pathname === "/api/codex-auth/accounts/pause" && req.method === "PUT") {
|
|
88
|
+
const body = await req.json().catch(() => ({})) as { id?: unknown; paused?: unknown };
|
|
89
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
90
|
+
if (id !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(id)) {
|
|
91
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
92
|
+
}
|
|
93
|
+
if (typeof body.paused !== "boolean") return jsonResponse({ error: "paused must be a boolean" }, 400);
|
|
94
|
+
|
|
95
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
96
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
97
|
+
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
98
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
99
|
+
|
|
100
|
+
setCodexAccountPaused(runtimeConfig, id, body.paused);
|
|
101
|
+
if (body.paused) {
|
|
102
|
+
clearThreadAccountMapForAccount(id);
|
|
103
|
+
selectFallbackAfterPause(runtimeConfig, id);
|
|
104
|
+
}
|
|
105
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
106
|
+
return jsonResponse({
|
|
107
|
+
ok: true,
|
|
108
|
+
id,
|
|
109
|
+
paused: body.paused,
|
|
110
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
111
|
+
appliesImmediately: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Deliberately a route of its own rather than a field on the alias PATCH: aliases
|
|
116
|
+
// are display-only and reject __main__, while selection order is routing metadata
|
|
117
|
+
// that the Desktop account must be able to carry. Re-ordering never kicks a live
|
|
118
|
+
// thread, so there is no affinity clearing and no appliesImmediately here.
|
|
119
|
+
if (url.pathname === "/api/codex-auth/accounts/priority" && req.method === "PUT") {
|
|
120
|
+
let parsedBody: unknown;
|
|
121
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
122
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
123
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
124
|
+
}
|
|
125
|
+
const body = parsedBody as { id?: unknown; priority?: unknown };
|
|
126
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
127
|
+
if (!isCodexAccountPriorityKey(id)) {
|
|
128
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let priority = DEFAULT_ACCOUNT_PRIORITY;
|
|
132
|
+
if (body.priority !== null) {
|
|
133
|
+
const parsed = parseAccountPriority(body.priority);
|
|
134
|
+
if (parsed === null) {
|
|
135
|
+
return jsonResponse({
|
|
136
|
+
error: `priority must be null or an integer ${MIN_ACCOUNT_PRIORITY}-${MAX_ACCOUNT_PRIORITY}`,
|
|
137
|
+
}, 400);
|
|
138
|
+
}
|
|
139
|
+
priority = parsed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
143
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
144
|
+
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
145
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
146
|
+
|
|
147
|
+
setCodexAccountPriority(runtimeConfig, id, priority);
|
|
148
|
+
// Both a pin and an order are the operator saying which account to use, so the newer
|
|
149
|
+
// statement wins. Without this a pin made before any order existed — an ordinary
|
|
150
|
+
// account switch — would outrank the order forever: it blocks preemption and caps
|
|
151
|
+
// every eligibility list at its own tier until that account drains or is paused.
|
|
152
|
+
clearCodexAccountPin(runtimeConfig);
|
|
153
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
154
|
+
return jsonResponse({
|
|
155
|
+
ok: true,
|
|
156
|
+
id,
|
|
157
|
+
priority,
|
|
158
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
|
|
163
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
164
|
+
const result = await pauseExhaustedCodexAccounts(
|
|
165
|
+
runtimeConfig,
|
|
166
|
+
() => saveRuntimeConfig(config, runtimeConfig),
|
|
167
|
+
);
|
|
168
|
+
const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
|
|
169
|
+
if (checkedAccountCount === 0 && failedAccountCount > 0) {
|
|
170
|
+
return jsonResponse({
|
|
171
|
+
ok: false,
|
|
172
|
+
error: "Failed to refresh any Codex account quota",
|
|
173
|
+
checkedAccountCount,
|
|
174
|
+
failedAccountCount,
|
|
175
|
+
}, 502);
|
|
176
|
+
}
|
|
177
|
+
return jsonResponse({
|
|
178
|
+
ok: true,
|
|
179
|
+
pausedAccountIds,
|
|
180
|
+
pausedCount: pausedAccountIds.length,
|
|
181
|
+
checkedAccountCount,
|
|
182
|
+
failedAccountCount,
|
|
183
|
+
complete: failedAccountCount === 0,
|
|
184
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
185
|
+
appliesImmediately: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Manual escape from a quota cooldown. Injected Codex routing makes this proxy the only
|
|
190
|
+
// model path for Codex Desktop, so a cooldown that outlives the real upstream limit
|
|
191
|
+
// otherwise leaves editing config.toml as the user's only recovery.
|
|
192
|
+
//
|
|
193
|
+
// Existence is deliberately NOT disclosed: an unknown id returns 200 with cleared:false
|
|
194
|
+
// exactly like an account that simply had no live cooldown, so this route cannot be used
|
|
195
|
+
// to enumerate configured accounts. Cooldown state is runtime-only and independent of the
|
|
196
|
+
// account list, so 404 would carry no useful meaning anyway.
|
|
197
|
+
if (url.pathname === "/api/codex-auth/accounts/clear-cooldown" && req.method === "POST") {
|
|
198
|
+
const body = await req.json().catch(() => ({})) as { id?: unknown };
|
|
199
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
200
|
+
if (id !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(id)) {
|
|
201
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
202
|
+
}
|
|
203
|
+
return jsonResponse({ ok: true, id, cleared: clearCodexAccountCooldown(id) });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
|
|
207
|
+
let body: { accountId: string | null };
|
|
208
|
+
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
209
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
210
|
+
const targetAccountId = body.accountId ?? MAIN_CODEX_ACCOUNT_ID;
|
|
211
|
+
if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
212
|
+
return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
|
|
213
|
+
}
|
|
214
|
+
if (isCodexAccountPaused(runtimeConfig, targetAccountId)) {
|
|
215
|
+
return jsonResponse({ error: "Account is paused" }, 409);
|
|
216
|
+
}
|
|
217
|
+
if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
218
|
+
if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
219
|
+
const exists = (runtimeConfig.codexAccounts ?? [])
|
|
220
|
+
.some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
|
|
221
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
222
|
+
if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
|
|
223
|
+
return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
227
|
+
// "Use this account now" outranks selection order until the account is spent:
|
|
228
|
+
// persisted here rather than in resetCodexRoutingForManualSelection, which is
|
|
229
|
+
// runtime state only. A null id clears the selection instead of making one, so it
|
|
230
|
+
// must release the pin rather than record one: pinning the `targetAccountId`
|
|
231
|
+
// fallback would leave a pin that no effective active account matches, which
|
|
232
|
+
// `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
|
|
233
|
+
// honours it as a ceiling — invisibly capping the pool at the main account's tier.
|
|
234
|
+
if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
|
|
235
|
+
else setCodexAccountPin(runtimeConfig, targetAccountId);
|
|
236
|
+
resetCodexRoutingForManualSelection(targetAccountId);
|
|
237
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
238
|
+
return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
|
|
242
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
243
|
+
return jsonResponse({
|
|
244
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
245
|
+
pinned: isEffectiveCodexAccountPinned(runtimeConfig),
|
|
246
|
+
// Which account carries the pin, not just whether the active one does. Under
|
|
247
|
+
// round-robin or fill-first the pin caps the tier ceiling at its own tier while the
|
|
248
|
+
// strategy cursor moves freely inside that tier, so `pinned` alone goes false on a
|
|
249
|
+
// sibling's turn even though the pin is still suppressing every higher tier. The id
|
|
250
|
+
// lets a surface mark the account the operator actually chose.
|
|
251
|
+
pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null,
|
|
252
|
+
autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
|
|
253
|
+
upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
|
|
254
|
+
accountPoolStrategy: normalizeCodexAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
255
|
+
accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (url.pathname === "/api/codex-auth/auto-switch" && req.method === "PUT") {
|
|
260
|
+
let body: { threshold: number };
|
|
261
|
+
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
262
|
+
if (typeof body.threshold !== "number" || !Number.isInteger(body.threshold) || body.threshold < 0 || body.threshold > 100) {
|
|
263
|
+
return jsonResponse({ error: "Threshold must be an integer 0-100" }, 400);
|
|
264
|
+
}
|
|
265
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
266
|
+
runtimeConfig.autoSwitchThreshold = body.threshold;
|
|
267
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
268
|
+
return jsonResponse({ ok: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
url.pathname === "/api/codex-auth/pool-strategy"
|
|
273
|
+
&& (req.method === "PUT" || req.method === "PATCH")
|
|
274
|
+
) {
|
|
275
|
+
let parsedBody: unknown;
|
|
276
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
277
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
278
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
279
|
+
}
|
|
280
|
+
const body = parsedBody as { strategy?: unknown; stickyLimit?: unknown };
|
|
281
|
+
if (body.strategy === undefined && body.stickyLimit === undefined) {
|
|
282
|
+
return jsonResponse({ error: "strategy or stickyLimit required" }, 400);
|
|
283
|
+
}
|
|
284
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
285
|
+
let nextStrategy: NonNullable<ReturnType<typeof parseCodexAccountPoolStrategy>> | undefined;
|
|
286
|
+
let nextSticky: NonNullable<ReturnType<typeof parseAccountPoolStickyLimit>> | undefined;
|
|
287
|
+
if (body.strategy !== undefined) {
|
|
288
|
+
const parsed = parseCodexAccountPoolStrategy(body.strategy);
|
|
289
|
+
if (parsed === null) {
|
|
290
|
+
return jsonResponse({ error: 'strategy must be one of: quota, round-robin, fill-first, reset-first' }, 400);
|
|
291
|
+
}
|
|
292
|
+
nextStrategy = parsed;
|
|
293
|
+
}
|
|
294
|
+
if (body.stickyLimit !== undefined) {
|
|
295
|
+
const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
|
|
296
|
+
if (parsed === null) {
|
|
297
|
+
return jsonResponse({ error: "stickyLimit must be an integer 1-100" }, 400);
|
|
298
|
+
}
|
|
299
|
+
nextSticky = parsed;
|
|
300
|
+
}
|
|
301
|
+
if (nextStrategy !== undefined) runtimeConfig.accountPoolStrategy = nextStrategy;
|
|
302
|
+
if (nextSticky !== undefined) runtimeConfig.accountPoolStickyLimit = nextSticky;
|
|
303
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
304
|
+
return jsonResponse({
|
|
305
|
+
ok: true,
|
|
306
|
+
accountPoolStrategy: normalizeCodexAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
307
|
+
accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (url.pathname === "/api/codex-auth/failover" && req.method === "PUT") {
|
|
312
|
+
let body: { threshold: number };
|
|
313
|
+
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
314
|
+
if (typeof body.threshold !== "number" || !Number.isInteger(body.threshold) || body.threshold < 0 || body.threshold > 20) {
|
|
315
|
+
return jsonResponse({ error: "Threshold must be an integer 0-20" }, 400);
|
|
316
|
+
}
|
|
317
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
318
|
+
runtimeConfig.upstreamFailoverThreshold = body.threshold;
|
|
319
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
320
|
+
return jsonResponse({ ok: true });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (url.pathname === "/api/codex-auth/quota/history" && req.method === "GET") {
|
|
324
|
+
const accountId = url.searchParams.get("accountId");
|
|
325
|
+
const rawLimit = url.searchParams.get("limit");
|
|
326
|
+
if (url.searchParams.getAll("accountId").length !== 1 || !isValidCodexAccountId(accountId)
|
|
327
|
+
|| url.searchParams.getAll("limit").length > 1
|
|
328
|
+
|| [...url.searchParams.keys()].some(key => key !== "accountId" && key !== "limit")
|
|
329
|
+
|| (rawLimit !== null && !/^(?:[1-9]|[1-9][0-9]|1[0-9]{2}|200)$/.test(rawLimit))) {
|
|
330
|
+
return jsonResponse({ error: "A stored pool accountId and optional limit from 1 to 200 are required" }, 400);
|
|
331
|
+
}
|
|
332
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
333
|
+
const account = configuredPoolAccount(runtimeConfig, accountId);
|
|
334
|
+
if (!account) return jsonResponse({ error: "Unknown pool account" }, 404);
|
|
335
|
+
const identity = poolQuotaHistoryIdentity(accountId);
|
|
336
|
+
const allHistory = getAccountQuotaHistory(accountId);
|
|
337
|
+
const limit = rawLimit === null ? 200 : Number(rawLimit);
|
|
338
|
+
const history = { ...allHistory, observations: allHistory.observations.slice(-limit), truncated: allHistory.observations.length > limit };
|
|
339
|
+
const label = account.logLabel;
|
|
340
|
+
const labelStillUnique = () => {
|
|
341
|
+
const current = getRuntimeConfig(config);
|
|
342
|
+
return configuredPoolAccount(current, accountId)?.logLabel === label
|
|
343
|
+
&& current.codexAccounts?.filter(row => codexAccountLogLabel(row) === label).length === 1;
|
|
344
|
+
};
|
|
345
|
+
let capacity: CodexCapacityResult = insufficientCodexCapacity("identity_unavailable");
|
|
346
|
+
if (identity && identity === poolQuotaHistoryIdentity(accountId) && label && CODEX_ACCOUNT_LOG_LABEL_RE.test(label) && labelStillUnique()) {
|
|
347
|
+
try {
|
|
348
|
+
const usage = await readUsageSnapshotForManagement();
|
|
349
|
+
if (poolQuotaHistoryIdentity(accountId) !== identity || !labelStillUnique()) capacity = insufficientCodexCapacity("identity_changed");
|
|
350
|
+
else if (!usage.revision) capacity = insufficientCodexCapacity("ledger_unavailable");
|
|
351
|
+
else if (usage.truncatedPrefixBytes > 0 || usage.entriesTruncated || usage.entriesDropped > 0) capacity = insufficientCodexCapacity("ledger_truncated");
|
|
352
|
+
else capacity = estimateCodexQuotaCapacity(allHistory.observations, usage.entries, label,
|
|
353
|
+
model => codexQuotaScopeForModel(model) === "shared");
|
|
354
|
+
} catch { capacity = insufficientCodexCapacity("ledger_unavailable"); }
|
|
355
|
+
}
|
|
356
|
+
if (!configuredPoolAccount(getRuntimeConfig(config), accountId)) return jsonResponse({ error: "Unknown pool account" }, 404);
|
|
357
|
+
if (identity !== poolQuotaHistoryIdentity(accountId) || (identity && label && !labelStillUnique())) {
|
|
358
|
+
return jsonResponse({ accountId, ...getAccountQuotaHistory(accountId, limit), capacity: insufficientCodexCapacity("identity_changed") });
|
|
359
|
+
}
|
|
360
|
+
return jsonResponse({ accountId, ...history, capacity });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (url.pathname === "/api/codex-auth/quota" && req.method === "GET") {
|
|
364
|
+
const quotas: Record<string, unknown> = {};
|
|
365
|
+
for (const [id, q] of listAccountQuotas()) quotas[id] = q;
|
|
366
|
+
return jsonResponse({ quotas });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (url.pathname === "/api/codex-auth/reset-credits" && req.method === "GET") {
|
|
370
|
+
const accountId = url.searchParams.get("accountId");
|
|
371
|
+
if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
return await inspectResetCredits(config, accountId, req.signal);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit lookup failed" }, 500);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
381
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
382
|
+
accountId?: string;
|
|
383
|
+
operationId?: unknown;
|
|
384
|
+
};
|
|
385
|
+
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
386
|
+
const accountId = body.accountId;
|
|
387
|
+
// Optional caller-owned idempotency identity (#3375 axis D). Absent => legacy
|
|
388
|
+
// behavior: a fresh random redeem_request_id and no durable ledger row.
|
|
389
|
+
// The ledger throws TypeError on a malformed id, so the format check has to
|
|
390
|
+
// happen here rather than at the call site, or it surfaces as a 500.
|
|
391
|
+
const hasOperationId = body.operationId !== undefined;
|
|
392
|
+
if (hasOperationId && !isCodexResetCreditOperationId(body.operationId)) {
|
|
393
|
+
return jsonResponse({ error: "Invalid operationId format" }, 400);
|
|
394
|
+
}
|
|
395
|
+
const requestedOperationId = hasOperationId ? body.operationId as string : undefined;
|
|
396
|
+
try {
|
|
397
|
+
return await consumeResetCredits(config, accountId, requestedOperationId);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
if (e instanceof PoolQuotaProbeBusyError) {
|
|
400
|
+
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
401
|
+
response.headers.set("Retry-After", "1");
|
|
402
|
+
return response;
|
|
403
|
+
}
|
|
404
|
+
return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit consume failed" }, 500);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
409
|
+
return handleCodexAuthLoginStart(req, config, convergeCodexCatalog);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (url.pathname === "/api/codex-auth/login/code" && req.method === "POST") {
|
|
413
|
+
return handleCodexAuthLoginCode(req);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (url.pathname === "/api/codex-auth/login/cancel" && req.method === "POST") {
|
|
417
|
+
return handleCodexAuthLoginCancel(req);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (url.pathname === "/api/codex-auth/login-status" && req.method === "GET") {
|
|
421
|
+
return handleCodexAuthLoginStatus(req, url, config);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
2
|
+
import { codexPlanValue } from "../plan";
|
|
3
|
+
import type { CodexAccount, OcxConfig } from "../../types";
|
|
4
|
+
import { isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
5
|
+
|
|
6
|
+
export function configuredPoolAccount(config: OcxConfig, accountId: string): CodexAccount | null {
|
|
7
|
+
if (!isValidCodexAccountId(accountId)) return null;
|
|
8
|
+
return (config.codexAccounts ?? [])
|
|
9
|
+
.find(account => account.id === accountId && isSelectableCodexPoolAccount(account)) ?? null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function nonEmptyPlan(value: unknown): string | null {
|
|
13
|
+
return codexPlanValue(value) ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isRuntimeConfig(config: OcxConfig): boolean {
|
|
17
|
+
return !!config && typeof config === "object" && !!config.providers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getRuntimeConfig(config: OcxConfig): OcxConfig {
|
|
21
|
+
return isRuntimeConfig(config) ? config : loadConfig();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void {
|
|
25
|
+
saveConfigPreservingClaudeCode(nextConfig);
|
|
26
|
+
if (sourceConfig === nextConfig || !isRuntimeConfig(sourceConfig)) return;
|
|
27
|
+
for (const key of Object.keys(sourceConfig) as Array<keyof OcxConfig>) {
|
|
28
|
+
delete sourceConfig[key];
|
|
29
|
+
}
|
|
30
|
+
Object.assign(sourceConfig, nextConfig);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function mapWithConcurrency<T, R>(
|
|
34
|
+
items: T[],
|
|
35
|
+
concurrency: number,
|
|
36
|
+
mapper: (item: T) => Promise<R>,
|
|
37
|
+
): Promise<R[]> {
|
|
38
|
+
const results = new Array<R>(items.length);
|
|
39
|
+
let next = 0;
|
|
40
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
41
|
+
while (next < items.length) {
|
|
42
|
+
const index = next++;
|
|
43
|
+
results[index] = await mapper(items[index]!);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
await Promise.all(workers);
|
|
47
|
+
return results;
|
|
48
|
+
}
|