@yansigit/opencodex 2.33.0 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -5,6 +5,7 @@ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nati
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_SUBAGENT_MODELS,
|
|
7
7
|
codexAutoStartEnabled,
|
|
8
|
+
deleteConfigTopLevelKey,
|
|
8
9
|
hasOwnProvider,
|
|
9
10
|
isValidProviderName,
|
|
10
11
|
multiAgentGuidanceEnabled,
|
|
@@ -298,6 +299,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
298
299
|
streamMode: config.streamMode ?? "auto",
|
|
299
300
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
300
301
|
codexAccountPickerEnabled: codexAccountPickerEnabled(config),
|
|
302
|
+
// Absent means hidden, so the GUI renders the switch without having to know that
|
|
303
|
+
// `undefined` and `false` mean the same thing.
|
|
304
|
+
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
301
305
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
302
306
|
// without having to know that `undefined` and `true` mean the same thing.
|
|
303
307
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
@@ -386,13 +390,15 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
386
390
|
appOwnedMemoryBudgetMb?: unknown;
|
|
387
391
|
codexAccountPickerEnabled?: unknown;
|
|
388
392
|
oauthOpenBrowser?: unknown;
|
|
393
|
+
showCodexSparkQuota?: unknown;
|
|
389
394
|
};
|
|
390
395
|
if (body.codexAutoStart === undefined
|
|
391
396
|
&& body.streamMode === undefined
|
|
392
397
|
&& body.appOwnedMemoryBudgetMb === undefined
|
|
393
398
|
&& body.codexAccountPickerEnabled === undefined
|
|
394
|
-
&& body.oauthOpenBrowser === undefined
|
|
395
|
-
|
|
399
|
+
&& body.oauthOpenBrowser === undefined
|
|
400
|
+
&& body.showCodexSparkQuota === undefined) {
|
|
401
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, oauthOpenBrowser, or showCodexSparkQuota" }, 400);
|
|
396
402
|
}
|
|
397
403
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
398
404
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -407,6 +413,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
407
413
|
&& typeof body.codexAccountPickerEnabled !== "boolean") {
|
|
408
414
|
return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400);
|
|
409
415
|
}
|
|
416
|
+
if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
|
|
417
|
+
return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
|
|
418
|
+
}
|
|
410
419
|
if (body.appOwnedMemoryBudgetMb !== undefined && (
|
|
411
420
|
typeof body.appOwnedMemoryBudgetMb !== "number"
|
|
412
421
|
|| !Number.isInteger(body.appOwnedMemoryBudgetMb)
|
|
@@ -428,6 +437,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
428
437
|
hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
|
|
429
438
|
oauthOpenBrowser: config.oauthOpenBrowser,
|
|
430
439
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
440
|
+
showCodexSparkQuota: config.showCodexSparkQuota,
|
|
441
|
+
hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
|
|
431
442
|
};
|
|
432
443
|
const pickerWasEnabled = codexAccountPickerEnabled(config);
|
|
433
444
|
let pickerIsEnabled = pickerWasEnabled;
|
|
@@ -437,7 +448,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
437
448
|
}
|
|
438
449
|
if (body.streamMode !== undefined) {
|
|
439
450
|
if (body.streamMode === "auto") {
|
|
440
|
-
|
|
451
|
+
deleteConfigTopLevelKey(config, "streamMode");
|
|
441
452
|
} else {
|
|
442
453
|
config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
|
|
443
454
|
}
|
|
@@ -454,25 +465,31 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
454
465
|
if (typeof body.oauthOpenBrowser === "boolean") {
|
|
455
466
|
config.oauthOpenBrowser = body.oauthOpenBrowser;
|
|
456
467
|
}
|
|
468
|
+
if (typeof body.showCodexSparkQuota === "boolean") {
|
|
469
|
+
config.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
470
|
+
}
|
|
457
471
|
pickerIsEnabled = codexAccountPickerEnabled(config);
|
|
458
472
|
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
459
473
|
} catch (error) {
|
|
460
474
|
if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart;
|
|
461
|
-
else
|
|
475
|
+
else deleteConfigTopLevelKey(config, "codexAutoStart");
|
|
462
476
|
if (previousSettings.hasStreamMode) config.streamMode = previousSettings.streamMode;
|
|
463
|
-
else
|
|
477
|
+
else deleteConfigTopLevelKey(config, "streamMode");
|
|
464
478
|
if (previousSettings.hasAppOwnedMemoryBudgetMb) {
|
|
465
479
|
config.appOwnedMemoryBudgetMb = previousSettings.appOwnedMemoryBudgetMb;
|
|
466
|
-
} else
|
|
480
|
+
} else deleteConfigTopLevelKey(config, "appOwnedMemoryBudgetMb");
|
|
467
481
|
if (previousSettings.hasCodexAccountNamespaces) {
|
|
468
482
|
config.codexAccountNamespaces = previousSettings.codexAccountNamespaces;
|
|
469
|
-
} else
|
|
483
|
+
} else deleteConfigTopLevelKey(config, "codexAccountNamespaces");
|
|
470
484
|
if (previousSettings.hasCodexAccountPickerEnabled) {
|
|
471
485
|
config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
|
|
472
|
-
} else
|
|
486
|
+
} else deleteConfigTopLevelKey(config, "codexAccountPickerEnabled");
|
|
473
487
|
if (previousSettings.hasOauthOpenBrowser) {
|
|
474
488
|
config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
|
|
475
|
-
} else
|
|
489
|
+
} else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
|
|
490
|
+
if (previousSettings.hasShowCodexSparkQuota) {
|
|
491
|
+
config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
|
|
492
|
+
} else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
|
|
476
493
|
throw error;
|
|
477
494
|
}
|
|
478
495
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
@@ -494,6 +511,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
494
511
|
codexAccountPickerEnabled: pickerIsEnabled,
|
|
495
512
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
496
513
|
catalogRefreshPending,
|
|
514
|
+
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
497
515
|
startupHealth: await readStartupHealth(config),
|
|
498
516
|
});
|
|
499
517
|
}
|
|
@@ -6,6 +6,7 @@ import type { StartupHealth } from "../../codex/autostart-health";
|
|
|
6
6
|
import type { StartupInstallAction } from "../startup-action-control";
|
|
7
7
|
import type { ManagementPrincipal } from "../management-auth";
|
|
8
8
|
import type { CatalogModel } from "../../codex/catalog";
|
|
9
|
+
import type { Paths as CodexPromptPaths } from "../../codex/prompt-layers";
|
|
9
10
|
import type { injectGrokConfig } from "../../grok/inject";
|
|
10
11
|
import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
11
12
|
import type { RuntimePortState } from "../../config/process-state";
|
|
@@ -91,6 +92,14 @@ export interface ManagementApiDeps {
|
|
|
91
92
|
* state inside their temporary Codex home.
|
|
92
93
|
*/
|
|
93
94
|
codexLogGuardMaintenanceDeps?: CodexLogGuardMaintenanceDeps;
|
|
95
|
+
/**
|
|
96
|
+
* Prompt-layer path seam. Production leaves this unset and
|
|
97
|
+
* `src/codex/prompt-layers.ts` resolves the real CODEX_HOME. A route test
|
|
98
|
+
* that could not inject paths would read AND WRITE the developer's live
|
|
99
|
+
* ~/.codex/config.toml — the same class of incident
|
|
100
|
+
* `saveConfigPreservingClaudeCode` above exists to prevent.
|
|
101
|
+
*/
|
|
102
|
+
codexPromptPaths?: CodexPromptPaths;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
submitManualLoginCode,
|
|
22
22
|
upsertOAuthProvider,
|
|
23
23
|
} from "../../oauth";
|
|
24
|
-
import { removeCredential } from "../../oauth/store";
|
|
24
|
+
import { getAccountSet, removeCredential } from "../../oauth/store";
|
|
25
25
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
26
26
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
27
27
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
@@ -204,10 +204,16 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
204
204
|
const filter = {
|
|
205
205
|
provider: url.searchParams.get("provider"),
|
|
206
206
|
model: url.searchParams.get("model"),
|
|
207
|
+
account: url.searchParams.get("account"),
|
|
207
208
|
};
|
|
209
|
+
const fallbackAccounts: Record<string, string> = {};
|
|
210
|
+
for (const p of listOAuthProviders()) {
|
|
211
|
+
const set = getAccountSet(p);
|
|
212
|
+
if (set && set.accounts.length === 1 && set.activeAccountId) fallbackAccounts[p] = set.activeAccountId;
|
|
213
|
+
}
|
|
208
214
|
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
209
|
-
projectUsageSummary(summary, filter, entries);
|
|
210
|
-
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
215
|
+
projectUsageSummary(summary, filter, entries, fallbackAccounts);
|
|
216
|
+
const filterRequested = Boolean(filter.provider ?? filter.model ?? filter.account);
|
|
211
217
|
const now = Date.now();
|
|
212
218
|
try {
|
|
213
219
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -242,7 +248,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
242
248
|
const revisionReadAt = Date.now();
|
|
243
249
|
const window = snapshotWindow(snapshot.entries);
|
|
244
250
|
const summary = {
|
|
245
|
-
...summarizeUsage(snapshot.entries, range, now, surface),
|
|
251
|
+
...summarizeUsage(snapshot.entries, range, now, surface, fallbackAccounts),
|
|
246
252
|
historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
247
253
|
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
|
|
248
254
|
entriesTruncated: snapshot.entriesTruncated,
|
|
@@ -270,7 +276,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
270
276
|
for (const nextRange of ranges) {
|
|
271
277
|
for (const nextSurface of surfaces) {
|
|
272
278
|
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
273
|
-
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface),
|
|
279
|
+
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface, fallbackAccounts),
|
|
274
280
|
historyTruncated: summary.historyTruncated,
|
|
275
281
|
truncatedPrefixBytes: summary.truncatedPrefixBytes,
|
|
276
282
|
entriesTruncated: summary.entriesTruncated,
|
|
@@ -99,6 +99,8 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
99
99
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
100
100
|
import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
101
101
|
import { knownModelIdsForProvider } from "../../router";
|
|
102
|
+
import { effectiveModelAliases, MODEL_ALIAS_PATTERN } from "../../providers/default-aliases";
|
|
103
|
+
import { comboPublicModelId } from "../../combos/types";
|
|
102
104
|
import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
103
105
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
104
106
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -149,6 +151,12 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
149
151
|
import type { ManagementContext } from "./context";
|
|
150
152
|
import { listManagementModelRows, loadExportModels } from "./model-rows";
|
|
151
153
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
154
|
+
import {
|
|
155
|
+
hasModelPreset,
|
|
156
|
+
markModelPresetDiverged,
|
|
157
|
+
materializeModelPreset,
|
|
158
|
+
modelPresetFor,
|
|
159
|
+
} from "../../providers/model-presets";
|
|
152
160
|
|
|
153
161
|
/**
|
|
154
162
|
* Counts read back off the SERIALIZED document rather than recomputed from the input rows.
|
|
@@ -171,6 +179,158 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
171
179
|
// ~/.opencodex/config.json with the `existing-uuid` test fixture.
|
|
172
180
|
const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
|
|
173
181
|
|
|
182
|
+
if (url.pathname === "/api/model-discovery" && req.method === "GET") {
|
|
183
|
+
const providers = Object.fromEntries(Object.entries(config.providers).map(([name, provider]) => [
|
|
184
|
+
name, provider.newModelPolicy ?? "inherit",
|
|
185
|
+
]));
|
|
186
|
+
const recentArrivals = Object.fromEntries(Object.entries(config.modelDiscovery?.recentArrivals ?? {}).map(([name, rows]) => [
|
|
187
|
+
name,
|
|
188
|
+
rows.map(row => ({
|
|
189
|
+
...row,
|
|
190
|
+
state: (config.disabledModels ?? []).some(slug => slugEquals(slug, name, row.id))
|
|
191
|
+
? "auto-disabled" : "enabled",
|
|
192
|
+
})),
|
|
193
|
+
]));
|
|
194
|
+
const baselineCounts = Object.fromEntries(Object.entries(config.modelDiscovery?.knownModels ?? {}).map(([name, baseline]) => [
|
|
195
|
+
name, baseline.ids.length,
|
|
196
|
+
]));
|
|
197
|
+
return jsonResponse({
|
|
198
|
+
policy: config.modelDiscovery?.newModelPolicy ?? "on", providers, recentArrivals, baselineCounts,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (url.pathname === "/api/model-discovery" && req.method === "PUT") {
|
|
203
|
+
let body: { policy?: unknown; provider?: unknown };
|
|
204
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
205
|
+
if (body.policy !== "on" && body.policy !== "off") return jsonResponse({ error: "policy must be on or off" }, 400);
|
|
206
|
+
const provider = typeof body.provider === "string" && body.provider.trim() ? body.provider.trim() : null;
|
|
207
|
+
let baselineBootstrapped = false;
|
|
208
|
+
if (provider) {
|
|
209
|
+
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
210
|
+
config.providers[provider].newModelPolicy = body.policy;
|
|
211
|
+
} else {
|
|
212
|
+
const wasAbsent = config.modelDiscovery?.newModelPolicy === undefined;
|
|
213
|
+
config.modelDiscovery ??= {};
|
|
214
|
+
config.modelDiscovery.newModelPolicy = body.policy;
|
|
215
|
+
if (body.policy === "off" && wasAbsent) {
|
|
216
|
+
const models = await fetchAllModels(config);
|
|
217
|
+
const known = config.modelDiscovery.knownModels ??= {};
|
|
218
|
+
const at = new Date().toISOString();
|
|
219
|
+
for (const name of Object.keys(config.providers)) {
|
|
220
|
+
known[name] ??= { ids: [...new Set(models.filter(m => m.provider === name).map(m => m.id))].sort(), removed: [], updatedAt: at };
|
|
221
|
+
}
|
|
222
|
+
baselineBootstrapped = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
persistConfig(config);
|
|
226
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
227
|
+
return jsonResponse({ ok: true, policy: body.policy, provider, ...(baselineBootstrapped ? { baselineBootstrapped } : {}), catalogRefresh });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (url.pathname === "/api/model-discovery/acknowledge" && req.method === "POST") {
|
|
231
|
+
let body: { provider?: unknown; ids?: unknown };
|
|
232
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
233
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
234
|
+
if (!provider || !Array.isArray(body.ids) || body.ids.some(id => typeof id !== "string")) {
|
|
235
|
+
return jsonResponse({ error: "provider and string ids are required" }, 400);
|
|
236
|
+
}
|
|
237
|
+
const acknowledged = new Set(body.ids as string[]);
|
|
238
|
+
const recent = config.modelDiscovery?.recentArrivals;
|
|
239
|
+
if (recent?.[provider]) recent[provider] = recent[provider].filter(row => !acknowledged.has(row.id));
|
|
240
|
+
persistConfig(config);
|
|
241
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
242
|
+
return jsonResponse({ ok: true, provider, acknowledged: [...acknowledged], catalogRefresh });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (url.pathname === "/api/aliases" && req.method === "GET") {
|
|
246
|
+
const providers: Record<string, string> = {};
|
|
247
|
+
const models: Record<string, Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }>> = {};
|
|
248
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
249
|
+
if (provider.alias) providers[name] = provider.alias;
|
|
250
|
+
const known = knownModelIdsForProvider(name, provider, config);
|
|
251
|
+
const knownSet = new Set(known);
|
|
252
|
+
const rows: Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }> = {};
|
|
253
|
+
for (const [id, value] of effectiveModelAliases(config, provider, new Set([...known, ...Object.keys(provider.modelAliases ?? {})]))) {
|
|
254
|
+
rows[id] = { ...value, ...(!knownSet.has(id) ? { stale: true } : {}) };
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(rows).length) models[name] = rows;
|
|
257
|
+
}
|
|
258
|
+
return jsonResponse({ providers, models, defaults: {
|
|
259
|
+
global: config.defaultModelAliases ?? false,
|
|
260
|
+
providers: Object.fromEntries(Object.entries(config.providers).filter(([, p]) => p.defaultAliases !== undefined).map(([n, p]) => [n, p.defaultAliases])),
|
|
261
|
+
} });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const providerAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/alias$/);
|
|
265
|
+
if (providerAliasMatch && req.method === "PUT") {
|
|
266
|
+
const name = decodeURIComponent(providerAliasMatch[1]!);
|
|
267
|
+
// `keys` is not a provider name: `/api/providers/keys/alias` is the API-KEY POOL's rename
|
|
268
|
+
// endpoint (oauth-account-routes.ts), and model routes are dispatched BEFORE it. Without
|
|
269
|
+
// this guard the alias route matched `name = "keys"`, found no such provider, and returned
|
|
270
|
+
// 404 for every key-pool rename.
|
|
271
|
+
if (name === "keys") return null;
|
|
272
|
+
const provider = config.providers[name];
|
|
273
|
+
if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
|
|
274
|
+
let raw: unknown;
|
|
275
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
276
|
+
if (!isPlainRecord(raw) || (raw.alias !== null && typeof raw.alias !== "string")) return jsonResponse({ error: "alias must be a string or null" }, 400, req, config);
|
|
277
|
+
const alias = typeof raw.alias === "string" ? raw.alias.trim() : null;
|
|
278
|
+
if (alias && !isValidProviderName(alias)) return jsonResponse({ error: "invalid provider alias" }, 400, req, config);
|
|
279
|
+
const lower = alias?.toLowerCase();
|
|
280
|
+
const collision = lower && Object.entries(config.providers).find(([other, p]) =>
|
|
281
|
+
other !== name && (other.toLowerCase() === lower || p.alias?.toLowerCase() === lower));
|
|
282
|
+
const comboCollision = lower && Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower);
|
|
283
|
+
const accountCollision = lower && Object.keys(config.codexAccountNamespaces ?? {}).find(value => value.toLowerCase() === lower);
|
|
284
|
+
if (collision || comboCollision || accountCollision) return jsonResponse({ error: `alias conflicts with '${collision?.[0] ?? comboCollision?.[0] ?? accountCollision}'` }, 409, req, config);
|
|
285
|
+
if (alias) provider.alias = alias; else delete provider.alias;
|
|
286
|
+
persistConfig(config);
|
|
287
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
288
|
+
return jsonResponse({ ok: true, provider: name, alias, catalogRefresh });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const modelAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-aliases$/);
|
|
292
|
+
if (modelAliasMatch && req.method === "PUT") {
|
|
293
|
+
const name = decodeURIComponent(modelAliasMatch[1]!);
|
|
294
|
+
if (name === "keys") return null;
|
|
295
|
+
const provider = config.providers[name];
|
|
296
|
+
if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
|
|
297
|
+
let raw: unknown;
|
|
298
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
299
|
+
if (!isPlainRecord(raw) || (raw.set !== undefined && !isPlainRecord(raw.set)) || (raw.remove !== undefined && !Array.isArray(raw.remove))) return jsonResponse({ error: "invalid model alias update" }, 400, req, config);
|
|
300
|
+
const next = { ...(provider.modelAliases ?? {}) };
|
|
301
|
+
for (const id of (raw.remove ?? []) as unknown[]) if (typeof id === "string") delete next[id];
|
|
302
|
+
const conflicts: Array<{ alias: string; heldBy: string }> = [];
|
|
303
|
+
const known = knownModelIdsForProvider(name, provider, config);
|
|
304
|
+
for (const [id, value] of Object.entries((raw.set ?? {}) as Record<string, unknown>)) {
|
|
305
|
+
if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value)) return jsonResponse({ error: `invalid model alias for '${id}'` }, 400, req, config);
|
|
306
|
+
const lower = value.toLowerCase();
|
|
307
|
+
const heldBy = Object.entries(next).find(([other, alias]) => other !== id && alias.toLowerCase() === lower)?.[0]
|
|
308
|
+
?? known.find(native => native.toLowerCase() === lower)
|
|
309
|
+
?? Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower)?.[0];
|
|
310
|
+
if (heldBy || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) conflicts.push({ alias: value, heldBy: heldBy ?? "native OpenAI family" });
|
|
311
|
+
else next[id] = value;
|
|
312
|
+
}
|
|
313
|
+
if (conflicts.length) return jsonResponse({ error: "model alias collision", conflicts }, 409, req, config);
|
|
314
|
+
provider.modelAliases = next;
|
|
315
|
+
persistConfig(config);
|
|
316
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
317
|
+
return jsonResponse({ ok: true, aliases: next, catalogRefresh });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (url.pathname === "/api/default-aliases" && req.method === "PUT") {
|
|
321
|
+
let raw: unknown;
|
|
322
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
323
|
+
if (!isPlainRecord(raw) || typeof raw.enabled !== "boolean" || (raw.provider !== undefined && typeof raw.provider !== "string")) return jsonResponse({ error: "enabled must be boolean" }, 400, req, config);
|
|
324
|
+
if (typeof raw.provider === "string") {
|
|
325
|
+
const provider = config.providers[raw.provider];
|
|
326
|
+
if (!provider) return jsonResponse({ error: `provider '${raw.provider}' not found` }, 404, req, config);
|
|
327
|
+
provider.defaultAliases = raw.enabled;
|
|
328
|
+
} else config.defaultModelAliases = raw.enabled;
|
|
329
|
+
persistConfig(config);
|
|
330
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
331
|
+
return jsonResponse({ ok: true, catalogRefresh });
|
|
332
|
+
}
|
|
333
|
+
|
|
174
334
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
175
335
|
const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
|
|
176
336
|
const catalog = readCatalog(readCodexCatalogPath());
|
|
@@ -370,6 +530,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
370
530
|
providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
|
|
371
531
|
}
|
|
372
532
|
disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
|
|
533
|
+
const arrivals = config.modelDiscovery?.recentArrivals?.[provider];
|
|
534
|
+
if (arrivals) config.modelDiscovery!.recentArrivals![provider] = arrivals.filter(row => (
|
|
535
|
+
!targets.some(target => !target.native && target.id === row.id)
|
|
536
|
+
));
|
|
373
537
|
}
|
|
374
538
|
} else {
|
|
375
539
|
for (const target of targets) {
|
|
@@ -543,6 +707,104 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
543
707
|
}
|
|
544
708
|
return jsonResponse({ selected, available, liveModelCounts });
|
|
545
709
|
}
|
|
710
|
+
if (url.pathname === "/api/model-presets" && req.method === "GET") {
|
|
711
|
+
// Preview without applying: rules evaluated against the CURRENT catalog, so the count the
|
|
712
|
+
// user sees is the count they would get.
|
|
713
|
+
const models = await fetchAllModels(config);
|
|
714
|
+
const byProvider = new Map<string, string[]>();
|
|
715
|
+
for (const m of models) {
|
|
716
|
+
const ids = byProvider.get(m.provider) ?? [];
|
|
717
|
+
ids.push(m.id);
|
|
718
|
+
byProvider.set(m.provider, ids);
|
|
719
|
+
}
|
|
720
|
+
const providers: Record<string, unknown> = {};
|
|
721
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
722
|
+
const preset = modelPresetFor(name);
|
|
723
|
+
if (!preset) continue;
|
|
724
|
+
const catalogIds = byProvider.get(name) ?? [];
|
|
725
|
+
const presetIds = materializeModelPreset(name, catalogIds);
|
|
726
|
+
providers[name] = {
|
|
727
|
+
mode: prov.modelPreset?.mode ?? "all",
|
|
728
|
+
...(prov.modelPreset?.appliedVersion !== undefined
|
|
729
|
+
? { appliedVersion: prov.modelPreset.appliedVersion }
|
|
730
|
+
: {}),
|
|
731
|
+
availableVersion: preset.version,
|
|
732
|
+
presetIds,
|
|
733
|
+
presetCount: presetIds.length,
|
|
734
|
+
totalCount: catalogIds.length,
|
|
735
|
+
...(prov.modelPreset?.fallback ? { fallback: prov.modelPreset.fallback } : {}),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return jsonResponse({ providers });
|
|
739
|
+
}
|
|
740
|
+
if (url.pathname === "/api/model-presets" && req.method === "PUT") {
|
|
741
|
+
let body: { provider?: unknown; mode?: unknown };
|
|
742
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
743
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
744
|
+
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
745
|
+
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|
|
746
|
+
}
|
|
747
|
+
const mode = body.mode;
|
|
748
|
+
if (mode !== "preset" && mode !== "all" && mode !== "custom") {
|
|
749
|
+
return jsonResponse({ error: "mode must be preset, all, or custom" }, 400);
|
|
750
|
+
}
|
|
751
|
+
const target = config.providers[provider];
|
|
752
|
+
if (mode === "all") {
|
|
753
|
+
// Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
|
|
754
|
+
delete target.selectedModels;
|
|
755
|
+
delete target.modelPreset;
|
|
756
|
+
persistConfig(config);
|
|
757
|
+
return jsonResponse({ ok: true, provider, mode, selected: [], catalogRefresh: await convergeCodexCatalog() });
|
|
758
|
+
}
|
|
759
|
+
if (mode === "custom") {
|
|
760
|
+
// Keep whatever is selected; only the marker changes, so a user can pin their edits
|
|
761
|
+
// without the proxy re-materializing over them.
|
|
762
|
+
target.modelPreset = { ...(target.modelPreset ?? {}), mode: "custom" };
|
|
763
|
+
persistConfig(config);
|
|
764
|
+
return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
|
|
765
|
+
}
|
|
766
|
+
if (!hasModelPreset(provider)) {
|
|
767
|
+
return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
|
|
768
|
+
}
|
|
769
|
+
const models = await fetchAllModels(config);
|
|
770
|
+
const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
|
|
771
|
+
const presetIds = materializeModelPreset(provider, catalogIds);
|
|
772
|
+
const preset = modelPresetFor(provider)!;
|
|
773
|
+
if (presetIds.length === 0) {
|
|
774
|
+
// NEVER write an empty allowlist from a preset: empty means ALL, so it would silently
|
|
775
|
+
// un-curate instead of curating. Keep the previous selection and record the fallback so
|
|
776
|
+
// the next convergence can retry.
|
|
777
|
+
target.modelPreset = {
|
|
778
|
+
mode: "all",
|
|
779
|
+
appliedVersion: preset.version,
|
|
780
|
+
appliedAt: new Date().toISOString(),
|
|
781
|
+
fallback: "preset-empty",
|
|
782
|
+
};
|
|
783
|
+
persistConfig(config);
|
|
784
|
+
return jsonResponse({
|
|
785
|
+
ok: true,
|
|
786
|
+
provider,
|
|
787
|
+
mode: "all",
|
|
788
|
+
fallback: "preset-empty",
|
|
789
|
+
selected: [...(target.selectedModels ?? [])],
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
target.selectedModels = presetIds;
|
|
793
|
+
target.modelPreset = {
|
|
794
|
+
mode: "preset",
|
|
795
|
+
appliedVersion: preset.version,
|
|
796
|
+
appliedAt: new Date().toISOString(),
|
|
797
|
+
};
|
|
798
|
+
persistConfig(config);
|
|
799
|
+
return jsonResponse({
|
|
800
|
+
ok: true,
|
|
801
|
+
provider,
|
|
802
|
+
mode: "preset",
|
|
803
|
+
appliedVersion: preset.version,
|
|
804
|
+
selected: presetIds,
|
|
805
|
+
catalogRefresh: await convergeCodexCatalog(),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
546
808
|
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
547
809
|
let body: { provider?: unknown; models?: unknown };
|
|
548
810
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
@@ -556,6 +818,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
556
818
|
// Empty list clears the allowlist (provider reverts to exposing all models).
|
|
557
819
|
if (models.length > 0) config.providers[provider].selectedModels = models;
|
|
558
820
|
else delete config.providers[provider].selectedModels;
|
|
821
|
+
// Divergence is detected at the WRITE path, not by diffing (#2465): a user edit while the
|
|
822
|
+
// provider is in preset mode makes the selection theirs, and the proxy must never
|
|
823
|
+
// re-materialize over it afterwards.
|
|
824
|
+
markModelPresetDiverged(config.providers[provider]);
|
|
559
825
|
persistConfig(config);
|
|
560
826
|
const catalogRefresh = await convergeCodexCatalog();
|
|
561
827
|
return jsonResponse({ ok: true, provider, selected: models, catalogRefresh });
|
|
@@ -438,10 +438,20 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
438
438
|
const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown };
|
|
439
439
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
440
440
|
const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
|
|
441
|
-
|
|
441
|
+
const supported = provider === "anthropic" || provider === "google-antigravity" || provider === "cursor" || provider === "command-code";
|
|
442
|
+
if (!supported) return jsonResponse({ error: "clear-cooldown is not supported for this provider" }, 400);
|
|
442
443
|
if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
let cleared = false;
|
|
445
|
+
if (provider === "anthropic") {
|
|
446
|
+
const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
|
|
447
|
+
cleared = clearAnthropicAccountCooldown(accountId);
|
|
448
|
+
} else if (provider === "google-antigravity") {
|
|
449
|
+
const { clearAntigravityAccountCooldown } = await import("../../oauth/antigravity-routing");
|
|
450
|
+
cleared = clearAntigravityAccountCooldown(accountId);
|
|
451
|
+
} else if (provider === "cursor" || provider === "command-code") {
|
|
452
|
+
const { clearPoolAccountCooldown } = await import("../../routing/account-pool");
|
|
453
|
+
cleared = clearPoolAccountCooldown(provider, accountId);
|
|
454
|
+
}
|
|
445
455
|
return jsonResponse({ ok: true, cleared });
|
|
446
456
|
}
|
|
447
457
|
|