@bitkyc08/opencodex 2.89.0 → 2.90.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.
Files changed (129) hide show
  1. package/gui/dist/assets/App-DgxkE6Md.js +58 -0
  2. package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-DbTNc_88.js} +1 -1
  3. package/gui/dist/assets/{index-uchD-ngf.js → index-TmaHa54u.js} +5 -5
  4. package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +2 -2
  7. package/src/adapters/base.ts +4 -0
  8. package/src/adapters/claude-cli/adapter.ts +13 -7
  9. package/src/adapters/claude-cli/usage-admission.ts +14 -2
  10. package/src/adapters/coding-agent/turn.ts +14 -4
  11. package/src/adapters/cursor/protobuf-request.ts +3 -0
  12. package/src/adapters/google-progress-history.ts +19 -1
  13. package/src/adapters/google.ts +28 -9
  14. package/src/adapters/image.ts +5 -3
  15. package/src/adapters/input-media-guard.ts +5 -2
  16. package/src/adapters/kiro-refusal.ts +41 -1
  17. package/src/adapters/ollama-native.ts +1 -0
  18. package/src/adapters/openai-chat/messages.ts +34 -4
  19. package/src/adapters/openai-chat/passthrough.ts +37 -4
  20. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  21. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  22. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  23. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  24. package/src/adapters/openai-chat.ts +7 -6
  25. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  26. package/src/adapters/openai-responses/passthrough.ts +5 -1
  27. package/src/adapters/openai-responses/transport.ts +13 -1
  28. package/src/claude/desktop-3p-library.ts +43 -3
  29. package/src/claude/desktop-3p.ts +20 -10
  30. package/src/claude/desktop-picker-profile.ts +18 -9
  31. package/src/claude/desktop-remote-store-io.ts +2 -2
  32. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  33. package/src/codex/account-lifecycle.ts +19 -6
  34. package/src/codex/account-store.ts +27 -8
  35. package/src/codex/account-usability.ts +4 -1
  36. package/src/codex/auth-api/account-list.ts +10 -5
  37. package/src/codex/auth-api/account-pause-group.ts +2 -22
  38. package/src/codex/auth-api/login-collision.ts +59 -0
  39. package/src/codex/auth-api/login-flow.ts +12 -7
  40. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  41. package/src/codex/auth-collision.ts +31 -4
  42. package/src/codex/auth-context.ts +48 -6
  43. package/src/codex/cli-update-plan.ts +18 -3
  44. package/src/codex/collision-identity.ts +78 -0
  45. package/src/codex/main-account-hard-lock.ts +14 -0
  46. package/src/codex/routing/cooldown-math.ts +5 -0
  47. package/src/codex/routing/failure-window.ts +61 -9
  48. package/src/codex/routing.ts +4 -3
  49. package/src/codex/shim-diagnostics.ts +2 -1
  50. package/src/codex/shim-fingerprint.ts +11 -2
  51. package/src/codex/shim-templates.ts +16 -5
  52. package/src/codex/shim-windows-refresh.ts +224 -0
  53. package/src/codex/shim.ts +45 -21
  54. package/src/combos/failover.ts +8 -1
  55. package/src/config/provider-validation.ts +7 -2
  56. package/src/generated/compatibility-version.json +168 -108
  57. package/src/generated/model-metadata.ts +1 -1
  58. package/src/images/loop.ts +2 -2
  59. package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
  60. package/src/lib/provider-admission-wait.ts +14 -0
  61. package/src/lib/service-secrets.ts +37 -2
  62. package/src/lib/translator-budget.ts +1 -1
  63. package/src/lib/upstream-retry.ts +15 -0
  64. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  65. package/src/oauth/account-import/parser.ts +15 -2
  66. package/src/oauth/account-import/types.ts +7 -0
  67. package/src/oauth/chatgpt-shared.ts +10 -1
  68. package/src/oauth/dsh.ts +45 -2
  69. package/src/oauth/kiro.ts +44 -1
  70. package/src/oauth/login-flow-state.ts +11 -2
  71. package/src/providers/claude-cli-usage.ts +20 -5
  72. package/src/providers/initial-model-selection.ts +2 -2
  73. package/src/providers/key-failover.ts +10 -2
  74. package/src/providers/key-store.ts +8 -3
  75. package/src/providers/mistral-transport.ts +207 -0
  76. package/src/providers/openai-sidecar.ts +5 -0
  77. package/src/providers/quota/anthropic-account-quota.ts +18 -6
  78. package/src/providers/quota/antigravity.ts +6 -2
  79. package/src/providers/quota/failure-codes.ts +18 -0
  80. package/src/providers/quota/vendor-probes-oauth.ts +12 -3
  81. package/src/providers/quota-routing-cache.ts +1 -1
  82. package/src/providers/quota.ts +5 -1
  83. package/src/providers/registry/entries-extended.ts +6 -1
  84. package/src/providers/routed-config.ts +1 -1
  85. package/src/providers/xai-transport.ts +2 -1
  86. package/src/responses/audio.ts +46 -0
  87. package/src/responses/input-media.ts +37 -15
  88. package/src/responses/parser-content.ts +22 -27
  89. package/src/responses/reasoning-retention.ts +218 -11
  90. package/src/responses/schema.ts +3 -4
  91. package/src/responses/web-search-replay.ts +11 -2
  92. package/src/server/chat-native.ts +8 -7
  93. package/src/server/index/serve-options.ts +8 -2
  94. package/src/server/index/websocket-handler.ts +10 -0
  95. package/src/server/lifecycle.ts +17 -1
  96. package/src/server/management/oauth-account-routes.ts +2 -1
  97. package/src/server/messages-native.ts +11 -0
  98. package/src/server/request-log.ts +16 -7
  99. package/src/server/responses/adapter-continuation.ts +2 -2
  100. package/src/server/responses/adapter-dispatch.ts +2 -2
  101. package/src/server/responses/codex-ws-exchange.ts +3 -0
  102. package/src/server/responses/codex-ws-metadata.ts +44 -0
  103. package/src/server/responses/codex-ws-session.ts +21 -1
  104. package/src/server/responses/combo-stream-preflight.ts +7 -2
  105. package/src/server/responses/compact.ts +2 -0
  106. package/src/server/responses/core-auth.ts +20 -0
  107. package/src/server/responses/core-codex-account.ts +21 -14
  108. package/src/server/responses/core-combo.ts +3 -0
  109. package/src/server/responses/core-opaque-recovery.ts +20 -11
  110. package/src/server/responses/deepseek-image-files.ts +121 -0
  111. package/src/server/responses/empty-completion-guard.ts +8 -0
  112. package/src/server/responses/fetch-helpers.ts +30 -10
  113. package/src/server/responses/input-admission.ts +6 -2
  114. package/src/server/responses/main-hard-lock-routing.ts +33 -8
  115. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  116. package/src/server/responses/passthrough-completion.ts +41 -0
  117. package/src/server/responses/passthrough-delivery.ts +2 -0
  118. package/src/server/responses/passthrough-dispatch.ts +13 -13
  119. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  120. package/src/server/responses/reasoning-archive-error.ts +21 -0
  121. package/src/server/responses/request-prepare.ts +11 -1
  122. package/src/server/responses/request-transport.ts +61 -3
  123. package/src/service/orchestration.ts +17 -0
  124. package/src/types/provider.ts +2 -0
  125. package/src/types/request.ts +16 -1
  126. package/src/types.ts +1 -0
  127. package/src/web-search/loop.ts +2 -2
  128. package/gui/dist/assets/App-BnRSujOq.js +0 -58
  129. package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
