@bitkyc08/opencodex 2.82.0 → 2.84.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/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/resolve.ts +1 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +333 -57
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +92 -390
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +129 -50
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
2
|
+
import { anthropicBillingPhysicalFetch } from "../../providers/anthropic-billing-ownership";
|
|
2
3
|
import { rebindPhysicalSend } from "../../lib/request-execution-budget";
|
|
3
4
|
import { anthropicModelQuotaFor } from "../../oauth/anthropic-model-quota";
|
|
4
5
|
import { anthropicRatePolicyFor } from "../../oauth/anthropic-rate-limit-policy";
|
|
@@ -53,6 +54,9 @@ import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
|
|
|
53
54
|
import { expandInferenceOAuthSendBudget } from "../inference/context";
|
|
54
55
|
import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
|
|
55
56
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
57
|
+
import { captureRouteStaticPolicy } from "../../router";
|
|
58
|
+
import { CopilotAutoHttpError, resolveCopilotAuto } from "../../providers/github-copilot-auto";
|
|
59
|
+
import { renameRoutedIdentityInContext } from "../../adapters/identity";
|
|
56
60
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
57
61
|
import {
|
|
58
62
|
providerApiKeySelectionIsCurrent,
|
|
@@ -84,9 +88,16 @@ import {
|
|
|
84
88
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
85
89
|
import { bindAttemptDeliveryRecorder } from "../../usage/attempt-delivery";
|
|
86
90
|
import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
|
|
87
|
-
import { clientCancelledResponse } from "./core-errors";
|
|
91
|
+
import { anthropicPoolCooldownResponse, clientCancelledResponse } from "./core-errors";
|
|
88
92
|
import { acquireAccountLease, KIRO_ACCOUNT_WAIT_MS } from "../../oauth/kiro-account-load";
|
|
89
93
|
|
|
94
|
+
/** Return a renewed Chat wire to the native owner before sending inference. */
|
|
95
|
+
export class CopilotAutoAdapterHandoff extends Error {
|
|
96
|
+
constructor(readonly adapter: ProviderAdapter) {
|
|
97
|
+
super("Copilot Auto dispatch requires the Chat adapter");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
/** Owns live credential selection and adapter bindings for one request. */
|
|
91
102
|
export async function prepareResponsesTransport(
|
|
92
103
|
requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
|
|
@@ -160,6 +171,8 @@ export async function prepareResponsesTransport(
|
|
|
160
171
|
|| route.providerName === "devin"
|
|
161
172
|
) && route.provider.authMode === "oauth";
|
|
162
173
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
174
|
+
// One inference-401 replay belongs to this request, even when Auto changes exchange wire.
|
|
175
|
+
let oauth401ReplayAttempted = false;
|
|
163
176
|
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
164
177
|
let anthropicPoolAccountId: string | null = null;
|
|
165
178
|
let anthropicPoolFailovers = 0;
|
|
@@ -281,6 +294,84 @@ export async function prepareResponsesTransport(
|
|
|
281
294
|
* the case the fence exists for. Stays 0 for every provider without a passive quota.
|
|
282
295
|
*/
|
|
283
296
|
let passiveQuotaWriterGeneration = 0;
|
|
297
|
+
const copilotRequestedModel = route.modelId;
|
|
298
|
+
/** Project only bounded Copilot refusal metadata; upstream bodies and session tokens never reach the client. */
|
|
299
|
+
const copilotRefusalResponse = (error: unknown): Response | undefined => {
|
|
300
|
+
if (!(error instanceof CopilotAutoHttpError) || ![401, 403, 429].includes(error.status)) return;
|
|
301
|
+
const response = formatErrorResponse(error.status, error.status === 429 ? "rate_limit_error" : "authentication_error",
|
|
302
|
+
error.message);
|
|
303
|
+
if (error.retryAfter) response.headers.set("retry-after", error.retryAfter);
|
|
304
|
+
return response;
|
|
305
|
+
};
|
|
306
|
+
let copilotSessionExpiresAt = Infinity;
|
|
307
|
+
let copilotNegotiationRefreshed = false;
|
|
308
|
+
let copilotAutoAdapter: OcxProviderConfig["adapter"] | undefined;
|
|
309
|
+
/** The operator-configured wire, captured before the first Auto negotiation can rewrite route.provider. */
|
|
310
|
+
let copilotConfiguredWire: { adapter: OcxProviderConfig["adapter"]; responsesPath?: string;
|
|
311
|
+
chatCompletionsPath?: string; apiVersion?: string } | undefined;
|
|
312
|
+
/** Start every negotiation from the configured wire, so a previous credential's Auto adapter,
|
|
313
|
+
* endpoint and session headers never carry over to a credential that keeps the named model. */
|
|
314
|
+
const copilotNegotiationBase = (provider: OcxProviderConfig): OcxProviderConfig => {
|
|
315
|
+
if (!copilotConfiguredWire) return provider;
|
|
316
|
+
const headers = new Headers(provider.headers);
|
|
317
|
+
headers.delete("Copilot-Session-Token");
|
|
318
|
+
if (copilotConfiguredWire.apiVersion === undefined) headers.delete("X-GitHub-Api-Version");
|
|
319
|
+
else headers.set("X-GitHub-Api-Version", copilotConfiguredWire.apiVersion);
|
|
320
|
+
const base: OcxProviderConfig = { ...provider, adapter: copilotConfiguredWire.adapter,
|
|
321
|
+
headers: Object.fromEntries(headers) };
|
|
322
|
+
if (copilotConfiguredWire.responsesPath === undefined) delete base.responsesPath;
|
|
323
|
+
else base.responsesPath = copilotConfiguredWire.responsesPath;
|
|
324
|
+
if (copilotConfiguredWire.chatCompletionsPath === undefined) delete base.chatCompletionsPath;
|
|
325
|
+
else base.chatCompletionsPath = copilotConfiguredWire.chatCompletionsPath;
|
|
326
|
+
return base;
|
|
327
|
+
};
|
|
328
|
+
/** Resolve the request's ephemeral wire selection and recover at most one OAuth negotiation 401. */
|
|
329
|
+
const resolveCopilotSelection = async (requestParsed: OcxParsedRequest, insideDispatch = false): Promise<void> => {
|
|
330
|
+
if (route.providerName !== "github-copilot" || route.provider.authMode === "forward") return;
|
|
331
|
+
copilotConfiguredWire ??= {
|
|
332
|
+
adapter: route.provider.adapter,
|
|
333
|
+
responsesPath: route.provider.responsesPath,
|
|
334
|
+
chatCompletionsPath: route.provider.chatCompletionsPath,
|
|
335
|
+
apiVersion: new Headers(route.provider.headers).get("X-GitHub-Api-Version") ?? undefined,
|
|
336
|
+
};
|
|
337
|
+
/** Fence control-plane sends to the captured credential; reuse an already-held dispatch concurrency lease. */
|
|
338
|
+
const negotiate = () => {
|
|
339
|
+
const binding: DispatchBinding | undefined = oauthSelection && servingOAuthSnapshot
|
|
340
|
+
? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
|
|
341
|
+
: { kind: "api-key", provider: { ...route.provider } };
|
|
342
|
+
return resolveCopilotAuto(copilotNegotiationBase(route.provider), copilotRequestedModel, requestParsed,
|
|
343
|
+
options.abortSignal ?? req.signal, /** Fence each negotiation send against the captured credential binding before touching the network. */
|
|
344
|
+
() => selectionIsCurrent(binding), !insideDispatch);
|
|
345
|
+
};
|
|
346
|
+
let selected: Awaited<ReturnType<typeof resolveCopilotAuto>>;
|
|
347
|
+
try { selected = await negotiate(); }
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (!(error instanceof CopilotAutoHttpError) || error.status !== 401 || copilotNegotiationRefreshed
|
|
350
|
+
|| oauth401ReplayAttempted || route.provider.authMode !== "oauth" || !servingOAuthSnapshot) throw error;
|
|
351
|
+
copilotNegotiationRefreshed = true;
|
|
352
|
+
const refreshed = await refreshResolvedOAuthSelection(servingOAuthSnapshot);
|
|
353
|
+
route.provider = resolveProviderTransport(route.providerName,
|
|
354
|
+
{ ...route.provider, apiKey: refreshed.accessToken }, parsed.options.promptCacheKey,
|
|
355
|
+
resolveCopilotApiBaseUrl(refreshed.apiBaseUrl));
|
|
356
|
+
sentOAuthSnapshot = refreshed;
|
|
357
|
+
replayOAuthCredentialSnapshot = { accountId: refreshed.accountId, generation: refreshed.generation };
|
|
358
|
+
selected = await negotiate();
|
|
359
|
+
}
|
|
360
|
+
copilotSessionExpiresAt = selected.expiresAt ?? Infinity;
|
|
361
|
+
route.provider = selected.provider;
|
|
362
|
+
route.modelId = selected.modelId;
|
|
363
|
+
route.staticPolicy = captureRouteStaticPolicy(route.providerName, route.modelId, route.provider,
|
|
364
|
+
route.staticPolicy.effectiveAlias, inboundWire);
|
|
365
|
+
copilotAutoAdapter = selected.auto ? selected.provider.adapter : undefined;
|
|
366
|
+
for (const target of new Set([parsed, requestParsed])) {
|
|
367
|
+
target.modelId = selected.modelId;
|
|
368
|
+
if (target._rawBody && typeof target._rawBody === "object")
|
|
369
|
+
(target._rawBody as { model?: string }).model = selected.modelId;
|
|
370
|
+
target.context = renameRoutedIdentityInContext(target.context, selected.modelId);
|
|
371
|
+
}
|
|
372
|
+
logCtx.model = selected.modelId;
|
|
373
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.model = selected.modelId;
|
|
374
|
+
};
|
|
284
375
|
/**
|
|
285
376
|
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
286
377
|
*
|
|
@@ -306,6 +397,7 @@ export async function prepareResponsesTransport(
|
|
|
306
397
|
const applyFailoverSnapshot = async (
|
|
307
398
|
snapshot: OAuthAccessSnapshot,
|
|
308
399
|
retryParsed: OcxParsedRequest = parsed,
|
|
400
|
+
insideDispatch = false,
|
|
309
401
|
): Promise<OAuthAccessSnapshot | null> => {
|
|
310
402
|
requireAnthropicAdmission();
|
|
311
403
|
if (snapshot.provider !== route.providerName) return null;
|
|
@@ -342,6 +434,9 @@ export async function prepareResponsesTransport(
|
|
|
342
434
|
}
|
|
343
435
|
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
344
436
|
route.provider = rotatedProvider;
|
|
437
|
+
await resolveCopilotSelection(retryParsed, insideDispatch);
|
|
438
|
+
// Negotiation itself may have recovered a 401 with a newer committed credential.
|
|
439
|
+
if (route.providerName === "github-copilot" && servingOAuthSnapshot) snapshot = servingOAuthSnapshot;
|
|
345
440
|
if (route.providerName === "kiro") {
|
|
346
441
|
const kiroContext = { ...(snapshot.kiro ?? {}) };
|
|
347
442
|
// Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
|
|
@@ -411,7 +506,9 @@ export async function prepareResponsesTransport(
|
|
|
411
506
|
&& (anthropicInstance === undefined || !anthropicRouting!.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePolicy!.anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicQuota!.anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
|
|
412
507
|
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
413
508
|
};
|
|
509
|
+
/** Bind the negotiated wire adapter and its built requests to the serving credential's authority. */
|
|
414
510
|
const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
|
|
511
|
+
if (copilotAutoAdapter) provider = { ...provider, adapter: copilotAutoAdapter };
|
|
415
512
|
const resolved = resolveAdapter(provider, retention, route.providerName);
|
|
416
513
|
if (route.provider.authMode === "forward") return resolved;
|
|
417
514
|
const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
|
|
@@ -468,6 +565,7 @@ export async function prepareResponsesTransport(
|
|
|
468
565
|
}
|
|
469
566
|
return resolved;
|
|
470
567
|
};
|
|
568
|
+
/** Rebuild a stale queued selection while retaining the caller's dispatch lease and renewing Auto metadata. */
|
|
471
569
|
const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
472
570
|
requireAnthropicAdmission();
|
|
473
571
|
if (route.provider.authMode === "oauth") {
|
|
@@ -479,7 +577,7 @@ export async function prepareResponsesTransport(
|
|
|
479
577
|
const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
480
578
|
candidate = await anthropicRouting!.getAnthropicPoolAccessSnapshot(accountId);
|
|
481
579
|
}
|
|
482
|
-
if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed)) {
|
|
580
|
+
if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed, true)) {
|
|
483
581
|
if (anthropicInstance !== undefined) await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
484
582
|
throw new Error("OAuth account selection changed before dispatch");
|
|
485
583
|
}
|
|
@@ -487,6 +585,7 @@ export async function prepareResponsesTransport(
|
|
|
487
585
|
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
|
|
488
586
|
if (!current) throw new Error("API key selection is unavailable before dispatch");
|
|
489
587
|
route.provider = current;
|
|
588
|
+
await resolveCopilotSelection(requestParsed, true);
|
|
490
589
|
}
|
|
491
590
|
adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
|
|
492
591
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
@@ -552,13 +651,14 @@ export async function prepareResponsesTransport(
|
|
|
552
651
|
throw new Error("Account selection changed repeatedly before turn dispatch");
|
|
553
652
|
} finally { producer?.close(); }
|
|
554
653
|
};
|
|
555
|
-
|
|
654
|
+
/** Fence the physical send, with bounded credential/session rebuilds and safe negotiated refusals. */
|
|
655
|
+
const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed, owner: "adapter" | "passthrough" = "adapter"): ProviderFetchOptions["dispatchOverride"] => {
|
|
556
656
|
if (route.provider.authMode === "forward") return undefined;
|
|
557
|
-
return async (input, init, execute) => {
|
|
657
|
+
return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute) => {
|
|
558
658
|
let destination = input;
|
|
559
659
|
let dispatchInit = init;
|
|
560
660
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
561
|
-
if (selectionIsCurrent(requestBindings.get(wireRequest))) {
|
|
661
|
+
if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
|
|
562
662
|
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
|
|
563
663
|
const binding = requestBindings.get(wireRequest);
|
|
564
664
|
const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
|
|
@@ -598,7 +698,7 @@ export async function prepareResponsesTransport(
|
|
|
598
698
|
}
|
|
599
699
|
commitKeyAttemptSend();
|
|
600
700
|
response = await sendWithConnectionPolicy(
|
|
601
|
-
fetchImpl,
|
|
701
|
+
anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider),
|
|
602
702
|
destination,
|
|
603
703
|
{ ...dispatchInit, redirect: "manual" },
|
|
604
704
|
{ providerName: route.providerName, provider: route.provider },
|
|
@@ -622,7 +722,16 @@ export async function prepareResponsesTransport(
|
|
|
622
722
|
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
623
723
|
return response;
|
|
624
724
|
}
|
|
625
|
-
|
|
725
|
+
let nextAdapter: ProviderAdapter;
|
|
726
|
+
try { nextAdapter = await refreshDispatchAdapter(requestParsed); }
|
|
727
|
+
catch (error) {
|
|
728
|
+
const refusal = copilotRefusalResponse(error);
|
|
729
|
+
if (refusal) return refusal;
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
if (owner === "passthrough" && route.providerName === "github-copilot" && nextAdapter.name === "openai-chat") {
|
|
733
|
+
throw new CopilotAutoAdapterHandoff(nextAdapter);
|
|
734
|
+
}
|
|
626
735
|
// Rebind before rebuilding: the rebuild's bridged-search restore and continuation
|
|
627
736
|
// restore key on the serving identity, which must be the refreshed route's, not the
|
|
628
737
|
// credential whose selection just lapsed.
|
|
@@ -676,12 +785,9 @@ export async function prepareResponsesTransport(
|
|
|
676
785
|
// Route names may resemble account IDs; log only the matched rule position.
|
|
677
786
|
if (anthropicRouteDecision) console.warn(`[${anthropicInstance}-pool] route:#${anthropicRouteDecision.position} ${selection.reason}; answering locally`);
|
|
678
787
|
if (selection.reason === "all-cooled") {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"rate_limit_error",
|
|
683
|
-
anthropicRouteDecision ? "Anthropic OAuth accounts for this model route are temporarily rate-limited" : "All Anthropic OAuth accounts are temporarily rate-limited",
|
|
684
|
-
retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
|
|
788
|
+
return anthropicPoolCooldownResponse(
|
|
789
|
+
anthropicRouting!.anthropicPoolCooldownError(Date.now(), anthropicRouteDecision, route.modelId),
|
|
790
|
+
{ comboAttempt: options.comboAttempt, logCtx },
|
|
685
791
|
);
|
|
686
792
|
}
|
|
687
793
|
return formatErrorResponse(401, "authentication_error", anthropicRouteDecision ? "No eligible Anthropic OAuth account for this model route" : "No eligible Anthropic OAuth account available");
|
|
@@ -876,7 +982,15 @@ export async function prepareResponsesTransport(
|
|
|
876
982
|
? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
|
|
877
983
|
: undefined,
|
|
878
984
|
);
|
|
985
|
+
try { await resolveCopilotSelection(parsed); }
|
|
986
|
+
catch (error) {
|
|
987
|
+
if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
|
|
988
|
+
const refusal = copilotRefusalResponse(error);
|
|
989
|
+
if (refusal) return refusal;
|
|
990
|
+
return formatErrorResponse(502, "provider_error", "GitHub Copilot Auto negotiation failed; retry or check your Copilot account permissions.");
|
|
991
|
+
}
|
|
879
992
|
let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
|
|
993
|
+
if (copilotAutoAdapter) adapterProvider = { ...adapterProvider, adapter: copilotAutoAdapter };
|
|
880
994
|
const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
881
995
|
&& !isCanonicalOpenAiForwardProvider(adapterProvider)
|
|
882
996
|
&& ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
|
|
@@ -1008,6 +1122,8 @@ export async function prepareResponsesTransport(
|
|
|
1008
1122
|
set genericFailovers(value: typeof genericFailovers) {
|
|
1009
1123
|
genericFailovers = value;
|
|
1010
1124
|
},
|
|
1125
|
+
get oauth401ReplayAttempted(): boolean { return oauth401ReplayAttempted; },
|
|
1126
|
+
set oauth401ReplayAttempted(value: boolean) { oauth401ReplayAttempted = value; },
|
|
1011
1127
|
get adapter(): ProviderAdapter {
|
|
1012
1128
|
return adapter;
|
|
1013
1129
|
},
|
|
@@ -1065,6 +1181,8 @@ export async function prepareResponsesTransport(
|
|
|
1065
1181
|
applyFailoverSnapshot,
|
|
1066
1182
|
selectionIsCurrent,
|
|
1067
1183
|
resolveSelectionAdapter,
|
|
1184
|
+
resolveCopilotSelection,
|
|
1185
|
+
copilotRefusalResponse,
|
|
1068
1186
|
refreshRunTurnAdapter,
|
|
1069
1187
|
oauthDispatch,
|
|
1070
1188
|
noteRoutedAttemptSend,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapSpendLedgerStorageError } from "./spend-storage-error";
|
|
1
2
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
2
3
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
4
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -20,7 +21,7 @@ import type { AttemptRecoveryKind } from "../../usage/log";
|
|
|
20
21
|
import { providerFetch } from "./fetch-helpers";
|
|
21
22
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
22
23
|
import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
|
|
23
|
-
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
24
|
+
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE, type OcxErrorPayload } from "../../lib/errors";
|
|
24
25
|
import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
25
26
|
import {
|
|
26
27
|
hasEligibleGenericOAuthFailoverTarget,
|
|
@@ -315,7 +316,8 @@ export async function executeResponsesRunTurn(
|
|
|
315
316
|
Object.assign(parsed, routeState);
|
|
316
317
|
}
|
|
317
318
|
} catch (err) {
|
|
318
|
-
|
|
319
|
+
const storageRefusal = mapSpendLedgerStorageError(err, logCtx);
|
|
320
|
+
emit(storageRefusal ? { type: "error", ...storageRefusal } : err instanceof RequestPacingQueueOverloadError
|
|
319
321
|
? {
|
|
320
322
|
type: "error",
|
|
321
323
|
status: 429,
|
|
@@ -713,7 +715,8 @@ export async function executeResponsesRunTurn(
|
|
|
713
715
|
runTurnAbort.abort();
|
|
714
716
|
queue.close();
|
|
715
717
|
const message = preflight.error?.message ?? "Adapter ended before producing a response";
|
|
716
|
-
const failure = formatErrorResponse(502,
|
|
718
|
+
const failure = formatErrorResponse(preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 502,
|
|
719
|
+
preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
|
|
717
720
|
// A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
|
|
718
721
|
// combo must not send this turn to another target: the failure stays with this child.
|
|
719
722
|
if (preflight.replayUnsafe) markResponseNonReplayable(failure);
|
|
@@ -912,7 +915,9 @@ export async function executeResponsesRunTurn(
|
|
|
912
915
|
const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
|
|
913
916
|
? firstMeaningful.message
|
|
914
917
|
: "Adapter ended before producing a response");
|
|
915
|
-
const
|
|
918
|
+
const storageRefusal = firstMeaningful?.type === "error" && firstMeaningful.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
919
|
+
const failure = formatErrorResponse(storageRefusal ? 503 : 502,
|
|
920
|
+
storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
|
|
916
921
|
if (replayUnsafe) markResponseNonReplayable(failure);
|
|
917
922
|
return failure;
|
|
918
923
|
}
|
|
@@ -956,7 +961,10 @@ export async function executeResponsesRunTurn(
|
|
|
956
961
|
commitReasoningReplayServingRoute();
|
|
957
962
|
}
|
|
958
963
|
notifyResponseComplete(json);
|
|
959
|
-
return new Response(JSON.stringify(json), {
|
|
964
|
+
return new Response(JSON.stringify(json), {
|
|
965
|
+
status: (json.error as OcxErrorPayload | undefined)?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 200,
|
|
966
|
+
headers: { "Content-Type": "application/json" },
|
|
967
|
+
});
|
|
960
968
|
} catch (error) {
|
|
961
969
|
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
962
970
|
runTurnAbort.abort();
|
|
@@ -32,7 +32,7 @@ import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
|
32
32
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
33
33
|
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
34
34
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
35
|
-
import { readDisplaySafeErrorText } from "./core-errors";
|
|
35
|
+
import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
36
36
|
import {
|
|
37
37
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
38
38
|
anthropicRoutingFor,
|
|
@@ -79,6 +79,7 @@ export async function executeResponsesSidecars(
|
|
|
79
79
|
| "anthropicSessionKey"
|
|
80
80
|
| "commitResolvedOAuthSelection"
|
|
81
81
|
| "resolveSelectionAdapter"
|
|
82
|
+
| "resolveCopilotSelection"
|
|
82
83
|
| "oauthDispatch"
|
|
83
84
|
| "noteRoutedAttemptSend"
|
|
84
85
|
| "bindKeyUsageFromBridge"
|
|
@@ -103,6 +104,7 @@ export async function executeResponsesSidecars(
|
|
|
103
104
|
anthropicSessionKey,
|
|
104
105
|
commitResolvedOAuthSelection,
|
|
105
106
|
resolveSelectionAdapter,
|
|
107
|
+
resolveCopilotSelection,
|
|
106
108
|
oauthDispatch,
|
|
107
109
|
} = transportState;
|
|
108
110
|
const {
|
|
@@ -196,6 +198,7 @@ export async function executeResponsesSidecars(
|
|
|
196
198
|
// A single request may rotate once for a verification refusal. Keep this separate from the
|
|
197
199
|
// broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
|
|
198
200
|
let antigravityValidationRotationAttempted = false;
|
|
201
|
+
/** Admit a replacement credential, or leave the loop's original refusal authoritative. */
|
|
199
202
|
const rotateSidecarProviderOn429 = async (
|
|
200
203
|
retryAfter: string | null,
|
|
201
204
|
responseHeaders?: Headers,
|
|
@@ -280,6 +283,8 @@ export async function executeResponsesSidecars(
|
|
|
280
283
|
// `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
|
|
281
284
|
} else if (rotated) {
|
|
282
285
|
route.provider = rotated;
|
|
286
|
+
try { await resolveCopilotSelection(retryParsed ?? parsed); }
|
|
287
|
+
catch { return null; } // Keep the sidecar's original refusal when replacement negotiation fails.
|
|
283
288
|
} else if (
|
|
284
289
|
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
285
290
|
// unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
|
|
@@ -550,7 +555,7 @@ export async function executeResponsesSidecars(
|
|
|
550
555
|
modelId: route.modelId,
|
|
551
556
|
...sidecarBudget.fetchOptions(transportState.adapter),
|
|
552
557
|
})(input, init)) as typeof globalThis.fetch;
|
|
553
|
-
const
|
|
558
|
+
const loopResponse = await runWithWebSearch({
|
|
554
559
|
parsed, adapter: transportState.adapter,
|
|
555
560
|
onIterationEnd: sidecarBudget.releaseUnsentHop,
|
|
556
561
|
fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
@@ -617,6 +622,15 @@ export async function executeResponsesSidecars(
|
|
|
617
622
|
notifyResponseComplete(response);
|
|
618
623
|
},
|
|
619
624
|
});
|
|
625
|
+
// A routed Anthropic model whose last pool account refused on spent quota fails the loop's
|
|
626
|
+
// first iteration with a generic 429 before any search runs. Answer the usage limit instead,
|
|
627
|
+
// as the main path does (#6991), so Codex stops rather than retrying the whole turn.
|
|
628
|
+
const poolUsageLimit = loopResponse.status === 429 && anthropicInstance && route.provider.authMode === "oauth"
|
|
629
|
+
&& !options.comboAttempt
|
|
630
|
+
? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
|
|
631
|
+
: null;
|
|
632
|
+
if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
|
|
633
|
+
const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx }) : loopResponse;
|
|
620
634
|
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
621
635
|
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
622
636
|
if (wsResponse.body) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import { SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE } from "../../lib/errors";
|
|
3
|
+
import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
|
|
4
|
+
import { SpendLedgerFileRefusedError } from "../../lib/spend-reservation-ledger";
|
|
5
|
+
|
|
6
|
+
type StorageRefusalLog = Pick<RequestLogContext, "localTerminalReason" | "terminalSource" | "errorCode">;
|
|
7
|
+
|
|
8
|
+
/** Local storage refusal, including retry wrappers: never provider-health evidence. */
|
|
9
|
+
export function mapSpendLedgerStorageError(error: unknown, logCtx?: StorageRefusalLog) {
|
|
10
|
+
const seen = new Set<unknown>();
|
|
11
|
+
while (error instanceof Error && !seen.has(error)) {
|
|
12
|
+
if (error instanceof SpendLedgerFileRefusedError) {
|
|
13
|
+
if (logCtx) {
|
|
14
|
+
markLocalRequestLogRefusal(logCtx, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE);
|
|
15
|
+
logCtx.errorCode = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
status: 503,
|
|
19
|
+
errorType: "server_error",
|
|
20
|
+
code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
|
|
21
|
+
message: error.message,
|
|
22
|
+
} as const;
|
|
23
|
+
}
|
|
24
|
+
seen.add(error);
|
|
25
|
+
error = error.cause;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function spendLedgerStorageErrorResponse(error: unknown, logCtx?: StorageRefusalLog): Response | undefined {
|
|
31
|
+
const refusal = mapSpendLedgerStorageError(error, logCtx);
|
|
32
|
+
return refusal && formatErrorResponse(refusal.status, refusal.code, refusal.message);
|
|
33
|
+
}
|
|
@@ -23,8 +23,9 @@ import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, CODEX_WS_FRAME_HEADER
|
|
|
23
23
|
import { codexWsExchange } from "./codex-ws-exchange";
|
|
24
24
|
import { CodexWsSession } from "./codex-ws-session";
|
|
25
25
|
import { codexWsPool, codexWsReuseIdentity } from "./codex-ws-pool";
|
|
26
|
-
import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
|
|
26
|
+
import { codexWsCreateFrameExceedsLimit, type CodexWsPreOutputRefusalHold } from "./codex-ws-wire";
|
|
27
27
|
import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
|
|
28
|
+
import { outboundCredentials } from "../../lib/outbound-credential-registry";
|
|
28
29
|
export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
|
|
29
30
|
MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
|
|
30
31
|
isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./codex-ws-wire";
|
|
@@ -162,6 +163,8 @@ export async function codexWsUpstreamFetch(
|
|
|
162
163
|
nativeControl?: NativeResponseControl,
|
|
163
164
|
beforeContinuation?: () => Promise<void>,
|
|
164
165
|
onPhysicalDispatch?: () => void,
|
|
166
|
+
/** Opt-in pre-output refusal hold (#6969); see codexWsExchange. */
|
|
167
|
+
preOutputRefusalHold?: CodexWsPreOutputRefusalHold,
|
|
165
168
|
): Promise<Response> {
|
|
166
169
|
const prepared = prepareCodexWsRequest(url, init);
|
|
167
170
|
if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
|
|
@@ -221,6 +224,10 @@ export async function codexWsUpstreamFetch(
|
|
|
221
224
|
// reused for a different destination or with stale plugin headers.
|
|
222
225
|
const dial = planCodexWsDial(wsUrl, headers, proxy);
|
|
223
226
|
if (!dial) return sseFallback(url, init);
|
|
227
|
+
// The final dial headers, after plugin rewrites, are what the WebSocket upstream receives; the
|
|
228
|
+
// per-turn headers planning drops from the upgrade still reach it inside the frame metadata.
|
|
229
|
+
outboundCredentials().remember(headers);
|
|
230
|
+
outboundCredentials().remember(dial.headers);
|
|
224
231
|
const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
|
|
225
232
|
const reuseAcrossTurns = identity !== null && codexWsReuseAcrossTurnsEnabled();
|
|
226
233
|
const pooled = identity
|
|
@@ -243,6 +250,7 @@ export async function codexWsUpstreamFetch(
|
|
|
243
250
|
session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
|
|
244
251
|
nativeControl: control,
|
|
245
252
|
beforeContinuation, onPhysicalDispatch,
|
|
253
|
+
preOutputRefusalHold,
|
|
246
254
|
bunVersion: typeof runtime === "string" ? runtime : runtime.version,
|
|
247
255
|
});
|
|
248
256
|
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
5
|
+
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
5
6
|
import {
|
|
6
7
|
WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER,
|
|
7
8
|
isWindowsSchtasksCreateAccessDenied,
|
|
@@ -61,6 +62,18 @@ export function setStartupInstallFinalizeForTests(next: FinalizeFn | null): void
|
|
|
61
62
|
finalizeImpl = next ?? finalizeWindowsSchedulerServiceRegistration;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
interface StartupInstallLaunch {
|
|
66
|
+
execFile?: typeof execFile;
|
|
67
|
+
/** Overrides Bun's own standalone detection, which a source-mode test cannot flip. */
|
|
68
|
+
isStandaloneExecutable?: boolean;
|
|
69
|
+
}
|
|
70
|
+
let installLaunch: StartupInstallLaunch = {};
|
|
71
|
+
|
|
72
|
+
/** Test-only seam: observe the installer child's argv and simulate a compiled `ocx`. */
|
|
73
|
+
export function setStartupInstallLaunchForTests(next: StartupInstallLaunch | null): void {
|
|
74
|
+
installLaunch = next ?? {};
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/** Snapshot of the install lock for diagnostics and tests. */
|
|
65
78
|
export function getStartupInstallState(): StartupInstallState {
|
|
66
79
|
return installState;
|
|
@@ -162,12 +175,19 @@ function runCliInstall(
|
|
|
162
175
|
): Promise<{ stdout: string; stderr: string }> {
|
|
163
176
|
const bun = durableBunPath();
|
|
164
177
|
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
165
|
-
|
|
178
|
+
// The Desktop app and the Linux deb run a compiled `ocx`, where `process.execPath` is the CLI
|
|
179
|
+
// itself: prefixing the source entrypoint made it an unknown command and every dashboard
|
|
180
|
+
// Install/Repair failed (#6963). Same builder as the startup-health probe.
|
|
181
|
+
const argv = selfLaunchArgv(startupInstallArgv(action, options), {
|
|
182
|
+
sourceEntrypoint: cli,
|
|
183
|
+
isStandaloneExecutable: installLaunch.isStandaloneExecutable,
|
|
184
|
+
});
|
|
185
|
+
const run = installLaunch.execFile ?? execFile;
|
|
166
186
|
return new Promise((resolve, reject) => {
|
|
167
187
|
const timeout = process.platform === "win32" && action === "install-service"
|
|
168
188
|
? 0
|
|
169
189
|
: 60_000;
|
|
170
|
-
|
|
190
|
+
run(bun, argv, {
|
|
171
191
|
encoding: "utf8",
|
|
172
192
|
env: process.env,
|
|
173
193
|
// A fresh Windows scheduler install now owns its UAC prompt inside this CLI
|
|
@@ -9,6 +9,7 @@ import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
|
9
9
|
import { desktopStartupOwnership } from "../service/desktop-startup";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
11
11
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
12
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
12
13
|
|
|
13
14
|
const CACHE_TTL_MS = 30_000;
|
|
14
15
|
const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
|
|
@@ -69,6 +70,8 @@ export function getStartupHealthSnapshot(
|
|
|
69
70
|
): StartupHealth {
|
|
70
71
|
const now = deps.now ?? Date.now;
|
|
71
72
|
if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
|
|
73
|
+
// A passive read (a Remote Link Child's, #6959) starts no diagnostic process on this machine.
|
|
74
|
+
if (isPassiveRead()) return staleOrFallback(config);
|
|
72
75
|
refreshInBackground(config, deps);
|
|
73
76
|
return staleOrFallback(config);
|
|
74
77
|
}
|
|
@@ -189,6 +192,7 @@ export async function getCachedStartupHealth(
|
|
|
189
192
|
): Promise<StartupHealth> {
|
|
190
193
|
const now = deps.now ?? Date.now;
|
|
191
194
|
if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
|
|
195
|
+
if (isPassiveRead()) return staleOrFallback(config);
|
|
192
196
|
refreshInBackground(config, deps);
|
|
193
197
|
// An expired or empty read is an explicit protection check. Wait for the
|
|
194
198
|
// isolated probe instead of presenting a synthetic failure while that probe
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
|
|
2
2
|
import { delimiter, join } from "node:path";
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
|
-
import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
4
|
+
import { claudeTierDisplayName, claudeToolSearchEnv, isClaudeTierLabelSlot, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
5
5
|
import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
|
|
6
6
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
7
7
|
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
|
|
@@ -115,7 +115,12 @@ export function writeShellEnvFile(
|
|
|
115
115
|
}
|
|
116
116
|
for (const [name, value] of Object.entries(modelEnv)) {
|
|
117
117
|
if (name === "ANTHROPIC_MODEL") continue;
|
|
118
|
-
|
|
118
|
+
// An alias tier slot carries its picker label (#6926) in the same branch, so the label is
|
|
119
|
+
// exported only when this file writes the slot and the shell has no label of its own.
|
|
120
|
+
const label = isClaudeTierLabelSlot(name) ? claudeTierDisplayName(value) : undefined;
|
|
121
|
+
lines.push(label === undefined
|
|
122
|
+
? conditional(name, value)
|
|
123
|
+
: `${conditional(name, value)} && [ -z "\${${name}_NAME+x}" ] && export ${name}_NAME=${shellValue(label)}`);
|
|
119
124
|
}
|
|
120
125
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
121
126
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
package/src/server/system-env.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
|
-
import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
5
|
+
import { CLAUDE_TIER_LABEL_SLOTS, claudeTierDisplayName, claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
6
6
|
import { PROXY_MARKER } from "../claude/auth-detect";
|
|
7
7
|
import { isProxyAdmissionSecret } from "./auth-cors";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
@@ -30,6 +30,8 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
|
|
|
30
30
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
31
31
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
32
32
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
33
|
+
// Picker labels paired with an alias tier slot this instance injected (#6926).
|
|
34
|
+
...CLAUDE_TIER_LABEL_SLOTS.map(slot => `${slot}_NAME`),
|
|
33
35
|
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
34
36
|
"DISABLE_COMPACT",
|
|
35
37
|
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
@@ -315,6 +317,14 @@ export async function injectSystemEnv(
|
|
|
315
317
|
if (name === "ANTHROPIC_MODEL") continue; // legacy slot handled by shell file only (back-compat)
|
|
316
318
|
injectLever(name, value);
|
|
317
319
|
}
|
|
320
|
+
// Picker labels (#6926) only for alias tier slots this instance owns after the loop above: a
|
|
321
|
+
// launchd slot the user set keeps whatever label it has, and a label nothing produces any
|
|
322
|
+
// more is unset by the produced-key sweep below like any other tracked lever.
|
|
323
|
+
for (const slot of CLAUDE_TIER_LABEL_SLOTS) {
|
|
324
|
+
const value = modelEnv[slot];
|
|
325
|
+
const label = value === undefined ? undefined : claudeTierDisplayName(value);
|
|
326
|
+
if (label !== undefined && injectedKeys.includes(slot)) injectLever(`${slot}_NAME`, label);
|
|
327
|
+
}
|
|
318
328
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
319
329
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
320
330
|
injectLever("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
|
package/src/service/cli.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { win32 } from "node:path";
|
|
|
27
27
|
import { serviceDiagnosticsSummary } from "./diagnostics";
|
|
28
28
|
import { runServiceClaim } from "./claim";
|
|
29
29
|
import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
|
|
30
|
+
import { linuxServiceBootHint } from "./linux-linger";
|
|
31
|
+
import { diagnoseDesktopStartup, desktopLoginStatusSummary } from "./desktop-startup";
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* `restart` is NO LONGER folded into `repair`.
|
|
@@ -336,7 +338,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
336
338
|
// and the baked one cannot have diverged yet — unlike `start`, which reads the
|
|
337
339
|
// installed artifact instead.
|
|
338
340
|
await reportServiceServing("installed", { port: resolveServiceListenPort() });
|
|
339
|
-
if (process.platform === "linux") console.log(
|
|
341
|
+
if (process.platform === "linux") console.log(linuxServiceBootHint());
|
|
340
342
|
// Service users never reach the `ocx start` prompt: the proxy they run is the
|
|
341
343
|
// supervised child, which always carries OCX_SERVICE=1. This command, though, is
|
|
342
344
|
// hand-typed in a real terminal, so it is the one interactive moment they get.
|
|
@@ -433,6 +435,8 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
433
435
|
const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
|
|
434
436
|
if (lease) console.log(lease);
|
|
435
437
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
438
|
+
const desktop = diagnoseDesktopStartup();
|
|
439
|
+
if (desktop) console.log(desktopLoginStatusSummary(desktop));
|
|
436
440
|
break;
|
|
437
441
|
}
|
|
438
442
|
case "uninstall":
|