@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
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-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
package/src/server/lifecycle.ts
CHANGED
|
@@ -365,12 +365,11 @@ export async function runListenerShutdown(
|
|
|
365
365
|
always: () => Promise<void>,
|
|
366
366
|
): Promise<void> {
|
|
367
367
|
const failures: unknown[] = [];
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
368
|
+
// Close admission and start connection-owner cleanup before waiting for any drain.
|
|
369
|
+
// A graceful listener stop can itself depend on a later owner closing its sockets.
|
|
370
|
+
const results = await Promise.allSettled(steps.map(async step => { await step(); }));
|
|
371
|
+
for (const result of results) {
|
|
372
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
374
373
|
}
|
|
375
374
|
try {
|
|
376
375
|
await always();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const EXTERNAL_CALL_PREFIX = "rtc_ocx_";
|
|
4
|
+
export const LIVE_CALL_TTL_MS = 30 * 60_000;
|
|
5
|
+
const MAX_CALL_BINDINGS = 1024;
|
|
6
|
+
|
|
7
|
+
export interface LiveCallBinding {
|
|
8
|
+
owner: string;
|
|
9
|
+
upstreamCallId: string;
|
|
10
|
+
joinStyle: "frameless-path" | "realtime-query";
|
|
11
|
+
providerName: string;
|
|
12
|
+
accountId?: string;
|
|
13
|
+
chatgptAccountId?: string;
|
|
14
|
+
keyedCredentialDigest?: string;
|
|
15
|
+
callerOwned: boolean;
|
|
16
|
+
sidebandBaseUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Caller-visible IDs are opaque aliases; expired aliases never become legacy upstream IDs. */
|
|
20
|
+
export class LiveCallBindings {
|
|
21
|
+
private entries = new Map<string, { binding: LiveCallBinding; expiresAt: number }>();
|
|
22
|
+
|
|
23
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
24
|
+
|
|
25
|
+
private prune(): void {
|
|
26
|
+
const now = this.now();
|
|
27
|
+
for (const [id, value] of this.entries) if (value.expiresAt <= now) this.entries.delete(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
hasCapacity(): boolean {
|
|
31
|
+
this.prune();
|
|
32
|
+
return this.entries.size < MAX_CALL_BINDINGS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
create(binding: LiveCallBinding): string | null {
|
|
36
|
+
if (!this.hasCapacity()) return null;
|
|
37
|
+
const id = EXTERNAL_CALL_PREFIX + randomUUID().replaceAll("-", "");
|
|
38
|
+
this.entries.set(id, { binding: { ...binding }, expiresAt: this.now() + LIVE_CALL_TTL_MS });
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get(id: string, owner: string): LiveCallBinding | undefined {
|
|
43
|
+
this.prune();
|
|
44
|
+
const value = this.entries.get(id);
|
|
45
|
+
return value?.binding.owner === owner ? { ...value.binding } : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clear(): void { this.entries.clear(); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function upstreamLiveCallId(location: string | null): string | null {
|
|
52
|
+
if (!location || location.length > 4096) return null;
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(location, "https://unused.invalid");
|
|
55
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null;
|
|
56
|
+
const segment = url.pathname.replace(/\/+$/, "").split("/").at(-1) ?? "";
|
|
57
|
+
const id = decodeURIComponent(segment);
|
|
58
|
+
return /^(?:rtc_[A-Za-z0-9_-]+|[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})$/.test(id) && id.length <= 128 ? id : null;
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
}
|
package/src/server/live.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
cooldownErrorResponse,
|
|
36
36
|
CodexAuthContextError,
|
|
37
37
|
CodexMainProfileDrainingError,
|
|
38
|
+
CodexModelAvailabilityError,
|
|
38
39
|
CodexPoolAuthenticationError,
|
|
39
40
|
CodexThreadAffinityExpiredError,
|
|
40
41
|
} from "../codex/auth-context";
|
|
@@ -48,6 +49,7 @@ import type { RequestLogContext } from "./request-log";
|
|
|
48
49
|
import { codexLogAccountId } from "./responses";
|
|
49
50
|
import type { AdmissionLease } from "../lib/admission";
|
|
50
51
|
import { codexAccountSelectionForTurn } from "./lifecycle";
|
|
52
|
+
import { codexModelAvailabilityErrorResponse } from "./responses/codex-auth-error";
|
|
51
53
|
|
|
52
54
|
/** Voice call create can wait on SDP negotiation; bound a hung upstream. */
|
|
53
55
|
const LIVE_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
@@ -371,7 +373,7 @@ export function buildLiveSidebandUpstreamWsUrl(
|
|
|
371
373
|
);
|
|
372
374
|
}
|
|
373
375
|
|
|
374
|
-
async function backendJsonBodyFromApiMultipart(
|
|
376
|
+
export async function backendJsonBodyFromApiMultipart(
|
|
375
377
|
body: ArrayBuffer,
|
|
376
378
|
contentType: string,
|
|
377
379
|
): Promise<{ body: Uint8Array; contentType: string } | Response> {
|
|
@@ -424,14 +426,20 @@ export async function readBodyCapped(
|
|
|
424
426
|
stream: ReadableStream<Uint8Array> | null,
|
|
425
427
|
maxBytes: number,
|
|
426
428
|
tooLargeMessage: (total: number) => string,
|
|
429
|
+
signal?: AbortSignal,
|
|
427
430
|
): Promise<ArrayBuffer | Response> {
|
|
428
431
|
if (!stream) return new ArrayBuffer(0);
|
|
429
432
|
const reader = stream.getReader();
|
|
433
|
+
const abortRead = () => { void reader.cancel(signal?.reason).catch(() => {}); };
|
|
434
|
+
signal?.addEventListener("abort", abortRead, { once: true });
|
|
430
435
|
const chunks: Uint8Array[] = [];
|
|
431
436
|
let total = 0;
|
|
432
437
|
try {
|
|
438
|
+
if (signal?.aborted) abortRead();
|
|
439
|
+
signal?.throwIfAborted();
|
|
433
440
|
for (;;) {
|
|
434
441
|
const { done, value } = await reader.read();
|
|
442
|
+
signal?.throwIfAborted();
|
|
435
443
|
if (done) break;
|
|
436
444
|
if (!value || value.byteLength === 0) continue;
|
|
437
445
|
total += value.byteLength;
|
|
@@ -450,6 +458,7 @@ export async function readBodyCapped(
|
|
|
450
458
|
await reader.cancel(err).catch(() => {});
|
|
451
459
|
throw err;
|
|
452
460
|
} finally {
|
|
461
|
+
signal?.removeEventListener("abort", abortRead);
|
|
453
462
|
try {
|
|
454
463
|
// Always release: `reader.cancel()` does NOT drop the lock, and holding it would leave
|
|
455
464
|
// the stream permanently locked for any later consumer (audit R-WP5-2).
|
|
@@ -558,6 +567,8 @@ export async function resolveLiveRelay(
|
|
|
558
567
|
"authentication_error",
|
|
559
568
|
"Selected Codex account needs reauthentication",
|
|
560
569
|
);
|
|
570
|
+
} else if (err instanceof CodexModelAvailabilityError) {
|
|
571
|
+
forwardAuthError = codexModelAvailabilityErrorResponse(err);
|
|
561
572
|
} else if (err instanceof CodexPoolAuthenticationError) {
|
|
562
573
|
forwardAuthError = formatErrorResponse(401, "authentication_error", err.message);
|
|
563
574
|
} else {
|
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import type { CatalogModel } from "../../codex/catalog";
|
|
4
4
|
import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
5
|
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
|
|
6
|
+
import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
|
|
6
7
|
import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_SUBAGENT_MODELS,
|
|
@@ -243,7 +244,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
243
244
|
enabled,
|
|
244
245
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
245
246
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
246
|
-
|
|
247
|
+
// Resolved, not raw: a hand-edited unsupported value survives config parsing, and the
|
|
248
|
+
// advisory below already reports the resolved surface. Two answers would disagree.
|
|
249
|
+
multiAgentMode: resolveMultiAgentMode(config),
|
|
250
|
+
multiAgentSurfaceAdvisory: multiAgentSurfaceAdvisory(config),
|
|
247
251
|
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
248
252
|
agentsEnabled: getAgentsEnabled(),
|
|
249
253
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
@@ -265,6 +269,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
265
269
|
agentsMaxDepth?: unknown;
|
|
266
270
|
subagentDeveloperInstructions?: unknown;
|
|
267
271
|
multiAgentModeHintText?: unknown;
|
|
272
|
+
multiAgentSurfaceAdvisoryAcknowledged?: unknown;
|
|
268
273
|
};
|
|
269
274
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
270
275
|
const wantsFlag = body.enabled !== undefined;
|
|
@@ -275,8 +280,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
275
280
|
const wantsMaxDepth = body.agentsMaxDepth !== undefined;
|
|
276
281
|
const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
|
|
277
282
|
const wantsModeHintText = body.multiAgentModeHintText !== undefined;
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
const wantsAdvisoryAck = body.multiAgentSurfaceAdvisoryAcknowledged !== undefined;
|
|
284
|
+
if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsAdvisoryAck) {
|
|
285
|
+
return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, multiAgentModeHintText, and/or multiAgentSurfaceAdvisoryAcknowledged" }, 400);
|
|
280
286
|
}
|
|
281
287
|
if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
282
288
|
if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
|
|
@@ -311,6 +317,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
311
317
|
&& (typeof body.multiAgentModeHintText !== "string" || body.multiAgentModeHintText.trim().length === 0)) {
|
|
312
318
|
return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
|
|
313
319
|
}
|
|
320
|
+
// A boolean, and only `true` acknowledges. `false` is an explicit no-op so a client
|
|
321
|
+
// that always sends the field cannot un-answer an advisory it already dismissed.
|
|
322
|
+
if (wantsAdvisoryAck && typeof body.multiAgentSurfaceAdvisoryAcknowledged !== "boolean") {
|
|
323
|
+
return jsonResponse({ error: "body.multiAgentSurfaceAdvisoryAcknowledged must be a boolean" }, 400);
|
|
324
|
+
}
|
|
314
325
|
const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
|
|
315
326
|
const effectiveMode = mode ?? config.multiAgentMode ?? "default";
|
|
316
327
|
const effectiveKeepNative = wantsKeepNative
|
|
@@ -378,6 +389,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
378
389
|
: "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
|
|
379
390
|
: "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
|
|
380
391
|
}
|
|
392
|
+
// Written after the mode, so a landed acknowledgement implies the mode write it was
|
|
393
|
+
// sent with already landed. The converse does not hold: this writer throws rather than
|
|
394
|
+
// reporting, exactly as the mode write above it does, so a failure here surfaces the
|
|
395
|
+
// mode change with the advisory still raised. That is the benign direction — the
|
|
396
|
+
// operator is asked again — and it is why this is not claimed as a transaction.
|
|
397
|
+
if (wantsAdvisoryAck && body.multiAgentSurfaceAdvisoryAcknowledged === true) {
|
|
398
|
+
config.multiAgentSurfaceAdvisoryVersion = MULTI_AGENT_SURFACE_ADVISORY_VERSION;
|
|
399
|
+
saveConfigPreservingClaudeCode(config);
|
|
400
|
+
}
|
|
381
401
|
// New-key scalar writes: each writer is individually atomic, so apply them in
|
|
382
402
|
// sequence after the transition. A failure here is a persistence failure (the
|
|
383
403
|
// writers' ok:false result or a throw from the underlying atomic write helper),
|
|
@@ -417,7 +437,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
417
437
|
enabled,
|
|
418
438
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
419
439
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
420
|
-
multiAgentMode: config
|
|
440
|
+
multiAgentMode: resolveMultiAgentMode(config),
|
|
441
|
+
multiAgentSurfaceAdvisory: multiAgentSurfaceAdvisory(config),
|
|
421
442
|
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
422
443
|
agentsEnabled: getAgentsEnabled(),
|
|
423
444
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
@@ -2,6 +2,21 @@ import type { OcxConfig } from "../../types";
|
|
|
2
2
|
import { isWildcardHostname } from "../../codex/loopback-target";
|
|
3
3
|
import { localInferenceDestination } from "../../lib/local-destinations";
|
|
4
4
|
import { probeHostname } from "../proxy-liveness";
|
|
5
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers-destination";
|
|
6
|
+
import { LIVE_AUDIO_MODEL, TRANSCRIPTION_MODEL } from "../audio-upstream";
|
|
7
|
+
|
|
8
|
+
export interface AudioApiAccess {
|
|
9
|
+
transcriptionEndpoint: string;
|
|
10
|
+
dictationStreamEndpoint: string;
|
|
11
|
+
liveEndpoint: string;
|
|
12
|
+
realtimeCallsEndpoint: string;
|
|
13
|
+
transcriptionModel: string;
|
|
14
|
+
liveModel: string;
|
|
15
|
+
/** Configuration only, not account health, entitlement or observed connectivity. */
|
|
16
|
+
transcriptionConfigured: boolean;
|
|
17
|
+
dictationConfigured: boolean;
|
|
18
|
+
liveConfigured: boolean;
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
export interface ApiAccessEndpoints {
|
|
7
22
|
baseUrl: string;
|
|
@@ -10,6 +25,7 @@ export interface ApiAccessEndpoints {
|
|
|
10
25
|
messagesEndpoint: string;
|
|
11
26
|
modelsEndpoint: string;
|
|
12
27
|
claudeCodeEnabled: boolean;
|
|
28
|
+
audio: AudioApiAccess;
|
|
13
29
|
/** Back-compat alias for older GUI clients. */
|
|
14
30
|
endpoint: string;
|
|
15
31
|
}
|
|
@@ -140,6 +156,16 @@ export function buildApiAccessEndpoints(
|
|
|
140
156
|
): ApiAccessEndpoints {
|
|
141
157
|
const baseUrl = resolveApiAccessBaseUrl(config, opts);
|
|
142
158
|
const responsesEndpoint = `${baseUrl}/responses`;
|
|
159
|
+
const socketBase = new URL(baseUrl);
|
|
160
|
+
socketBase.protocol = socketBase.protocol === "https:" ? "wss:" : "ws:";
|
|
161
|
+
const forward = config.providers?.[OPENAI_CODEX_PROVIDER_ID];
|
|
162
|
+
const chatgptConfigured = !!forward && forward.disabled !== true
|
|
163
|
+
&& isCanonicalOpenAiForwardProvider({ ...forward, authMode: forward.authMode ?? "forward" });
|
|
164
|
+
const keyed = config.providers?.[OPENAI_API_PROVIDER_ID];
|
|
165
|
+
// Do not resolve key references or inspect accounts on a management metadata read.
|
|
166
|
+
const apiConfigured = !!keyed && keyed.disabled !== true && keyed.adapter === "openai-responses"
|
|
167
|
+
&& keyed.authMode !== "forward" && keyed.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1"
|
|
168
|
+
&& typeof keyed.apiKey === "string" && !!keyed.apiKey.trim();
|
|
143
169
|
return {
|
|
144
170
|
baseUrl,
|
|
145
171
|
responsesEndpoint,
|
|
@@ -147,6 +173,17 @@ export function buildApiAccessEndpoints(
|
|
|
147
173
|
messagesEndpoint: `${baseUrl}/messages`,
|
|
148
174
|
modelsEndpoint: `${baseUrl}/models`,
|
|
149
175
|
claudeCodeEnabled: config.claudeCode?.enabled !== false,
|
|
176
|
+
audio: {
|
|
177
|
+
transcriptionEndpoint: `${baseUrl}/audio/transcriptions`,
|
|
178
|
+
dictationStreamEndpoint: `${socketBase.href}/audio/transcriptions/stream`,
|
|
179
|
+
liveEndpoint: `${socketBase.href}/live`,
|
|
180
|
+
realtimeCallsEndpoint: `${baseUrl}/realtime/calls`,
|
|
181
|
+
transcriptionModel: TRANSCRIPTION_MODEL,
|
|
182
|
+
liveModel: LIVE_AUDIO_MODEL,
|
|
183
|
+
transcriptionConfigured: chatgptConfigured || apiConfigured,
|
|
184
|
+
dictationConfigured: chatgptConfigured,
|
|
185
|
+
liveConfigured: chatgptConfigured,
|
|
186
|
+
},
|
|
150
187
|
endpoint: responsesEndpoint,
|
|
151
188
|
};
|
|
152
189
|
}
|
|
@@ -19,6 +19,9 @@ export type ApiKeyUsage =
|
|
|
19
19
|
export interface ApiKeyUsageSnapshot {
|
|
20
20
|
rollup: Map<string, ApiKeyUsage>;
|
|
21
21
|
historyTruncated?: true;
|
|
22
|
+
/** Positive evidence of skipped oversized rows; absence is not a completeness guarantee. */
|
|
23
|
+
usageIncomplete?: true;
|
|
24
|
+
usageIncompleteReason?: "oversized_rows";
|
|
22
25
|
/**
|
|
23
26
|
* Earliest row carrying a recognized `admissionKind`. A property of the DATA
|
|
24
27
|
* SET, not of a key, so it is singular and lives beside the map: it is what
|
|
@@ -223,9 +226,11 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
|
|
|
223
226
|
const flight = (async (): Promise<ApiKeyUsageSnapshot> => {
|
|
224
227
|
const accumulator = createApiKeyUsageAccumulator(configuredIds, now);
|
|
225
228
|
const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
|
|
226
|
-
if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
|
|
227
229
|
return cacheApiKeyUsageFromRollup(
|
|
228
|
-
|
|
230
|
+
{
|
|
231
|
+
...accumulator.snapshot(),
|
|
232
|
+
...(scan.oversizedRows > 0 ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
|
|
233
|
+
},
|
|
229
234
|
configuredIds,
|
|
230
235
|
usageLogIdentityKey(scan.revision),
|
|
231
236
|
scan.revision?.size ?? 0,
|
|
@@ -316,9 +316,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
316
316
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
317
317
|
codexAccountPickerEnabled: codexAccountPickerEnabled(config),
|
|
318
318
|
codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
|
|
319
|
-
// Absent means hidden, so the GUI renders the switch without having to know that
|
|
320
|
-
// `undefined` and `false` mean the same thing.
|
|
321
|
-
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
322
319
|
// Absent means off, same convention: the GUI renders a plain switch without
|
|
323
320
|
// needing to know that `undefined` and `false` mean the same thing here.
|
|
324
321
|
ultraFastTier: config.ultraFastTier === true,
|
|
@@ -417,7 +414,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
417
414
|
codexAccountPickerEnabled?: unknown;
|
|
418
415
|
codexQuotaAutoRefresh?: unknown;
|
|
419
416
|
oauthOpenBrowser?: unknown;
|
|
420
|
-
showCodexSparkQuota?: unknown;
|
|
421
417
|
ultraFastTier?: unknown;
|
|
422
418
|
codexMainAccountHardLock?: unknown;
|
|
423
419
|
codexDesktopAuthless?: unknown;
|
|
@@ -429,12 +425,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
429
425
|
&& body.codexAccountPickerEnabled === undefined
|
|
430
426
|
&& body.codexQuotaAutoRefresh === undefined
|
|
431
427
|
&& body.oauthOpenBrowser === undefined
|
|
432
|
-
&& body.showCodexSparkQuota === undefined
|
|
433
428
|
&& body.ultraFastTier === undefined
|
|
434
429
|
&& body.codexMainAccountHardLock === undefined
|
|
435
430
|
&& body.codexDesktopAuthless === undefined
|
|
436
431
|
&& body.codexClientCompaction === undefined) {
|
|
437
|
-
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser,
|
|
432
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
|
|
438
433
|
}
|
|
439
434
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
440
435
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -449,9 +444,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
449
444
|
&& typeof body.codexAccountPickerEnabled !== "boolean") {
|
|
450
445
|
return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400);
|
|
451
446
|
}
|
|
452
|
-
if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
|
|
453
|
-
return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
|
|
454
|
-
}
|
|
455
447
|
if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
|
|
456
448
|
return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
|
|
457
449
|
}
|
|
@@ -508,8 +500,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
508
500
|
hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"),
|
|
509
501
|
oauthOpenBrowser: config.oauthOpenBrowser,
|
|
510
502
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
511
|
-
showCodexSparkQuota: config.showCodexSparkQuota,
|
|
512
|
-
hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
|
|
513
503
|
ultraFastTier: config.ultraFastTier,
|
|
514
504
|
hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
|
|
515
505
|
codexMainAccountHardLock: config.codexMainAccountHardLock,
|
|
@@ -546,9 +536,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
546
536
|
if (typeof body.oauthOpenBrowser === "boolean") {
|
|
547
537
|
config.oauthOpenBrowser = body.oauthOpenBrowser;
|
|
548
538
|
}
|
|
549
|
-
if (typeof body.showCodexSparkQuota === "boolean") {
|
|
550
|
-
config.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
551
|
-
}
|
|
552
539
|
// Off deletes the key rather than persisting `false`: absent is the documented
|
|
553
540
|
// default, and a written `false` would survive as a decision nobody made.
|
|
554
541
|
if (body.ultraFastTier === true) config.ultraFastTier = true;
|
|
@@ -592,9 +579,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
592
579
|
if (previousSettings.hasOauthOpenBrowser) {
|
|
593
580
|
config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
|
|
594
581
|
} else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
|
|
595
|
-
if (previousSettings.hasShowCodexSparkQuota) {
|
|
596
|
-
config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
|
|
597
|
-
} else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
|
|
598
582
|
if (previousSettings.hasUltraFastTier) {
|
|
599
583
|
config.ultraFastTier = previousSettings.ultraFastTier;
|
|
600
584
|
} else deleteConfigTopLevelKey(config, "ultraFastTier");
|
|
@@ -636,7 +620,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
636
620
|
codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
|
|
637
621
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
638
622
|
catalogRefreshPending,
|
|
639
|
-
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
640
623
|
codexDesktopAuthless: authlessIsEnabled,
|
|
641
624
|
codexClientCompaction: clientCompactionIsEnabled,
|
|
642
625
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
@@ -20,7 +20,22 @@ import type {
|
|
|
20
20
|
readCodexAppServerState,
|
|
21
21
|
} from "../../codex/app-server-restart-service";
|
|
22
22
|
|
|
23
|
+
import type { RemoteWorkspaceHub } from "../../remote-control/workspace-hub";
|
|
24
|
+
import type { RemoteWorkspaceSessionService } from "../../remote-control/workspace-sessions";
|
|
25
|
+
|
|
26
|
+
export type RemoteWorkspaceHubApi = Pick<RemoteWorkspaceHub,
|
|
27
|
+
"identity" | "createPairingGrant" | "assertPairingSourceAllowed" | "pairDevice"
|
|
28
|
+
| "authenticateDeviceToken" | "attachConnection" | "updateDeviceCapabilities"
|
|
29
|
+
| "detachConnection" | "listDevices" | "revokeDevice" | "closeAllConnections">;
|
|
30
|
+
export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
|
|
31
|
+
"availability" | "list" | "create" | "prompt" | "submitPrompt" | "stop" | "shutdown">;
|
|
32
|
+
|
|
23
33
|
export interface ManagementApiDeps {
|
|
34
|
+
remoteWorkspaceHub?: RemoteWorkspaceHubApi;
|
|
35
|
+
remoteWorkspaceSessions?: RemoteWorkspaceSessionsApi;
|
|
36
|
+
/** The listener retains and awaits teardown only after this optional subsystem activates. */
|
|
37
|
+
remoteWorkspaceStopping?: () => boolean;
|
|
38
|
+
onRemoteWorkspaceShutdown?: (shutdown: () => Promise<void>) => void;
|
|
24
39
|
/** Isolates automatic owned-client writes in route tests. */
|
|
25
40
|
refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
|
|
26
41
|
/** Platform seam for capability projections; does not alter host-level startup behavior. */
|
|
@@ -225,6 +225,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
225
225
|
const accumulator = filteredAggregate.accumulator;
|
|
226
226
|
return jsonResponse({
|
|
227
227
|
...accumulator.summarize(range, now, surface),
|
|
228
|
+
...(filteredAggregate.usageIncomplete ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
|
|
228
229
|
historyTruncated: false,
|
|
229
230
|
truncatedPrefixBytes: 0,
|
|
230
231
|
entriesTruncated: false,
|
|
@@ -247,6 +248,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
247
248
|
const revisionKey = `${usageLogRevisionKey(aggregate.revision)}\0${effectiveReadLimit}`;
|
|
248
249
|
const lastSeenSize = aggregate.revision?.size ?? 0;
|
|
249
250
|
const baseReadMetadata = {
|
|
251
|
+
...(aggregate.usageIncomplete ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
|
|
250
252
|
historyTruncated: false,
|
|
251
253
|
truncatedPrefixBytes: 0,
|
|
252
254
|
entriesTruncated: false,
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
normalizeAccountPoolStrategy,
|
|
40
40
|
parseAccountPoolStickyLimit,
|
|
41
41
|
parseAccountPoolStrategy,
|
|
42
|
+
parseCodexAccountPoolStrategy,
|
|
42
43
|
} from "../../codex/pool-rotation";
|
|
43
44
|
import { normalizeAccountPoolQuotaWindow, parseAccountPoolQuotaWindow } from "../../oauth/anthropic-routing";
|
|
44
45
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
@@ -78,6 +79,25 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
78
79
|
import type { ManagementContext } from "./context";
|
|
79
80
|
import { readManagementJsonBody, readManagementJsonBodyOr, rethrowManagementBodyTooLarge } from "./body";
|
|
80
81
|
import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Provider ids that share the Devin cloud-direct client, and therefore share its
|
|
85
|
+
* process-memory caches.
|
|
86
|
+
*
|
|
87
|
+
* `devin-cli` is a deprecated alias for the merged `devin` provider, but a
|
|
88
|
+
* config row the startup migration has not rekeyed yet can still arrive here —
|
|
89
|
+
* and its logout/removal must clear the same caches, because both ids hand the
|
|
90
|
+
* same api_key to the same client and one cache serves both.
|
|
91
|
+
*/
|
|
92
|
+
function isDevinCloudDirectProvider(provider: string): boolean {
|
|
93
|
+
return provider === "devin" || provider === "devin-cli";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function clearDevinCloudDirectCaches(): Promise<void> {
|
|
97
|
+
const { clearCachedUserJwt, clearCachedCatalog } = await import("../../adapters/devin/cloud-direct");
|
|
98
|
+
clearCachedUserJwt();
|
|
99
|
+
clearCachedCatalog();
|
|
100
|
+
}
|
|
81
101
|
import { ACCOUNT_IMPORT_DEADLINE_MS, ACCOUNT_IMPORT_MAX_REQUEST_BYTES } from "../../oauth/account-import";
|
|
82
102
|
import { readBoundedJsonRequestBody } from "../request-decompress";
|
|
83
103
|
|
|
@@ -255,14 +275,12 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
255
275
|
const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
|
|
256
276
|
clearProviderQuotaCache();
|
|
257
277
|
clearAccountQuotaCache(provider);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
clearCachedCatalog();
|
|
265
|
-
}
|
|
278
|
+
// The cached user_jwt's payload contains the api_key, and the catalog is
|
|
279
|
+
// keyed by that key. Without this they outlive the credential in process
|
|
280
|
+
// memory until the JWT's own ~24 minute expiry. `devin-cli` is a deprecated
|
|
281
|
+
// alias whose unmigrated rows share the one cache, so gating on `devin`
|
|
282
|
+
// alone left a CLI-imported key's JWT resident after its own logout.
|
|
283
|
+
if (isDevinCloudDirectProvider(provider)) await clearDevinCloudDirectCaches();
|
|
266
284
|
return jsonResponse({ success: true });
|
|
267
285
|
}
|
|
268
286
|
|
|
@@ -325,7 +343,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
325
343
|
return {
|
|
326
344
|
...account,
|
|
327
345
|
quota: row.quota,
|
|
328
|
-
...(quotaMode === "probe" ? { quotaUnavailable: row.unavailable === true
|
|
346
|
+
...(quotaMode === "probe" ? { quotaUnavailable: row.unavailable === true,
|
|
347
|
+
...(row.unavailable && row.quotaFailure && row.quotaFailureIsCurrent?.() === true ? { quotaFailure: row.quotaFailure } : {}),
|
|
348
|
+
} : {}),
|
|
329
349
|
};
|
|
330
350
|
}),
|
|
331
351
|
});
|
|
@@ -383,8 +403,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
383
403
|
// sticky limit is refused identically whichever pool is addressed.
|
|
384
404
|
let strategy: string | undefined;
|
|
385
405
|
if (fields.strategy !== undefined) {
|
|
386
|
-
const parsed = parseGenericPoolStrategy(fields.strategy);
|
|
387
|
-
if (parsed === null) return jsonResponse({ error:
|
|
406
|
+
const parsed = kind === "codex" ? parseCodexAccountPoolStrategy(fields.strategy) : parseGenericPoolStrategy(fields.strategy);
|
|
407
|
+
if (parsed === null) return jsonResponse({ error: kind === "codex"
|
|
408
|
+
? "strategy must be one of: quota, round-robin, fill-first, reset-first"
|
|
409
|
+
: "strategy must be one of: quota, round-robin, fill-first" }, 400);
|
|
388
410
|
strategy = parsed;
|
|
389
411
|
}
|
|
390
412
|
let stickyLimit: number | undefined;
|
|
@@ -685,6 +707,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
685
707
|
const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
|
|
686
708
|
clearProviderQuotaCache();
|
|
687
709
|
clearAccountQuotaCache(provider);
|
|
710
|
+
// Same reasoning as logout. Removing the last account for a provider used to
|
|
711
|
+
// leave the JWT and catalog in memory, because only the logout route cleared
|
|
712
|
+
// them.
|
|
713
|
+
if (isDevinCloudDirectProvider(provider)) await clearDevinCloudDirectCaches();
|
|
688
714
|
return jsonResponse({ ok: true });
|
|
689
715
|
}
|
|
690
716
|
|
|
@@ -833,7 +859,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
833
859
|
requestOrigin: req.headers.get("origin"),
|
|
834
860
|
});
|
|
835
861
|
const { readApiKeyUsageRollup } = await import("./api-key-usage");
|
|
836
|
-
const { rollup, attributionSince, historyTruncated } = await readApiKeyUsageRollup(keys.map(k => k.id), config.managementUsageMaxReadBytes);
|
|
862
|
+
const { rollup, attributionSince, historyTruncated, usageIncomplete, usageIncompleteReason } = await readApiKeyUsageRollup(keys.map(k => k.id), config.managementUsageMaxReadBytes);
|
|
837
863
|
return jsonResponse({
|
|
838
864
|
// 8 random hex past the fixed `ocx_data_` literal: enough to tell two keys
|
|
839
865
|
// apart in a list, with 128 bits of the tail still unrevealed. Masking only
|
|
@@ -853,6 +879,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
853
879
|
// Dataset-level and singular: it describes the usage log, not any one key.
|
|
854
880
|
...(attributionSince ? { attributionSince } : {}),
|
|
855
881
|
...(historyTruncated ? { historyTruncated: true } : {}),
|
|
882
|
+
...(usageIncomplete ? { usageIncomplete: true, usageIncompleteReason } : {}),
|
|
856
883
|
authMatrix: AUTH_MATRIX,
|
|
857
884
|
...endpoints,
|
|
858
885
|
}, 200, req, config);
|