@bitkyc08/opencodex 2.77.0 → 2.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A Messages request whose settled route is a proxy-managed key on the `anthropic` adapter is
|
|
5
5
|
* sent as Messages: the source body (after the ingress's managed-client steps) cut to a field
|
|
6
6
|
* allowlist, the wire model, and the provider's own key. With `managedMessagesNativeOAuth` on
|
|
7
|
-
* (PF-10), an
|
|
7
|
+
* (PF-10), an Anthropic OAuth account is sent the same way with the access token the
|
|
8
8
|
* existing OAuth selection resolves at dispatch (`messages-native-oauth.ts`). Modelled on the native Chat lane and
|
|
9
9
|
* built on the same shared pieces — attempt row, finish-once final log, spend tracker, proactive
|
|
10
10
|
* key selection, key failover and 429 replay, connection policy — so nothing the Responses
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
|
|
19
19
|
*/
|
|
20
|
+
import { resolveAnthropicModelRoute } from "../oauth/anthropic-model-routes";
|
|
21
|
+
import { bindAnthropicRefusalCredential, rotateAnthropicAccountOnResponse } from "../oauth/anthropic-account-refusal";
|
|
22
|
+
import { claimAnthropicFamilyRevalidation } from "../oauth/anthropic-model-quota";
|
|
23
|
+
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
24
|
+
import { recordAnthropicAccountQuotaFromHeaders } from "../providers/quota";
|
|
25
|
+
import { credentialGeneration, getAccountCredentialWithStatus } from "../oauth/store";
|
|
26
|
+
import { ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST } from "../oauth/anthropic-routing";
|
|
20
27
|
import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
|
|
21
28
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
22
29
|
import { formatAnthropicErrorBody } from "../adapters/anthropic";
|
|
@@ -84,6 +91,7 @@ import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./mess
|
|
|
84
91
|
import {
|
|
85
92
|
nativeOAuthBindingIsCurrent,
|
|
86
93
|
NativeOAuthSelectionChangedError,
|
|
94
|
+
NativeOAuthModelRouteError,
|
|
87
95
|
resolveNativeOAuthBinding,
|
|
88
96
|
restoreOAuthToolNamesInMessage,
|
|
89
97
|
restoreOAuthToolNamesInSse,
|
|
@@ -150,6 +158,7 @@ export interface HandleNativeMessagesOptions {
|
|
|
150
158
|
*/
|
|
151
159
|
callerAnthropicBeta?: string | null;
|
|
152
160
|
clientIdentity?: AnthropicClientIdentity;
|
|
161
|
+
sessionKey?: string | null;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
|
|
@@ -373,7 +382,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
373
382
|
const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
|
|
374
383
|
if (route.provider.authMode === "oauth") {
|
|
375
384
|
try {
|
|
376
|
-
oauthBinding = await resolveNativeOAuthBinding(config);
|
|
385
|
+
oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
|
|
377
386
|
} catch (error) {
|
|
378
387
|
cleanupAbort();
|
|
379
388
|
upstream.abort();
|
|
@@ -381,6 +390,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
381
390
|
const localRefusal = localOAuthFailure(error);
|
|
382
391
|
if (localRefusal) return localRefusal;
|
|
383
392
|
if (error instanceof NativeOAuthSelectionChangedError) return fail(409, error.message, "api_error");
|
|
393
|
+
if (error instanceof NativeOAuthModelRouteError) return fail(400, error.message, "invalid_request_error");
|
|
384
394
|
return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
|
|
385
395
|
}
|
|
386
396
|
} else {
|
|
@@ -455,7 +465,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
455
465
|
const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
|
|
456
466
|
const selector: NativeMessagesSelector = options.selector ?? {};
|
|
457
467
|
|
|
458
|
-
const send = async (recovery?: "rate-limit-429" | "key-429" | "key-401"): Promise<Response> => {
|
|
468
|
+
const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
459
469
|
const remaining = remainingTransientSends();
|
|
460
470
|
if (requestTransientPolicy && remaining <= 0) {
|
|
461
471
|
throw new Error("native Messages transient send budget exhausted before recovery dispatch");
|
|
@@ -479,10 +489,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
479
489
|
for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
|
|
480
490
|
if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
|
|
481
491
|
try {
|
|
482
|
-
oauthBinding = await resolveNativeOAuthBinding(config);
|
|
492
|
+
oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
|
|
483
493
|
} catch (error) {
|
|
484
494
|
if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
|
|
485
|
-
|| error instanceof AnthropicAccountCooldownError) throw error;
|
|
495
|
+
|| error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
|
|
486
496
|
throw new NativeOAuthSelectionChangedError();
|
|
487
497
|
}
|
|
488
498
|
rebuildFor(oauthProvider(oauthBinding));
|
|
@@ -500,15 +510,34 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
500
510
|
const headers = new Headers(wire.headers);
|
|
501
511
|
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
502
512
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
513
|
+
const snapshot = oauthBinding?.snapshot;
|
|
514
|
+
const providerAccountUuid = oauthBinding?.providerAccountUuid;
|
|
515
|
+
const writerGeneration = snapshot ? captureConfigGeneration() : 0;
|
|
516
|
+
const ownsBearer = snapshot && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
|
|
517
|
+
const releaseFamily = snapshot ? claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
518
|
+
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
519
|
+
let dispatched: Response;
|
|
520
|
+
try {
|
|
521
|
+
if (init.signal?.aborted) throw init.signal.reason;
|
|
522
|
+
if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
|
|
523
|
+
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
524
|
+
dispatched = await sendWithConnectionPolicy(
|
|
525
|
+
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
526
|
+
wire.url,
|
|
527
|
+
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
|
|
528
|
+
{ providerName: route.providerName, provider: activeProvider },
|
|
529
|
+
);
|
|
530
|
+
} finally { releaseFamily(); }
|
|
531
|
+
if (ownsBearer && snapshot) {
|
|
532
|
+
try {
|
|
533
|
+
const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
|
|
534
|
+
if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation
|
|
535
|
+
&& current.credential.accountId === providerAccountUuid) {
|
|
536
|
+
bindAnthropicRefusalCredential(dispatched, snapshot, providerAccountUuid);
|
|
537
|
+
recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
|
|
538
|
+
}
|
|
539
|
+
} catch { /* Passive observation must not fail the response. */ }
|
|
540
|
+
}
|
|
512
541
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
513
542
|
return dispatched;
|
|
514
543
|
},
|
|
@@ -544,7 +573,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
544
573
|
rebuildFor(rotated);
|
|
545
574
|
response = await send("key-401");
|
|
546
575
|
}
|
|
547
|
-
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
576
|
+
const retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
|
|
548
577
|
let retries = 0;
|
|
549
578
|
// A refusal this proxy synthesized for a reset replay is not a provider rate limit.
|
|
550
579
|
while (
|
|
@@ -577,6 +606,37 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
577
606
|
rebuildFor(rotated);
|
|
578
607
|
response = await send("key-429");
|
|
579
608
|
}
|
|
609
|
+
const oauthRetryKey = {};
|
|
610
|
+
const triedAccountIds = new Set<string>();
|
|
611
|
+
let oauthFailovers = 0;
|
|
612
|
+
while (oauthBinding && (response.status === 429 || response.status === 403)) {
|
|
613
|
+
const sendingBinding = oauthBinding;
|
|
614
|
+
const expectedRecoverySelection = sendingBinding.selection;
|
|
615
|
+
triedAccountIds.add(sendingBinding.snapshot.accountId);
|
|
616
|
+
const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
|
|
617
|
+
config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
|
|
618
|
+
sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
|
|
619
|
+
excludedAccountIds: triedAccountIds,
|
|
620
|
+
currentDecision: () => {
|
|
621
|
+
const resolved = resolveAnthropicModelRoute(config, route.modelId);
|
|
622
|
+
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
623
|
+
return resolved.decision;
|
|
624
|
+
},
|
|
625
|
+
signal: upstream.signal, requestKey: oauthRetryKey,
|
|
626
|
+
canRetry: transientSendAvailable() && oauthFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
627
|
+
});
|
|
628
|
+
if (!nextAccountId) break;
|
|
629
|
+
const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
|
|
630
|
+
discard(response);
|
|
631
|
+
oauthBinding = await resolveNativeOAuthBinding(config, {
|
|
632
|
+
sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
|
|
633
|
+
expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
|
|
634
|
+
});
|
|
635
|
+
oauthFailovers++;
|
|
636
|
+
rebuildFor(oauthProvider(oauthBinding));
|
|
637
|
+
logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
638
|
+
response = await send(recovery);
|
|
639
|
+
}
|
|
580
640
|
} catch (error) {
|
|
581
641
|
releaseRetainedRequest();
|
|
582
642
|
cleanupAbort();
|
|
@@ -591,6 +651,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
591
651
|
const localRefusal = localOAuthFailure(sendError);
|
|
592
652
|
if (localRefusal) return localRefusal;
|
|
593
653
|
if (sendError instanceof NativeOAuthSelectionChangedError) return fail(409, sendError.message, "api_error");
|
|
654
|
+
if (sendError instanceof NativeOAuthModelRouteError) return fail(400, sendError.message, "invalid_request_error");
|
|
594
655
|
if (sendError instanceof NativeOpaqueStateRefusal) {
|
|
595
656
|
logCtx.errorCode = "unsupported_feature";
|
|
596
657
|
return fail(400, sendError.message, "invalid_request_error", "unsupported_feature");
|
|
@@ -791,6 +852,7 @@ export function nativeMessagesCountBody(
|
|
|
791
852
|
cc: OcxConfig["claudeCode"],
|
|
792
853
|
body: Rec,
|
|
793
854
|
rows: Pick<NativeMessagesSelector, "effortRow" | "fastRow">,
|
|
855
|
+
clientIdentity?: AnthropicClientIdentity,
|
|
794
856
|
): Rec | undefined {
|
|
795
857
|
if (typeof body.model !== "string") return undefined;
|
|
796
858
|
try {
|
|
@@ -812,7 +874,7 @@ export function nativeMessagesCountBody(
|
|
|
812
874
|
};
|
|
813
875
|
if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
|
|
814
876
|
// The body without a credential: counting never resolves or refreshes an OAuth account.
|
|
815
|
-
return anthropicMessagesNativeWireBody(route.provider, route.modelId, body).wireBody;
|
|
877
|
+
return anthropicMessagesNativeWireBody(route.provider, route.modelId, body, { clientIdentity }).wireBody;
|
|
816
878
|
} catch {
|
|
817
879
|
return undefined;
|
|
818
880
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
2
2
|
import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
|
|
3
|
+
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
4
|
+
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
5
|
+
import { transientSendCapFor } from "./request-send-budget";
|
|
3
6
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
4
7
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
5
8
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -25,6 +28,7 @@ import { withProviderRequestSlot } from "../../providers/request-pacing";
|
|
|
25
28
|
import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
|
|
26
29
|
import {
|
|
27
30
|
transientRetryPolicyFor,
|
|
31
|
+
resetReplayPolicyFor,
|
|
28
32
|
rateLimitRetryPolicyFor,
|
|
29
33
|
hasKeyPoolFailover,
|
|
30
34
|
rotateProviderTransportOn401,
|
|
@@ -156,6 +160,8 @@ export async function prepareAdapterExchange(
|
|
|
156
160
|
| "reserveCredentialHop"
|
|
157
161
|
| "pendingHopPermit"
|
|
158
162
|
| "workflowRootId"
|
|
163
|
+
| "claimAmbiguousResend"
|
|
164
|
+
| "sendsUsed"
|
|
159
165
|
>,
|
|
160
166
|
) {
|
|
161
167
|
const { options, config, logCtx, req } = requestContext;
|
|
@@ -189,9 +195,18 @@ export async function prepareAdapterExchange(
|
|
|
189
195
|
recoveryClassFor,
|
|
190
196
|
sendBudgetExhausted,
|
|
191
197
|
reserveCredentialHop,
|
|
198
|
+
claimAmbiguousResend,
|
|
192
199
|
} = sendBudgetState;
|
|
193
200
|
|
|
194
201
|
|
|
202
|
+
let selfContainedJudgment: boolean | undefined;
|
|
203
|
+
const requestIsSelfContained = (): boolean =>
|
|
204
|
+
selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
|
|
205
|
+
const claimPreHeaderResend = (): boolean => authorizeResendForRecovery(
|
|
206
|
+
"pre-header", "connection-reset",
|
|
207
|
+
ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend),
|
|
208
|
+
).allowed;
|
|
209
|
+
|
|
195
210
|
const upstream = new AbortController();
|
|
196
211
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
197
212
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
@@ -343,6 +358,13 @@ export async function prepareAdapterExchange(
|
|
|
343
358
|
const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
|
|
344
359
|
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
345
360
|
let compactPrepaidUsed = false;
|
|
361
|
+
// Combo and emergency compaction admission already book this target's first send.
|
|
362
|
+
// Its configured initial ceiling is target-local; the shared remainder below still
|
|
363
|
+
// accounts for earlier targets without deducting their sends from this target twice.
|
|
364
|
+
const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
|
|
365
|
+
? transientSendCapFor(transientPolicy?.attempts,
|
|
366
|
+
(options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
|
|
367
|
+
: 1;
|
|
346
368
|
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
347
369
|
? fetchWithTransientRetry
|
|
348
370
|
: fetchWithResetRetry;
|
|
@@ -367,15 +389,12 @@ export async function prepareAdapterExchange(
|
|
|
367
389
|
{
|
|
368
390
|
abortSignal: upstream.signal,
|
|
369
391
|
label: safeHostLabel(builtInitialRequest.url),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// holder but used to take a fresh full allowance on its own first send, so the
|
|
373
|
-
// shared counter was inherited without ever being read as a limit.
|
|
392
|
+
claimAmbiguousResend: claimPreHeaderResend,
|
|
393
|
+
...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
|
|
374
394
|
? {
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
remainingTransientSendBudget(transientPolicy?.attempts ?? 1) + (compactPrepaid ? 1 : 0)),
|
|
395
|
+
// A pending compaction permit already booked this leg's first physical send.
|
|
396
|
+
attempts: Math.min(initialSendCap,
|
|
397
|
+
remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
|
|
379
398
|
onSendsConsumed: noteTransientSends,
|
|
380
399
|
}
|
|
381
400
|
: {}),
|
|
@@ -545,14 +564,21 @@ export async function prepareAdapterExchange(
|
|
|
545
564
|
const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
|
|
546
565
|
? fetchWithTransientRetry
|
|
547
566
|
: fetchWithResetRetry;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
?
|
|
553
|
-
|
|
554
|
-
|
|
567
|
+
const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
|
|
568
|
+
const prepaid = sendBudgetState.pendingHopPermit;
|
|
569
|
+
const configuredTotal = refetchTransientPolicy?.attempts;
|
|
570
|
+
const refetchCap = transientSendCapFor(configuredTotal,
|
|
571
|
+
sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
|
|
572
|
+
// An exact total includes a booked hop, even when it consumed the last base slot.
|
|
573
|
+
// Keep that funded send while forbidding the final reserve from widening the total.
|
|
574
|
+
const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
|
|
575
|
+
&& refetchCap > 0 && remainingTransientSendBudget(refetchCap) === 0;
|
|
576
|
+
if (prepaidLastSlot) sendBudgetState.pendingHopPermit = undefined;
|
|
577
|
+
const refetchAllowance = helperCountsSends
|
|
578
|
+
? prepaidLastSlot ? { attempts: 1, permit: prepaid } : recoverySendAllowance(
|
|
579
|
+
refetchCap, recoveryClassFor(recovery),
|
|
555
580
|
`${route.providerName}|${route.modelId}|${recovery}`,
|
|
581
|
+
{ allowFinalRecoveryReserve: configuredTotal === undefined },
|
|
556
582
|
)
|
|
557
583
|
: undefined;
|
|
558
584
|
try {
|
|
@@ -566,7 +592,7 @@ export async function prepareAdapterExchange(
|
|
|
566
592
|
// can be refused above before it does. use() past the first attempt is a no-op.
|
|
567
593
|
onDispatch?.();
|
|
568
594
|
if (preserveFailureResponse) replacementAdmitted = true;
|
|
569
|
-
if (!
|
|
595
|
+
if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
|
|
570
596
|
return fetchWithHeaderTimeout(retryRequest.url,
|
|
571
597
|
applyUpstreamRecoveryInit({
|
|
572
598
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
@@ -580,6 +606,7 @@ export async function prepareAdapterExchange(
|
|
|
580
606
|
{
|
|
581
607
|
abortSignal: upstream.signal,
|
|
582
608
|
label: safeHostLabel(retryRequest.url),
|
|
609
|
+
claimAmbiguousResend: claimPreHeaderResend,
|
|
583
610
|
...(refetchAllowance
|
|
584
611
|
? {
|
|
585
612
|
attempts: refetchAllowance.attempts,
|
|
@@ -638,7 +665,7 @@ export async function prepareAdapterExchange(
|
|
|
638
665
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
639
666
|
const hop = reserveCredentialHop("auth-recovery",
|
|
640
667
|
`${route.providerName}|${route.modelId}|antigravity-auth`,
|
|
641
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
668
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
|
|
642
669
|
if (!hop.allowed) return null;
|
|
643
670
|
try {
|
|
644
671
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
|
|
@@ -708,7 +735,7 @@ export async function prepareAdapterExchange(
|
|
|
708
735
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
709
736
|
const hop = reserveCredentialHop("auth-recovery",
|
|
710
737
|
`${route.providerName}|${route.modelId}|terminal-refresh-account`,
|
|
711
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
738
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
|
|
712
739
|
if (hop.allowed) {
|
|
713
740
|
try {
|
|
714
741
|
const admitted = await applyFailoverSnapshot(alternate);
|
|
@@ -861,7 +888,7 @@ export async function prepareAdapterExchange(
|
|
|
861
888
|
const hop = reserveCredentialHop(
|
|
862
889
|
"repair",
|
|
863
890
|
`${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
|
|
864
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
891
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
865
892
|
);
|
|
866
893
|
if (hop.allowed) {
|
|
867
894
|
anthropicFastDowngradeGuard.attempted = true;
|
|
@@ -1059,10 +1086,9 @@ export async function prepareAdapterExchange(
|
|
|
1059
1086
|
const hop = reserveCredentialHop(
|
|
1060
1087
|
"auth-recovery",
|
|
1061
1088
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
1089
|
+
// Transient and opted-in reset helpers report physical sends; an adapter-owned
|
|
1090
|
+
// ladder settles its own booking.
|
|
1091
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1066
1092
|
);
|
|
1067
1093
|
if (!hop.allowed) break;
|
|
1068
1094
|
const nextAccountId = rotateGenericOAuthAccountOnRefusal(
|
|
@@ -1164,10 +1190,9 @@ export async function prepareAdapterExchange(
|
|
|
1164
1190
|
const hop = reserveCredentialHop(
|
|
1165
1191
|
"auth-recovery",
|
|
1166
1192
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
1193
|
+
// Transient and opted-in reset helpers report physical sends; an adapter-owned
|
|
1194
|
+
// ladder settles its own booking.
|
|
1195
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1171
1196
|
);
|
|
1172
1197
|
if (!hop.allowed) break;
|
|
1173
1198
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
@@ -1276,7 +1301,7 @@ export async function prepareAdapterExchange(
|
|
|
1276
1301
|
const hop = reserveCredentialHop(
|
|
1277
1302
|
"auth-recovery",
|
|
1278
1303
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-verify`,
|
|
1279
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
1304
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1280
1305
|
);
|
|
1281
1306
|
if (!hop.allowed) break;
|
|
1282
1307
|
// Auth-refusal rotation, not the rate-limit one: a verification refusal
|
|
@@ -41,13 +41,19 @@ import { markLocalRequestLogRefusal } from "../request-log";
|
|
|
41
41
|
import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
|
|
42
42
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
43
43
|
import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
44
|
+
import { safeCallerSessionId } from "../caller-session-identity";
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
+
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
46
47
|
export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Empty values count as absent, matching materializeCodexUpstreamAuth, so first attempts and
|
|
49
|
+
// retries rebuilt from caller headers pick the same identity.
|
|
50
|
+
if (!isCanonicalOpenAiForwardProvider(provider) || headers.get("session_id")) return headers;
|
|
51
|
+
const alias = ["session-id", "thread-id"].find(name => headers.get(name));
|
|
52
|
+
const nativeSession = alias ? safeCallerSessionId(headers.get(alias)) : sessionId;
|
|
53
|
+
// A non-empty invalid alias suppresses weaker metadata identity.
|
|
54
|
+
if (!nativeSession) return headers;
|
|
49
55
|
const forwarded = new Headers(headers);
|
|
50
|
-
forwarded.set("session_id",
|
|
56
|
+
forwarded.set("session_id", nativeSession);
|
|
51
57
|
return forwarded;
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
concreteComboRequestBody,
|
|
29
29
|
comboDefaultEffort,
|
|
30
30
|
isComboTargetInCooldown,
|
|
31
|
+
snapshotComboQuotaCooldowns,
|
|
31
32
|
noteComboSuccess,
|
|
32
33
|
comboFailureDecision,
|
|
33
34
|
advanceComboAfterFailure,
|
|
@@ -72,6 +73,7 @@ import { isPlainObject } from "../../lib/plain-data";
|
|
|
72
73
|
import {
|
|
73
74
|
clientCancelledResponse,
|
|
74
75
|
comboUnavailable,
|
|
76
|
+
comboUnavailableResponse,
|
|
75
77
|
targetIncompatibleResponse,
|
|
76
78
|
unreadableEncryptedAgentTaskResponse,
|
|
77
79
|
} from "./core-errors";
|
|
@@ -412,8 +414,8 @@ export async function executeComboResponses(
|
|
|
412
414
|
}
|
|
413
415
|
};
|
|
414
416
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
415
|
-
// Attempts remain the complete physical history; the logical row mirrors the
|
|
416
|
-
//
|
|
417
|
+
// Attempts remain the complete physical history; the logical row mirrors the failed
|
|
418
|
+
// target whose response is returned, including an earlier quota refusal on exhaustion.
|
|
417
419
|
Object.assign(logCtx, childLog, {
|
|
418
420
|
requestedModel,
|
|
419
421
|
model: requestedModel,
|
|
@@ -676,6 +678,41 @@ export async function executeComboResponses(
|
|
|
676
678
|
// is gone, so carry the loop's own classification decision instead of re-deriving a
|
|
677
679
|
// weaker one from the status alone (#4149).
|
|
678
680
|
let lastFailureClassifiesOverflow = false;
|
|
681
|
+
// The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
|
|
682
|
+
// credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
|
|
683
|
+
// request failed: the primary target ran out. Returning that fallback refusal told clients
|
|
684
|
+
// "OpenAI account pool has no usable account credential" while every Claude account was spent.
|
|
685
|
+
let quotaFailure: Response | undefined;
|
|
686
|
+
let quotaFailedChildLog: RequestLogContext | undefined;
|
|
687
|
+
// Snapshotted before dispatch: a 401/403 inside this ladder cools its own provider, and that
|
|
688
|
+
// fresh cooldown is not a reason the request found no target.
|
|
689
|
+
const quotaCooldownSnapshot = snapshotComboQuotaCooldowns(config, comboId);
|
|
690
|
+
const exhaustedFailure = (): Response => {
|
|
691
|
+
const returnsQuota = lastFailure && [400, 401, 403].includes(lastFailure.status) && quotaFailure;
|
|
692
|
+
const failedChildLog = returnsQuota ? quotaFailedChildLog : lastFailedChildLog;
|
|
693
|
+
if (failedChildLog) adoptFailedChildLog(failedChildLog);
|
|
694
|
+
if (!lastFailure || ![400, 401, 403].includes(lastFailure.status)) return lastFailure!;
|
|
695
|
+
if (quotaFailure) return quotaFailure;
|
|
696
|
+
// The quota-refused target was cooled before this request picked, so the ladder never saw
|
|
697
|
+
// its refusal. The cooldown, with its Retry-After, is still the honest answer.
|
|
698
|
+
let quotaCooldownUntil: number | undefined;
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
for (const { target, cooldownUntil } of quotaCooldownSnapshot) {
|
|
701
|
+
if (cooldownUntil <= now) continue;
|
|
702
|
+
try {
|
|
703
|
+
if (!targetEligible(target)) continue;
|
|
704
|
+
} catch {
|
|
705
|
+
// An eligibility check can exhaust the translator budget; it is not quota evidence.
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
quotaCooldownUntil = Math.min(quotaCooldownUntil ?? cooldownUntil, cooldownUntil);
|
|
709
|
+
}
|
|
710
|
+
const remainingQuotaCooldownMs = quotaCooldownUntil === undefined ? 0 : quotaCooldownUntil - Date.now();
|
|
711
|
+
return remainingQuotaCooldownMs > 0
|
|
712
|
+
? comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
|
|
713
|
+
retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
|
|
714
|
+
}) : lastFailure;
|
|
715
|
+
};
|
|
679
716
|
while (pick) {
|
|
680
717
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
681
718
|
const firstComboTarget = comboTargetsDispatched === 0;
|
|
@@ -698,8 +735,7 @@ export async function executeComboResponses(
|
|
|
698
735
|
// return the last real upstream answer with its status, headers and any quota body
|
|
699
736
|
// intact rather than to mint a synthetic error, and a later target only exists because
|
|
700
737
|
// an earlier one already recorded one.
|
|
701
|
-
|
|
702
|
-
return lastFailure!;
|
|
738
|
+
return exhaustedFailure();
|
|
703
739
|
}
|
|
704
740
|
const targetSendBudget = comboSendScope
|
|
705
741
|
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
@@ -979,6 +1015,10 @@ export async function executeComboResponses(
|
|
|
979
1015
|
);
|
|
980
1016
|
attemptRetained = true;
|
|
981
1017
|
lastFailure = failure.response;
|
|
1018
|
+
if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
|
|
1019
|
+
quotaFailure = lastFailure;
|
|
1020
|
+
quotaFailedChildLog = childLog;
|
|
1021
|
+
}
|
|
982
1022
|
lastFailedChildLog = childLog;
|
|
983
1023
|
// A replacement that answers 200 is unmarked, and its zero-output failure only exists once
|
|
984
1024
|
// preflight has rebuilt the stream as a fresh Response. A spent grant never hops: a status the
|
|
@@ -1141,9 +1181,9 @@ export async function executeComboResponses(
|
|
|
1141
1181
|
}
|
|
1142
1182
|
// Waiting or recovery may have observed cancellation after the check above.
|
|
1143
1183
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1144
|
-
adoptFailedChildLog(childLog);
|
|
1145
1184
|
}
|
|
1146
1185
|
}
|
|
1186
|
+
const failure = exhaustedFailure();
|
|
1147
1187
|
if (
|
|
1148
1188
|
lastFailure?.status === 413
|
|
1149
1189
|
&& lastFailureClassifiesOverflow
|
|
@@ -1152,5 +1192,5 @@ export async function executeComboResponses(
|
|
|
1152
1192
|
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
1153
1193
|
: jsonContextOverflowResponse();
|
|
1154
1194
|
}
|
|
1155
|
-
return
|
|
1195
|
+
return failure;
|
|
1156
1196
|
}
|
package/src/service/cli.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { resolveServiceListenPort, reportServiceServing } from "./health";
|
|
|
16
16
|
import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
|
|
17
17
|
import { repairService } from "./repair";
|
|
18
18
|
import type { ServiceRepairVerb } from "./repair";
|
|
19
|
-
import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership } from "./state";
|
|
19
|
+
import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership, serviceStatePaths } from "./state";
|
|
20
|
+
import { ownershipMutationLeaseStatusLine } from "./ownership-mutation-lease.mjs";
|
|
20
21
|
import { foreignServiceOwnerRefusal, unknownServiceOwnerRefusal } from "./repair";
|
|
21
22
|
import type { ServiceBackend } from "./state";
|
|
22
23
|
import { unitPath } from "./systemd";
|
|
@@ -419,6 +420,10 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
419
420
|
// subsumes the not-installed case and adds the serving / stale-plist split.
|
|
420
421
|
console.log(await serviceStatusReport());
|
|
421
422
|
}
|
|
423
|
+
// A busy lease is why a supervised start can keep failing while the line above only
|
|
424
|
+
// says "not running"; report its recorded PID with unverified identity and lease age here.
|
|
425
|
+
const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
|
|
426
|
+
if (lease) console.log(lease);
|
|
422
427
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
423
428
|
break;
|
|
424
429
|
}
|
|
@@ -5,6 +5,22 @@ export interface OwnershipMutationLeaseOptions {
|
|
|
5
5
|
readonly processAlive?: (pid: number) => boolean;
|
|
6
6
|
readonly beforeRelease?: (lockPath: string) => void;
|
|
7
7
|
readonly joinToken?: string;
|
|
8
|
+
/** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
|
|
9
|
+
readonly processImage?: (pid: number) => string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The recorded lease owner, identity unverified, read for diagnostics; never used to reclaim it. */
|
|
13
|
+
export interface OwnershipMutationLeaseHolder {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
/** The recorded PID; null when the directory holds no parseable owner. */
|
|
16
|
+
readonly pid: number | null;
|
|
17
|
+
/** Liveness of the current process at the recorded PID; identity is not verified. */
|
|
18
|
+
readonly alive: boolean | null;
|
|
19
|
+
/** The current executable at the recorded PID when alive and readable; identity is not verified. */
|
|
20
|
+
readonly image: string | null;
|
|
21
|
+
/** Milliseconds since the owner was written, on the clock stale recovery uses. */
|
|
22
|
+
readonly ageMs: number | null;
|
|
23
|
+
readonly record: "complete" | "incomplete" | "empty" | "unreadable";
|
|
8
24
|
}
|
|
9
25
|
|
|
10
26
|
export interface OwnershipMutationLease { readonly token: string; release(): void }
|
|
@@ -30,3 +46,13 @@ export declare function withOwnershipMutationLease<T>(
|
|
|
30
46
|
run: () => T,
|
|
31
47
|
options?: OwnershipMutationLeaseOptions,
|
|
32
48
|
): T;
|
|
49
|
+
|
|
50
|
+
export declare function inspectOwnershipMutationLease(
|
|
51
|
+
statePaths: readonly string[],
|
|
52
|
+
options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
|
|
53
|
+
): OwnershipMutationLeaseHolder | null;
|
|
54
|
+
|
|
55
|
+
export declare function ownershipMutationLeaseStatusLine(
|
|
56
|
+
statePaths: readonly string[],
|
|
57
|
+
options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
|
|
58
|
+
): string | null;
|