@@ -1,4 +1,4 @@
1
- import { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
1
+ import { recoverAutoPausedAccount, rotatePassthroughKeyOn429 } from "./passthrough-key-rotation";
2
2
  import { adoptCodexWsStage, capturePassthroughAffinity } from "./passthrough-observations";
3
3
  import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
4
4
  import { requestPacingOverloadResponse } from "./pacing-overload";
@@ -53,6 +53,7 @@ import {
53
53
  import { restoreRoutedCustomCalls, RoutedCustomToolCompatError } from "../../responses/custom-tool-compat";
54
54
  import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
55
55
  import { MuseToolChoiceCompatibilityError } from "../../adapters/openai-responses/muse-tool-choice";
56
+ import { OpenGatewayInputCompatibilityError } from "../../adapters/openai-responses/opengateway-input";
56
57
  import { formatErrorResponse } from "../../bridge";
57
58
  import { redactSecretString } from "../../lib/redact";
58
59
  import {
@@ -132,7 +133,7 @@ import {
132
133
  import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
133
134
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
134
135
  import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
135
- import { recordCodexUpstreamOutcome } from "../../codex/routing";
136
+ import { codexFailureWindowOutcomeMeta, recordCodexUpstreamOutcome } from "../../codex/routing";
136
137
  import { describeUpstreamConnectFailure } from "./upstream-error";
137
138
  import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
138
139
  import {
@@ -172,6 +173,7 @@ import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION }
172
173
  import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
173
174
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
174
175
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
176
+ import { guardPassthroughCompletion, parseOutboundRequestBody } from "./passthrough-completion";
175
177
 
176
178
  /** Transfer an uncommitted Copilot exchange to the existing translated pipeline without new admission. */
177
179
  export interface PassthroughAdapterHandoff { kind: "adapter-handoff" }
@@ -386,6 +388,7 @@ export async function preparePassthroughExchange(
386
388
  || error instanceof XaiToolSchemaCompatibilityError
387
389
  || error instanceof RoutedCustomToolCompatError
388
390
  || error instanceof MuseToolChoiceCompatibilityError
391
+ || error instanceof OpenGatewayInputCompatibilityError
389
392
  ) {
390
393
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
391
394
  }
@@ -434,16 +437,6 @@ export async function preparePassthroughExchange(
434
437
  // `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
435
438
  // `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
436
439
  // too because the proxy cannot establish the caller's declared authorization boundary.
437
- const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
438
- try {
439
- const body = JSON.parse(bodyText) as unknown;
440
- return body && typeof body === "object" && !Array.isArray(body)
441
- ? body as Record<string, unknown>
442
- : undefined;
443
- } catch {
444
- return undefined;
445
- }
446
- };
447
440
  let outboundRequestBody: Record<string, unknown> | undefined;
448
441
  const declaredWireToolNames = new Set<string>();
449
442
  const declaredBareWireToolNames = new Set<string>();
@@ -990,6 +983,7 @@ export async function preparePassthroughExchange(
990
983
  }
991
984
  if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
992
985
  recordCodexUpstreamOutcome(config, admissionState.authCtx.accountId, outcome, {
986
+ ...codexFailureWindowOutcomeMeta(admissionState.authCtx),
993
987
  threadId: admissionState.authCtx.affinityKey,
994
988
  fixedAccount: admissionState.authCtx.fixedAccount,
995
989
  modelId: route.modelId,
@@ -1157,6 +1151,7 @@ export async function preparePassthroughExchange(
1157
1151
  `${route.providerName}|${route.modelId}|${recovery}`,
1158
1152
  { allowFinalRecoveryReserve: transientSendPolicy() === null },
1159
1153
  );
1154
+ if (recovery === "empty-completion") allowance.attempts = Math.min(1, allowance.attempts);
1160
1155
  try {
1161
1156
  return await fetchWithTransientRetry(
1162
1157
  innerRecovery => {
@@ -1188,7 +1183,8 @@ export async function preparePassthroughExchange(
1188
1183
  .then(adoptObservedResponse);
1189
1184
  },
1190
1185
  { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
1191
- onSendsConsumed: transientSendReporter(allowance.permit), claimAmbiguousResend: claimPreHeaderResend },
1186
+ onSendsConsumed: transientSendReporter(allowance.permit),
1187
+ claimAmbiguousResend: recovery === "empty-completion" ? undefined : claimPreHeaderResend },
1192
1188
  );
1193
1189
  } catch (err) {
1194
1190
  return { failed: transportFailureResponse(err) };
@@ -1606,6 +1602,7 @@ export async function preparePassthroughExchange(
1606
1602
  return transportFailureResponse(err);
1607
1603
  }
1608
1604
  }
1605
+ const keyRotation = await rotatePassthroughKeyOn429({ config, route, parsed, response: upstreamResponse, adapterName: transportState.adapter.name, budgetSpent: () => sendBudgetExhausted(transientSendAttempts()), refetch: () => rebuildAndRefetch("key-429") }); if (keyRotation) { if ("failed" in keyRotation) return keyRotation.failed; upstreamResponse = keyRotation; continue passthroughRecovery; }
1609
1606
  const captureAffinityResponse = (
1610
1607
  response: Response,
1611
1608
  captureAuthCtx: CodexAuthContext = admissionState.authCtx,
@@ -1942,6 +1939,9 @@ export async function preparePassthroughExchange(
1942
1939
  break;
1943
1940
  }
1944
1941
 
1942
+ upstreamResponse = await guardPassthroughCompletion(
1943
+ upstreamResponse, request, config, upstream.signal, logCtx, rebuildAndRefetch,
1944
+ );
1945
1945
  // Where this relay dials upstream. Local infrastructure (loopback / private / `.local` / `.lan`)
1946
1946
  // is operator-trusted and its silent phases are normal, so an unset stall budget resolves to
1947
1947
  // disabled for it. Provider rotation above keeps the same endpoint origin, so the routed
@@ -0,0 +1,40 @@
1
+ import { isNonReplayableResponse } from "../../lib/upstream-retry";
2
+ import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
3
+ import type { RouteResult } from "../../router";
4
+ import type { OcxConfig, OcxParsedRequest } from "../../types";
5
+ import { bindRouteReasoningReplayScope } from "./core-replay";
6
+
7
+ // Passthrough's credential moves share one import site so passthrough-dispatch.ts stays under its line cap.
8
+ export { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
9
+
10
+ /**
11
+ * Key-pool failover for a native passthrough 429 before any output. This wire returns before the
12
+ * adapter loop's key-429 arm, so without it a pooled key's 429 reached the client while the same
13
+ * refusal rotates on the adapter and chat-native wires. Distributed (round-robin) selection already
14
+ * cools the key on the send and deliberately answers without a replay. A synthesized replay refusal
15
+ * is not a provider rate limit and must not cool a key. A spent send budget is checked before any
16
+ * key is cooled, so it keeps the real 429 body. Returns undefined when nothing rotated.
17
+ */
18
+ export async function rotatePassthroughKeyOn429<T>(options: {
19
+ config: OcxConfig;
20
+ route: Pick<RouteResult, "provider" | "providerName">;
21
+ parsed: OcxParsedRequest;
22
+ response: Response;
23
+ adapterName: string;
24
+ budgetSpent: () => boolean;
25
+ refetch: () => Promise<T>;
26
+ }): Promise<T | undefined> {
27
+ const { config, route, parsed, response, adapterName, refetch } = options;
28
+ if (response.status !== 429 || isNonReplayableResponse(response) || !hasKeyPoolFailover(route.provider)
29
+ || route.provider._apiKeyAttempt?.distributed || options.budgetSpent()) return;
30
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
31
+ retryAfter: response.headers.get("retry-after"),
32
+ attemptedKey: route.provider.apiKey,
33
+ promptCacheKey: parsed.options.promptCacheKey,
34
+ });
35
+ if (!rotated) return;
36
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
37
+ route.provider = rotated;
38
+ bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: rotated, adapterName });
39
+ return refetch();
40
+ }
@@ -0,0 +1,21 @@
1
+ import { formatErrorResponse } from "../../bridge";
2
+ import { ReasoningArchiveBusyError, ReasoningArchiveError,
3
+ ReasoningArchiveQuotaError } from "../../responses/reasoning-retention";
4
+
5
+ /** Local storage refusal before any send. Never expose filesystem messages or causes. */
6
+ export function reasoningArchiveErrorResponse(error: unknown): Response | undefined {
7
+ let code: string;
8
+ let message: string;
9
+ if (error instanceof ReasoningArchiveQuotaError) {
10
+ code = "reasoning_archive_quota_exceeded";
11
+ message = "Local reasoning archive is full. Review and manually remove older reasoning archives, then retry the unchanged request.";
12
+ } else if (error instanceof ReasoningArchiveBusyError) {
13
+ code = "reasoning_archive_busy";
14
+ message = "Another local reasoning archive writer is active. Wait for it to finish, then retry the unchanged request.";
15
+ } else if (error instanceof ReasoningArchiveError) {
16
+ code = "reasoning_archive_unavailable";
17
+ message = "Local reasoning archive is unavailable. Check archive directory permissions and local storage health before retrying the unchanged request.";
18
+ } else return undefined;
19
+ // Match the local spend-ledger storage refusal's 503 convention without guessing a delay.
20
+ return formatErrorResponse(503, "server_error", message, { code, classified: true });
21
+ }
@@ -100,6 +100,7 @@ import type {
100
100
  import {
101
101
  codexRouteCredentialDomainHeaders,
102
102
  codexRouteCredentialOwnership,
103
+ mainHardLockFallbackCallerCheck,
103
104
  resolveResponsesCodexAuth,
104
105
  withClaudeNativeSession,
105
106
  } from "./core-auth";
@@ -364,7 +365,16 @@ export async function prepareResponsesRequest(
364
365
  // must run before comboIdFromRawBody reads `model`.
365
366
  if (!concreteSelection && !options.compactionRoutingOverride && !isCompactionTrigger && !options.internalDecisionCall
366
367
  && !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
367
- options.mainHardLockFallbackOverride = applyMainHardLockFallbackOverride(body, config);
368
+ try {
369
+ options.mainHardLockFallbackOverride = await applyMainHardLockFallbackOverride(
370
+ body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options),
371
+ );
372
+ } catch (error) {
373
+ if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
374
+ const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now() });
375
+ if (mapped) return mapped;
376
+ throw error;
377
+ }
368
378
  }
369
379
  const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
370
380
  // A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
@@ -71,7 +71,8 @@ import {
71
71
  resolveCurrentProviderApiKeyTransport,
72
72
  } from "../../providers/api-key-selection";
73
73
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
74
- import { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
74
+ import { codexWsExecutorBinding, physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
75
+ import { deepSeekImageFileBody, needsDeepSeekImageFiles } from "./deepseek-image-files";
75
76
  import type { ProviderFetchOptions } from "./fetch-helpers";
76
77
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
77
78
  import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
@@ -590,10 +591,62 @@ export async function prepareResponsesTransport(
590
591
  };
591
592
  }
592
593
  const build = resolved.buildRequest.bind(resolved);
594
+ const fileProvider = route.provider;
595
+ const fileProviderName = route.providerName;
596
+ // Persistence consent is configuration: capture its generation with the provider snapshot.
597
+ const fileConsentGeneration = captureConfigGeneration();
593
598
  resolved.buildRequest = async (requestParsed, incoming) => {
594
599
  const request = await build(requestParsed, incoming);
595
600
  // Capture at adapter creation, never from mutable serving state after an await.
596
601
  if (binding) requestBindings.set(request, binding);
602
+ const allowedImageFileModels = fileProvider.modelCapabilities
603
+ ? Object.entries(fileProvider.modelCapabilities)
604
+ .filter(([, cap]) => cap?.deepseekImageFiles === true)
605
+ .map(([model]) => model)
606
+ : [];
607
+ if (allowedImageFileModels.includes(request.outboundModelId ?? requestParsed.modelId) && needsDeepSeekImageFiles(request)) {
608
+ const consentModel = request.outboundModelId ?? requestParsed.modelId;
609
+ try {
610
+ // Ordinary uploads use provider pacing. A queued rebuild retains its enclosing
611
+ // fetch lease; reacquiring that same concurrency slot would deadlock at a limit of one.
612
+ const uploadFetch = providerFetch(fileProvider, options.codexWsRuntimeIdentity, {
613
+ httpOnly: true, providerName: fileProviderName, modelId: requestParsed.modelId,
614
+ beforeDispatch: () => {
615
+ if (!selectionIsCurrent(binding)) throw new Error("Credential selection changed before image upload");
616
+ // A queued upload must not outlive a revoked opt-in or a configuration reload.
617
+ if (captureConfigGeneration() !== fileConsentGeneration
618
+ || fileProvider.modelCapabilities?.[consentModel]?.deepseekImageFiles !== true) {
619
+ throw new Error("DeepSeek image file consent changed before image upload");
620
+ }
621
+ },
622
+ });
623
+ const uploadSignal = incoming.abortSignal ?? options.abortSignal ?? req.signal;
624
+ const pacedUpload: typeof fetch = incoming.dispatchPacingHeld
625
+ ? (async (input, init) => {
626
+ // The inference lease owns concurrency, but each physical upload still
627
+ // consumes an interval slot, including after a credential rebuild.
628
+ const slot = await waitForProviderRequestSlot(fileProviderName, fileProvider, requestParsed.modelId,
629
+ init?.signal ?? uploadSignal, { concurrency: false });
630
+ try { return await uploadFetch.unpacedFetch!(input, init); }
631
+ finally { releaseProviderRequestSlot(slot); }
632
+ }) as typeof fetch
633
+ : uploadFetch;
634
+ const body = await deepSeekImageFileBody(request, pacedUpload,
635
+ uploadSignal, {
636
+ allowedModels: allowedImageFileModels, budget: incoming.translatorBudget,
637
+ });
638
+ if (body !== undefined) {
639
+ request.releaseBodyObservation?.();
640
+ request.body = body;
641
+ const headers = new Headers(request.headers);
642
+ headers.delete("content-length");
643
+ request.headers = Object.fromEntries(headers);
644
+ request.releaseBodyObservation = incoming.translatorBudget.observeExternallyCapped(
645
+ "passthrough_serialization", Buffer.byteLength(body, "utf8"),
646
+ );
647
+ }
648
+ } catch (error) { request.releaseBodyObservation?.(); throw error; }
649
+ }
597
650
  return request;
598
651
  };
599
652
  if (resolved.runTurn) {
@@ -758,7 +811,8 @@ export async function prepareResponsesTransport(
758
811
  pacingChanged = false;
759
812
  }
760
813
  if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
761
- const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
814
+ const ws = codexWsExecutorBinding(execute);
815
+ const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? ws?.http ?? execute;
762
816
  const binding = requestBindings.get(wireRequest);
763
817
  const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
764
818
  ? binding.snapshot : undefined;
@@ -798,7 +852,10 @@ export async function prepareResponsesTransport(
798
852
  passiveQuotaWriterGeneration = writerGeneration;
799
853
  }
800
854
  commitKeyAttemptSend();
801
- response = await sendWithConnectionPolicy(
855
+ // The WS executor dials its own socket: HTTP connection policy and body coding do not apply.
856
+ // WS eligibility (egress, pacing, URL) was checked for the provider it was built for; a
857
+ // provider reselected above was not, so it takes the HTTP fallback under its own route.
858
+ response = ws?.provider === route.provider ? await execute(destination, dispatchInit) : await sendWithConnectionPolicy(
802
859
  distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
803
860
  destination,
804
861
  { ...dispatchInit, redirect: "manual",
@@ -843,6 +900,7 @@ export async function prepareResponsesTransport(
843
900
  adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
844
901
  const rebuilt = await nextAdapter.buildRequest(requestParsed, {
845
902
  headers: requestState.selectedForwardHeaders, providerName: route.providerName, translatorBudget,
903
+ dispatchPacingHeld: true,
846
904
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
847
905
  });
848
906
  const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
@@ -238,6 +238,11 @@ export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
238
238
  supervisionLatch?: SupervisionLatch;
239
239
  diagnose?: () => ServiceDiagnostic;
240
240
  managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
241
+ /**
242
+ * Task Scheduler only: ends surviving launcher/wrapper processes of THIS
243
+ * installation. Scoped to canonical script/launcher tokens, never a bare PID.
244
+ */
245
+ killServiceWrappers?: () => void;
241
246
  stopTrackedProxy?: () => Promise<unknown>;
242
247
  platform?: NodeJS.Platform;
243
248
  }
@@ -279,6 +284,18 @@ export async function prepareServiceInstall(
279
284
  if (!manager) throw new Error(`Background service manager is unavailable for ${backend}.`);
280
285
  if (manager.status() !== null) manager.stop();
281
286
  }
287
+ if (platform === "win32") {
288
+ // `schtasks /end` ends the task instance but leaves the cmd `:loop` wrapper
289
+ // alive to respawn the proxy mid-install — the same survivor
290
+ // `ocx stop` kills via killWindowsServiceWrapperProcesses. Run it
291
+ // after every manager has been asked to stop and before the tracked-proxy
292
+ // kill so a child respawned in that gap is still caught below. It also runs
293
+ // when no manager stop happened: a registration deleted mid-uninstall can
294
+ // leave the detached wrapper looping. A manager stop that refused propagates
295
+ // above before this runs, so the respawner of a still-running service is
296
+ // never killed.
297
+ (deps.killServiceWrappers ?? killWindowsServiceWrapperProcesses)();
298
+ }
282
299
  await (deps.stopTrackedProxy ?? stopTrackedProxyIfRunning)();
283
300
  }
284
301
 
@@ -288,6 +288,8 @@ export interface ModelCapabilities {
288
288
  /** Requested tier only; does not imply an upstream window or activate an unverified wire. */
289
289
  contextTier?: "default" | "long_context";
290
290
  video?: { processing?: "static" | "agentic" };
291
+ /** Opt in exact first-party DeepSeek models to one-hour image file persistence. */
292
+ deepseekImageFiles?: boolean;
291
293
  }
292
294
 
293
295
  export interface OcxProviderConfig {
@@ -322,8 +322,23 @@ export interface OcxDocumentContent {
322
322
  filename?: string;
323
323
  }
324
324
 
325
+ /**
326
+ * Inline audio carried by Codex's Responses input. Chat Completions accepts this as an
327
+ * `input_audio` content part only for WAV and MP3 payloads, so the normalized form keeps the
328
+ * base64 bytes and a wire-safe format label rather than the original data URL.
329
+ */
330
+ export interface OcxAudioContent {
331
+ type: "audio";
332
+ /** Base64 payload with no `data:` prefix. */
333
+ data: string;
334
+ /** Chat Completions input_audio supports WAV and MP3. */
335
+ format: "mp3" | "wav";
336
+ /** Text marker for wires that cannot carry the bytes. */
337
+ text: string;
338
+ }
339
+
325
340
  /** A user/developer message content part: text, native media, or an attached document. */
326
- export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent;
341
+ export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent | OcxAudioContent;
327
342
 
328
343
  export interface OcxThinkingContent {
329
344
  type: "thinking";
package/src/types.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  OcxToolResultMessage,
52
52
  OcxTextContent,
53
53
  OcxImageContent,
54
+ OcxAudioContent,
54
55
  OcxDocumentContent,
55
56
  OcxContentPart,
56
57
  OcxThinkingContent,
@@ -24,7 +24,7 @@ import type { WebSearchBackendId } from "./index";
24
24
  import { pacingHeaderDeadline } from "./header-deadline";
25
25
  import { redactSecretString } from "../lib/redact";
26
26
  import { readBoundedResponseBody } from "../lib/bounded-body";
27
- import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
27
+ import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, isSameTargetRetryHold, SendBudgetExhaustedError } from "../lib/upstream-retry";
28
28
  import { rateLimitRetryDelayMs } from "../providers/key-failover";
29
29
  import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
30
30
  import {
@@ -670,7 +670,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
670
670
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
671
671
  // rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
672
672
  while (
673
- prepared.response.status === 429
673
+ prepared.response.status === 429 && !isSameTargetRetryHold(prepared.response)
674
674
  && rateLimitRetryPolicy !== null
675
675
  && rateLimitRetries < rateLimitRetryPolicy.attempts
676
676
  ) {