@bitkyc08/opencodex 2.79.0 → 2.81.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/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +10 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +347 -199
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +16 -16
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +21 -18
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/desktop-startup.ts +100 -3
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
package/src/server/gui-static.ts
CHANGED
|
@@ -20,12 +20,46 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
20
20
|
*/
|
|
21
21
|
const HASHED_ASSET_PATTERN = /-[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9]+$/;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** `productName` in desktop/src-tauri/tauri.conf.json; Linux packages install resources under `lib/<productName>`. */
|
|
24
|
+
export const DESKTOP_PRODUCT_NAME = "OpenCodex";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where a compiled `ocx` looks for `gui/dist`, given the directory holding the executable.
|
|
28
|
+
*
|
|
29
|
+
* Beside the binary covers a release archive and the Windows desktop install. The desktop shell
|
|
30
|
+
* ships `ocx` as a sidecar and its resources where Tauri's `resource_dir` puts them: `../Resources`
|
|
31
|
+
* inside a macOS app bundle, `../lib/<productName>` for a Linux package (`/usr/bin`, `/usr/local/bin`,
|
|
32
|
+
* an AppImage's `usr/bin`). The shell passes that directory as OPENCODEX_GUI_DIST only to the sidecar
|
|
33
|
+
* it starts itself; `ocx ensure` from the Codex shim and the login service start the same binary
|
|
34
|
+
* without the variable, so they find the dashboard here.
|
|
35
|
+
*/
|
|
36
|
+
export function standaloneGuiDistCandidates(executableDir: string, platform: NodeJS.Platform = process.platform): string[] {
|
|
37
|
+
const candidates = [join(executableDir, "gui", "dist")];
|
|
38
|
+
if (platform === "darwin") candidates.push(join(executableDir, "..", "Resources", "gui", "dist"));
|
|
39
|
+
if (platform === "linux") candidates.push(join(executableDir, "..", "lib", DESKTOP_PRODUCT_NAME, "gui", "dist"));
|
|
40
|
+
return candidates;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Overrides for tests; production callers pass nothing and get this process's own layout. */
|
|
44
|
+
export interface GuiDistLookup {
|
|
45
|
+
/** Directory holding a compiled `ocx`, or null when running from source. */
|
|
46
|
+
standaloneDir?: string | null;
|
|
47
|
+
platform?: NodeJS.Platform;
|
|
48
|
+
/** Directory of this module; a source checkout's `gui/dist` is resolved from it. */
|
|
49
|
+
moduleDir?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Lookup only: checks for `index.html` and never creates, copies or builds anything. */
|
|
53
|
+
export function findGuiDist(lookup: GuiDistLookup = {}): string | null {
|
|
54
|
+
const standaloneDir = lookup.standaloneDir === undefined
|
|
55
|
+
? (isStandaloneBinary() ? standaloneRoot() : null)
|
|
56
|
+
: lookup.standaloneDir;
|
|
57
|
+
const moduleDir = lookup.moduleDir ?? import.meta.dir;
|
|
24
58
|
const candidates = [
|
|
25
59
|
process.env.OPENCODEX_GUI_DIST,
|
|
26
|
-
...(
|
|
27
|
-
join(
|
|
28
|
-
join(
|
|
60
|
+
...(standaloneDir === null ? [] : standaloneGuiDistCandidates(standaloneDir, lookup.platform)),
|
|
61
|
+
join(moduleDir, "..", "..", "gui", "dist"),
|
|
62
|
+
join(moduleDir, "..", "..", "..", "gui", "dist"),
|
|
29
63
|
].filter((candidate): candidate is string => Boolean(candidate));
|
|
30
64
|
for (const c of candidates) {
|
|
31
65
|
if (existsSync(join(c, "index.html"))) return c;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
configureSharedSpendLedger,
|
|
8
8
|
spendPolicyFromConfig,
|
|
9
|
+
waitForSharedSpendReporterDrain,
|
|
9
10
|
} from "../../lib/spend-reservation-ledger";
|
|
10
11
|
import { warnIfSyncedStateDirectory } from "../../lib/synced-state-location";
|
|
11
12
|
|
|
@@ -26,7 +27,7 @@ export function waitForFailedStartRollback(error: unknown): Promise<void> {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface SpendLedgerServerLifecycle {
|
|
29
|
-
configure(spend: OcxSpendConfig | undefined): void;
|
|
30
|
+
configure(spend: OcxSpendConfig | undefined, poolAliases?: unknown, providerIds?: readonly string[]): void;
|
|
30
31
|
track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T;
|
|
31
32
|
release(): void;
|
|
32
33
|
releaseAfterFailedStart(): Promise<void>;
|
|
@@ -49,13 +50,18 @@ export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedge
|
|
|
49
50
|
owner.release();
|
|
50
51
|
};
|
|
51
52
|
return {
|
|
52
|
-
configure(spend): void {
|
|
53
|
-
configureSharedSpendLedger(spendPolicyFromConfig(spend));
|
|
53
|
+
configure(spend, poolAliases, providerIds): void {
|
|
54
|
+
configureSharedSpendLedger(spendPolicyFromConfig(spend, poolAliases, providerIds));
|
|
54
55
|
},
|
|
55
56
|
track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T {
|
|
56
57
|
// Capture the raw stop before startServer replaces the public method with full teardown.
|
|
57
58
|
const stop = server.stop.bind(server);
|
|
58
|
-
|
|
59
|
+
const drainedStop = async (closeActiveConnections?: boolean): Promise<void> => {
|
|
60
|
+
await stop(closeActiveConnections);
|
|
61
|
+
await waitForSharedSpendReporterDrain();
|
|
62
|
+
};
|
|
63
|
+
Object.defineProperty(server, "stop", { configurable: true, value: drainedStop });
|
|
64
|
+
failedStartStops.push(() => drainedStop(true));
|
|
59
65
|
return server;
|
|
60
66
|
},
|
|
61
67
|
release,
|
package/src/server/index.ts
CHANGED
|
@@ -302,7 +302,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
302
302
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
303
303
|
enforceAppOwnedMemoryBudget();
|
|
304
304
|
// Observe-only mode still journals physical sends, so every server owns before configuring.
|
|
305
|
-
spendLedgerLifecycle.configure(config.spend);
|
|
305
|
+
spendLedgerLifecycle.configure(config.spend, config.spendPoolAliases, Object.keys(config.providers));
|
|
306
306
|
// After ownership: a second server on the same home is refused above, so the process running
|
|
307
307
|
// this line is the only one appending to usage.jsonl and the only one that may compact it.
|
|
308
308
|
setUsageLedgerRetention(config.usageLedgerMaxBytes);
|
|
@@ -463,21 +463,19 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
463
463
|
return serveGuiFile(rawPath) !== null;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
// Codex
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// back.
|
|
466
|
+
// Codex maps 503 + server_is_overloaded to ServerOverloaded ("model at capacity"), retried
|
|
467
|
+
// only with retry advice that older clients ignore (#6642); server_restarting is a
|
|
468
|
+
// retryable UnexpectedStatus in every version and never reads as model capacity.
|
|
469
|
+
// Keep JSON (empty / non-JSON 503 means "Unknown error", #452) and Retry-After.
|
|
470
|
+
// Drain and busy run BEFORE auth/origin checks: use the receiving listener's policy
|
|
471
|
+
// (#1102), because shared public `config` could echo a rebinding origin on loopback.
|
|
473
472
|
function drainingResponse(req: Request, policy: RequestPolicyView): Response {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return new Response(response.body, { status: 503, headers });
|
|
473
|
+
return withCors(new Response(JSON.stringify({
|
|
474
|
+
error: { type: "server_error", code: "server_restarting", message: "OpenCodex is restarting; retry this request." },
|
|
475
|
+
}), {
|
|
476
|
+
status: 503,
|
|
477
|
+
headers: { "Content-Type": "application/json", "Retry-After": "5" },
|
|
478
|
+
}), req, policy);
|
|
481
479
|
}
|
|
482
480
|
|
|
483
481
|
function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
|
|
@@ -507,7 +505,9 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
507
505
|
const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
508
506
|
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
509
507
|
// Root, lane, and the refusal that follows from them, all live in ./workflow-refusal.
|
|
510
|
-
|
|
508
|
+
let workflow: ReturnType<typeof admitHttpWorkflowTurn>;
|
|
509
|
+
try { workflow = admitHttpWorkflowTurn(req.headers); }
|
|
510
|
+
catch (error) { lease.release(); throw error; }
|
|
511
511
|
if (workflow && !workflow.admitted) {
|
|
512
512
|
lease.release();
|
|
513
513
|
// withCors, because without Access-Control-Allow-Origin the exposed refusal header is
|
|
@@ -34,7 +34,7 @@ export function createInferenceSendBudget(
|
|
|
34
34
|
export function expandInferenceOAuthSendBudget(budget: TransientSendBudget | undefined, accounts: number): void {
|
|
35
35
|
if (!budget) return;
|
|
36
36
|
const policy = ingressPolicies.get(budget);
|
|
37
|
-
if (!policy || budget.used !== 0) return;
|
|
37
|
+
if (!policy || budget.used !== 0 || ("physicalStarted" in budget && Number(budget.physicalStarted) > 0)) return;
|
|
38
38
|
ingressPolicies.delete(budget);
|
|
39
39
|
if (accounts < 2 || !Number.isSafeInteger(accounts)) return;
|
|
40
40
|
const sends = Math.min(accounts, GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST) * TRANSIENT_RETRY_MAX_ATTEMPTS;
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -479,7 +479,7 @@ export async function drainAndShutdown(
|
|
|
479
479
|
beginBackgroundShellShutdown();
|
|
480
480
|
try {
|
|
481
481
|
while (admittedTurns.size > 0 && Date.now() < deadline) {
|
|
482
|
-
await Bun.sleep(100);
|
|
482
|
+
await Bun.sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
483
483
|
}
|
|
484
484
|
if (admittedTurns.size > 0) {
|
|
485
485
|
console.warn(`⚠️ Aborting ${admittedTurns.size} in-flight turn(s) after ${timeoutMs}ms deadline`);
|
|
@@ -8,6 +8,8 @@ import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCach
|
|
|
8
8
|
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
|
|
9
9
|
import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
|
|
10
10
|
import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
|
|
11
|
+
import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
|
|
12
|
+
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
11
13
|
import {
|
|
12
14
|
adoptPersistedClaudeCode,
|
|
13
15
|
DEFAULT_SUBAGENT_MODELS,
|
|
@@ -71,9 +73,13 @@ import {
|
|
|
71
73
|
webSearchCandidateRows,
|
|
72
74
|
webSearchModelIsRejected,
|
|
73
75
|
webSearchModelRejection,
|
|
76
|
+
sidecarAnthropicPoolOptions,
|
|
77
|
+
sidecarSettingsAfterPatch,
|
|
78
|
+
sidecarOptionsAuth,
|
|
74
79
|
type WebSearchBackend,
|
|
75
80
|
} from "./web-search-sidecar-options";
|
|
76
81
|
import { drainAndShutdown } from "../lifecycle";
|
|
82
|
+
import { inheritedAnthropicInstance } from "../../sidecar/auth";
|
|
77
83
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
78
84
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
79
85
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
@@ -1368,6 +1374,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1368
1374
|
);
|
|
1369
1375
|
const webSearchOverride = config.claudeCode?.webSearchSidecar;
|
|
1370
1376
|
const visionOverride = config.claudeCode?.visionSidecar;
|
|
1377
|
+
const helperParent = config.claudeCode?.model?.includes("/") ? config.claudeCode.model.split("/")[0] : undefined;
|
|
1378
|
+
const helperVision = { ...config.visionSidecar, ...visionOverride };
|
|
1379
|
+
// Same pool the runtime picks (vision/plan.ts preferredVisionBackend): explicit, else inherited
|
|
1380
|
+
// from the Claude model's instance, so a usable inherited Pool 2 reports the Anthropic backend.
|
|
1381
|
+
const helperVisionInstance = helperVision.anthropicInstance ?? inheritedAnthropicInstance(config, helperParent);
|
|
1382
|
+
const helperVisionBackend = helperVision.backend ?? (sidecarOptionsAuth(config, helperVisionInstance).isAnthropicAuth ? "anthropic" : "openai");
|
|
1371
1383
|
const { firstPartyDesired, readFirstPartyProxyStatus } = await import("../../claude/first-party-settings");
|
|
1372
1384
|
const { observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
1373
1385
|
const { claudeInterceptEnabled, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
|
|
@@ -1411,11 +1423,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1411
1423
|
autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
|
|
1412
1424
|
blockedSkills: config.claudeCode?.blockedSkills ?? null,
|
|
1413
1425
|
injectAgents: config.claudeCode?.injectAgents !== false,
|
|
1426
|
+
sidecarPools: {
|
|
1427
|
+
webSearchSidecar: sidecarAnthropicPoolOptions(config, { ...config.webSearchSidecar, ...webSearchOverride }, helperParent),
|
|
1428
|
+
visionSidecar: sidecarAnthropicPoolOptions(config, { ...helperVision, backend: helperVisionBackend }, helperParent),
|
|
1429
|
+
},
|
|
1414
1430
|
...(webSearchOverride && Object.keys(webSearchOverride).length > 0
|
|
1415
|
-
? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model } }
|
|
1431
|
+
? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model, ...(webSearchOverride.anthropicInstance ? { anthropicInstance: webSearchOverride.anthropicInstance } : {}) } }
|
|
1416
1432
|
: {}),
|
|
1417
1433
|
...(visionOverride && Object.keys(visionOverride).length > 0
|
|
1418
|
-
? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
|
|
1434
|
+
? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model, ...(visionOverride.anthropicInstance ? { anthropicInstance: visionOverride.anthropicInstance } : {}) } }
|
|
1419
1435
|
: {}),
|
|
1420
1436
|
fastMode: config.fastMode,
|
|
1421
1437
|
forceAvailable: [...visibleNativeSlugs(config), ...filterCatalogVisibleModels(models, config).map(catalogModelSlug)],
|
|
@@ -1563,6 +1579,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1563
1579
|
...(residual ? ["settings_residual"] : []),
|
|
1564
1580
|
] });
|
|
1565
1581
|
}
|
|
1582
|
+
const instanceError = anthropicSidecarPatchError(config, body, true);
|
|
1583
|
+
if (instanceError) return jsonResponse({ error: instanceError }, 400);
|
|
1566
1584
|
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1567
1585
|
const section = body[field];
|
|
1568
1586
|
if (section === undefined || section === null) continue;
|
|
@@ -1587,7 +1605,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1587
1605
|
// gates cannot drift.
|
|
1588
1606
|
if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
|
|
1589
1607
|
const requested = section.model;
|
|
1590
|
-
const candidates = await visionCandidateRows(config
|
|
1608
|
+
const candidates = await visionCandidateRows({ ...config, visionSidecar: {
|
|
1609
|
+
...config.visionSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.visionSidecar, section),
|
|
1610
|
+
} });
|
|
1591
1611
|
const hint = section.backend === "anthropic" || section.backend === "openai"
|
|
1592
1612
|
|| section.backend === "routed"
|
|
1593
1613
|
? section.backend
|
|
@@ -1610,7 +1630,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1610
1630
|
// module as /api/sidecar-settings — a gate on one route and a stale copy on
|
|
1611
1631
|
// the other is no gate at all.
|
|
1612
1632
|
if (field === "webSearchSidecar"
|
|
1613
|
-
&& (section.model !== undefined || section.backend !== undefined)) {
|
|
1633
|
+
&& (section.model !== undefined || section.backend !== undefined || section.anthropicInstance !== undefined)) {
|
|
1614
1634
|
const stored = config.claudeCode?.webSearchSidecar;
|
|
1615
1635
|
// Validate against the SUBMITTED backend across the whole union, not
|
|
1616
1636
|
// just openai/anthropic (#2457). allowedBackends above already refused
|
|
@@ -1629,7 +1649,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1629
1649
|
: typeof section.model === "string"
|
|
1630
1650
|
? section.model
|
|
1631
1651
|
: stored?.model ?? config.webSearchSidecar?.model;
|
|
1632
|
-
const candidates = await webSearchCandidateRows(config
|
|
1652
|
+
const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: {
|
|
1653
|
+
...config.webSearchSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.webSearchSidecar, section),
|
|
1654
|
+
} });
|
|
1633
1655
|
if (effectiveModel && webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
|
|
1634
1656
|
return jsonResponse(webSearchModelRejection(
|
|
1635
1657
|
"webSearchSidecar.model",
|
|
@@ -1650,8 +1672,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1650
1672
|
}
|
|
1651
1673
|
// The per-field validation above guarantees vision only ever carries the two-member
|
|
1652
1674
|
// union; the cast is the loop's shared-shape compromise, not a wider write path.
|
|
1653
|
-
const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string };
|
|
1675
|
+
const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string; anthropicInstance?: AnthropicInstanceId | null };
|
|
1654
1676
|
const override = { ...next[field] } as NonNullable<OcxClaudeCodeConfig[typeof field]>;
|
|
1677
|
+
if (requested.anthropicInstance === null) delete override.anthropicInstance;
|
|
1678
|
+
else if (isAnthropicInstanceId(requested.anthropicInstance)) override.anthropicInstance = requested.anthropicInstance;
|
|
1655
1679
|
if (requested.backend === null) delete override.backend;
|
|
1656
1680
|
else if (requested.backend !== undefined) override.backend = requested.backend as never;
|
|
1657
1681
|
if (requested.model === "") delete override.model;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isAnthropicOAuthInstance, configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
1
2
|
import type { OcxConfig } from "../../types";
|
|
2
|
-
import {
|
|
3
|
+
import { effectiveAnthropicAccountThresholdForInstance, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
|
|
3
4
|
import { OAUTH_PROVIDERS } from "../../oauth";
|
|
4
|
-
import {
|
|
5
|
+
import { setAnthropicAccountThresholdForInstance } from "../../oauth/store";
|
|
5
6
|
import { jsonResponse } from "../auth-cors";
|
|
6
7
|
import { readManagementJsonBodyOr } from "./body";
|
|
7
8
|
|
|
@@ -10,18 +11,20 @@ export async function handleAnthropicAccountThreshold(req: Request, config: OcxC
|
|
|
10
11
|
const body = await readManagementJsonBodyOr(req, {});
|
|
11
12
|
if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
12
13
|
const fields = body as Record<string, unknown>;
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const provider = fields.provider;
|
|
15
|
+
if (typeof provider !== "string" || !isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
|
|
16
|
+
const definition = OAUTH_PROVIDERS[provider];
|
|
17
|
+
const effectiveProvider = config.providers[provider]
|
|
15
18
|
?? definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
|
|
16
|
-
if (
|
|
19
|
+
if (effectiveProvider?.authMode !== "oauth" || (provider === "anthropic2" && !configuredAnthropicInstance(config, provider))) {
|
|
17
20
|
return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
|
|
18
21
|
}
|
|
19
22
|
if (typeof fields.accountId !== "string" || !fields.accountId.trim()) return jsonResponse({ error: "missing accountId" }, 400);
|
|
20
23
|
const threshold = parseAnthropicAccountThreshold(fields.threshold);
|
|
21
24
|
if (fields.threshold !== null && threshold === null) return jsonResponse({ error: "threshold must be an integer 0-100 or null" }, 400);
|
|
22
|
-
if (!await
|
|
23
|
-
return jsonResponse({ ok: true, provider
|
|
25
|
+
if (!await setAnthropicAccountThresholdForInstance(provider, fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
|
|
26
|
+
return jsonResponse({ ok: true, provider, accountId: fields.accountId,
|
|
24
27
|
autoSwitchThresholdOverride: threshold,
|
|
25
|
-
effectiveAutoSwitchThreshold:
|
|
26
|
-
autoSwitchThreshold:
|
|
28
|
+
effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config, { autoSwitchThresholdOverride: threshold ?? undefined }),
|
|
29
|
+
autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config) });
|
|
27
30
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import type { AnthropicAccountPoolConfig } from "../../types/anthropic-account-pool";
|
|
3
|
+
import { configuredAnthropicInstance, type AnthropicInstanceId } from "../../providers/anthropic-instance";
|
|
4
|
+
import { rawAnthropicAccountPool, resolveAnthropicAccountPoolConfig } from "../../oauth/anthropic-pool-config";
|
|
5
|
+
import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
|
|
6
|
+
import { ConfigWritePublishedError } from "../../config/persist-unlocked";
|
|
7
|
+
import { mutatePersistedConfig } from "../../config";
|
|
8
|
+
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
9
|
+
import { jsonResponse } from "../auth-cors";
|
|
10
|
+
|
|
11
|
+
/** Required instance; never synthesizes a deleted B provider row. */
|
|
12
|
+
export function writeAnthropicPoolSettings(config: OcxConfig, instance: AnthropicInstanceId, value: AnthropicAccountPoolConfig): void {
|
|
13
|
+
if (instance === "anthropic") config.anthropicAccountPool = value;
|
|
14
|
+
else {
|
|
15
|
+
const row = config.providers[instance];
|
|
16
|
+
if (!configuredAnthropicInstance(config, instance)) throw new Error("Anthropic pool instance is no longer configured");
|
|
17
|
+
row!.anthropicAccountPool = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Both Anthropic writers share publication-aware recovery from the atomic mutation owner.
|
|
23
|
+
* Recovery covers only the durable write. Once the file holds the new value, a stale live row
|
|
24
|
+
* or a failed reconcile is reported as saved with the fixed bookkeeping warning, never a 500.
|
|
25
|
+
*/
|
|
26
|
+
export function persistAnthropicPoolPatch(
|
|
27
|
+
config: OcxConfig,
|
|
28
|
+
instance: AnthropicInstanceId,
|
|
29
|
+
patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
|
|
30
|
+
): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
|
|
31
|
+
const before = readConfigFileSnapshot();
|
|
32
|
+
const unknown = () => ({ status: "failed" as const, response: jsonResponse({
|
|
33
|
+
error: "Pool settings save state is unknown; reload settings before editing again",
|
|
34
|
+
code: "config_save_state_unknown",
|
|
35
|
+
}, 409) });
|
|
36
|
+
const publish = (value: AnthropicAccountPoolConfig, warned: boolean): { status: "saved"; warning?: "config_bookkeeping_failed" } => {
|
|
37
|
+
let warning = warned;
|
|
38
|
+
try { writeAnthropicPoolSettings(config, instance, value); } catch { warning = true; }
|
|
39
|
+
try { reconcileLiveStateStores(); } catch { warning = true; }
|
|
40
|
+
return warning ? { status: "saved", warning: "config_bookkeeping_failed" } : { status: "saved" };
|
|
41
|
+
};
|
|
42
|
+
let durable: AnthropicAccountPoolConfig | undefined;
|
|
43
|
+
try {
|
|
44
|
+
const saved = mutatePersistedConfig(target => {
|
|
45
|
+
if (instance === "anthropic2" && !configuredAnthropicInstance(target, instance)) {
|
|
46
|
+
throw new Error("Anthropic pool instance is no longer configured");
|
|
47
|
+
}
|
|
48
|
+
return patch(target);
|
|
49
|
+
});
|
|
50
|
+
if (saved.status === "unavailable") return unknown();
|
|
51
|
+
durable = saved.value;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const persisted = readConfigFileSnapshot();
|
|
54
|
+
if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
|
|
55
|
+
// File reads may salvage hand edits; recovery success requires a strict, authoritative document.
|
|
56
|
+
let validated: ReturnType<typeof validateConfigCandidate>;
|
|
57
|
+
try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
|
|
58
|
+
catch { return unknown(); }
|
|
59
|
+
if (!validated.ok) return unknown();
|
|
60
|
+
const current = validated.config;
|
|
61
|
+
if (instance === "anthropic2" && !configuredAnthropicInstance(current, instance)) return unknown();
|
|
62
|
+
let expected: AnthropicAccountPoolConfig;
|
|
63
|
+
try { expected = patch(structuredClone(current)).value; } catch { return unknown(); }
|
|
64
|
+
const matches = JSON.stringify(rawAnthropicAccountPool(current, instance)) === JSON.stringify(expected);
|
|
65
|
+
// A failed live write or reconcile must not turn confirmed publication back into a rollback.
|
|
66
|
+
if (matches) return publish(resolveAnthropicAccountPoolConfig(current, instance), true);
|
|
67
|
+
if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
|
|
68
|
+
return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
|
|
69
|
+
}
|
|
70
|
+
if (durable === undefined) return unknown();
|
|
71
|
+
return publish(durable, false);
|
|
72
|
+
}
|