@bitkyc08/opencodex 2.10.2 → 2.11.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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -171,14 +171,23 @@ import {
171
171
  imageGenToolCallAliases,
172
172
  restoreImageGenCallsInJson,
173
173
  } from "../responses-image-gen-repair";
174
- import { composeSsePayloadRewrites, relaySseWithPayloadRewrite } from "../sse-payload-rewrite";
174
+ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
175
175
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
176
176
 
177
177
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
178
178
  import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
179
179
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
180
180
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
181
- import { recordUpstreamHostFailure, resetUpstreamHostHealth, upstreamHostHealthKey } from "../../codex/upstream-host-health";
181
+ import {
182
+ acquireUpstreamHostAdmission,
183
+ disableUpstreamHostCircuitForKey,
184
+ normalizeUpstreamHostCircuitThreshold,
185
+ recordUpstreamHostFailure,
186
+ releaseUpstreamHostAdmission,
187
+ resetUpstreamHostHealth,
188
+ upstreamHostHealthKey,
189
+ type UpstreamHostAdmissionLease,
190
+ } from "../../codex/upstream-host-health";
182
191
  import {
183
192
  createResponsesSnapshotBlockRewrite,
184
193
  hasResponsesSnapshotRepair,
@@ -186,9 +195,11 @@ import {
186
195
  } from "../responses-snapshot-repair";
187
196
  import {
188
197
  composeSseBlockRewrites,
198
+ composeSsePayloadRewrites,
189
199
  payloadRewriteAsBlockRewrite,
190
200
  relaySseWithBlockRewrite,
191
201
  } from "../sse-payload-rewrite";
202
+ import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
192
203
  import { responsesJsonToSseBody } from "../responses-json-events";
193
204
  import { guardTerminalEventStream } from "./terminal-guard";
194
205
 
@@ -242,6 +253,30 @@ export function usesCodexForwardPoolAuth(
242
253
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
243
254
  }
244
255
 
256
+ export function preAuthUpstreamHostCircuitKey(
257
+ route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
258
+ config: OcxConfig,
259
+ options: { requireResponsesAdapter?: boolean } = {},
260
+ ): string | null {
261
+ if (
262
+ normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) === 0
263
+ || route.codexAccountMode !== "pool"
264
+ || route.codexAccountId !== undefined
265
+ || route.provider.authMode !== "forward"
266
+ || (options.requireResponsesAdapter !== false && route.provider.adapter !== "openai-responses")
267
+ ) return null;
268
+ return upstreamHostHealthKey(route.providerName, safeOriginLabel(route.provider.baseUrl ?? ""));
269
+ }
270
+
271
+ export function upstreamHostCircuitOpenResponse(retryAfterSeconds: number): Response {
272
+ return formatErrorResponse(
273
+ 503,
274
+ "upstream_host_circuit_open",
275
+ "Provider host is temporarily unavailable",
276
+ { retryAfter: String(retryAfterSeconds) },
277
+ );
278
+ }
279
+
245
280
  function normalizeCodexUnsupportedModelDetail(value: string): string {
246
281
  return value.trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
247
282
  }
@@ -456,7 +491,12 @@ async function retryCodexPoolOnAlternateAccount(
456
491
  route.provider.authMode === "forward",
457
492
  );
458
493
  // A real HTTP response proves the host was reached (#914).
459
- resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url)));
494
+ const retryHostKey = upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url));
495
+ if (normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0) {
496
+ resetUpstreamHostHealth(retryHostKey, null);
497
+ } else {
498
+ resetUpstreamHostHealth(retryHostKey);
499
+ }
460
500
  return {
461
501
  kind: "retried",
462
502
  authCtx: retryAuthCtx,
@@ -864,6 +904,12 @@ async function applyFinalRouteRequestNormalization(args: {
864
904
  }): Promise<void> {
865
905
  const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
866
906
 
907
+ // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
908
+ // their existing response.model contract even when their public and wire model ids differ.
909
+ const responseModelId = parsed.modelId;
910
+ const preserveAnthropicResponseModel = route.providerName === "anthropic"
911
+ || route.provider.adapter === "anthropic";
912
+
867
913
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
868
914
  if (route.modelId !== parsed.modelId) {
869
915
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
@@ -882,6 +928,7 @@ async function applyFinalRouteRequestNormalization(args: {
882
928
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
883
929
  // this request will actually use (#404).
884
930
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
931
+ if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
885
932
  logCtx.model = route.modelId;
886
933
  logCtx.provider = route.providerName;
887
934
  logCtx.providerAdapter = route.provider.adapter;
@@ -899,6 +946,10 @@ async function applyFinalRouteRequestNormalization(args: {
899
946
 
900
947
  // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
901
948
  applyOpenAiVirtualModel(parsed, route, logCtx);
949
+ if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
950
+ logCtx.resolvedModel = route.modelId;
951
+ logCtx.preserveResolvedModelFromRoute = true;
952
+ }
902
953
 
903
954
  // Fast mode override for OpenAI-routed models, only where the provider's Responses
904
955
  // route documents `service_tier` support (capability gate below strips everywhere else).
@@ -1305,6 +1356,9 @@ async function handleResponsesInner(
1305
1356
  logCtx: RequestLogContext,
1306
1357
  options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
1307
1358
  ): Promise<Response> {
1359
+ let pendingHostAdmissionLease: UpstreamHostAdmissionLease | null = null;
1360
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
1361
+ try {
1308
1362
  // The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
1309
1363
  // so an omitted value means a genuine Responses inbound.
1310
1364
  const inboundWire = options.inboundWire ?? "responses";
@@ -1439,7 +1493,6 @@ async function handleResponsesInner(
1439
1493
  nativeMainSelectionOnly: !nativeMainRecoveryBlocked
1440
1494
  && previewSelectionAdmission?.mainProfileDraining === true,
1441
1495
  };
1442
- let authCtx: CodexAuthContext = { kind: "main", accountId: null };
1443
1496
  let selectedForwardHeaders = req.headers;
1444
1497
  let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
1445
1498
  let subagentQuotaFailureModel = parsed.modelId;
@@ -1544,6 +1597,19 @@ async function handleResponsesInner(
1544
1597
  logCtx.provider = `${route.providerName}-${route.codexAccountNamespace}`;
1545
1598
  }
1546
1599
 
1600
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1601
+ const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
1602
+ if (preAuthHostKey) {
1603
+ const admission = acquireUpstreamHostAdmission(
1604
+ preAuthHostKey,
1605
+ config.upstreamHostCircuitThreshold,
1606
+ );
1607
+ if (admission.kind === "blocked") {
1608
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
1609
+ }
1610
+ pendingHostAdmissionLease = admission.lease;
1611
+ }
1612
+
1547
1613
  {
1548
1614
  const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
1549
1615
  if (!finalAuth.ok) return finalAuth.response;
@@ -1637,6 +1703,19 @@ async function handleResponsesInner(
1637
1703
  const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
1638
1704
  const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
1639
1705
  logCtx.providerAdapter = adapter.name;
1706
+ // Ordinary requests receive one durable attempt only after their final initial
1707
+ // adapter is resolved. Combo children own their attempt and retries keep it.
1708
+ if (!options.comboAttempt && !logCtx.activeAttempt) {
1709
+ const attempt = beginRequestAttempt(
1710
+ (logCtx.attempts?.length ?? 0) + 1,
1711
+ logCtx.provider,
1712
+ route.modelId,
1713
+ adapter.name,
1714
+ );
1715
+ logCtx.activeAttempt = attempt;
1716
+ logCtx.activeAttemptStartedAt = Date.now();
1717
+ (logCtx.attempts ??= []).push(attempt);
1718
+ }
1640
1719
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
1641
1720
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
1642
1721
 
@@ -1741,10 +1820,22 @@ async function handleResponsesInner(
1741
1820
  delete parsed._webSearch;
1742
1821
  delete parsed.options.toolChoice;
1743
1822
  delete parsed.options.parallelToolCalls;
1823
+ // The compaction turn is a plain prose summary; a surviving structured-output format
1824
+ // would force schema-constrained JSON into the synthetic compaction item. The flag and
1825
+ // the raw `text` controls go too: Kiro's capability guard reads both and would reject
1826
+ // the turn outright, and the key-mode openai-responses adapter builds from _rawBody.
1827
+ delete parsed.options.textFormat;
1828
+ delete parsed._structuredOutput;
1829
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1830
+ delete (parsed._rawBody as Record<string, unknown>).text;
1831
+ }
1744
1832
  parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
1745
1833
  }
1746
1834
 
1747
1835
  if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
1836
+ let hostAdmissionLease = pendingHostAdmissionLease;
1837
+ pendingHostAdmissionLease = null;
1838
+ try {
1748
1839
  const imageGenCallAliases = route.provider.authMode === "forward"
1749
1840
  ? new Map<string, { namespace: string; name: string }>()
1750
1841
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
@@ -1769,8 +1860,52 @@ async function handleResponsesInner(
1769
1860
  + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
1770
1861
  );
1771
1862
  }
1772
- let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1863
+ let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
1864
+ try {
1865
+ request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1866
+ } catch (error) {
1867
+ releaseCodexAuthContextProbeLease(authCtx);
1868
+ throw error;
1869
+ }
1773
1870
  recordAdapterReasoning(logCtx, request);
1871
+ const actualHostKey = upstreamHostHealthKey(
1872
+ route.providerName,
1873
+ safeOriginLabel(request.url),
1874
+ );
1875
+ const hostKey = route.provider.authMode === "forward"
1876
+ ? actualHostKey
1877
+ : null;
1878
+ const hostCircuitEnabled = hostKey !== null
1879
+ && normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
1880
+ if (hostKey !== null && !hostCircuitEnabled) {
1881
+ disableUpstreamHostCircuitForKey(actualHostKey);
1882
+ }
1883
+ if (hostAdmissionLease && hostAdmissionLease.key !== hostKey) {
1884
+ return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
1885
+ }
1886
+ if (options.abortSignal?.aborted) {
1887
+ releaseCodexAuthContextProbeLease(authCtx);
1888
+ return clientCancelledResponse();
1889
+ }
1890
+ if (!hostAdmissionLease && hostCircuitEnabled) {
1891
+ const admission = acquireUpstreamHostAdmission(
1892
+ hostKey!,
1893
+ config.upstreamHostCircuitThreshold,
1894
+ );
1895
+ if (admission.kind === "blocked") {
1896
+ releaseCodexAuthContextProbeLease(authCtx);
1897
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
1898
+ }
1899
+ hostAdmissionLease = admission.lease;
1900
+ }
1901
+ const settleObservedHostResponse = (): void => {
1902
+ if (hostCircuitEnabled) {
1903
+ resetUpstreamHostHealth(actualHostKey, hostAdmissionLease);
1904
+ } else {
1905
+ resetUpstreamHostHealth(actualHostKey);
1906
+ }
1907
+ hostAdmissionLease = null;
1908
+ };
1774
1909
  const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1775
1910
  ? request.usageLog.inputTokens
1776
1911
  : undefined;
@@ -1786,16 +1921,30 @@ async function handleResponsesInner(
1786
1921
  let upstreamResponse: Response;
1787
1922
  const transportFailureResponse = (err: unknown): Response => {
1788
1923
  upstream.abort();
1789
- if (options.abortSignal?.aborted) return clientCancelledResponse();
1924
+ if (options.abortSignal?.aborted) {
1925
+ releaseUpstreamHostAdmission(hostAdmissionLease);
1926
+ hostAdmissionLease = null;
1927
+ releaseCodexAuthContextProbeLease(authCtx);
1928
+ return clientCancelledResponse();
1929
+ }
1790
1930
  const outcome = classifyTransportFailureKind(err);
1791
1931
  // Host-level evidence stands regardless of pool membership: a direct
1792
1932
  // forward send has no pool accounting, but the reachability failure is
1793
1933
  // still host-wide, not account evidence (#914 review).
1794
1934
  if (outcome === "connect_neutral") {
1795
- recordUpstreamHostFailure(
1796
- upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url)),
1797
- { code: transportErrorCode(err) },
1798
- );
1935
+ if (hostCircuitEnabled) {
1936
+ recordUpstreamHostFailure(actualHostKey, {
1937
+ code: transportErrorCode(err),
1938
+ threshold: config.upstreamHostCircuitThreshold,
1939
+ lease: hostAdmissionLease,
1940
+ });
1941
+ } else {
1942
+ recordUpstreamHostFailure(actualHostKey, { code: transportErrorCode(err) });
1943
+ }
1944
+ hostAdmissionLease = null;
1945
+ } else {
1946
+ releaseUpstreamHostAdmission(hostAdmissionLease);
1947
+ hostAdmissionLease = null;
1799
1948
  }
1800
1949
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1801
1950
  recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
@@ -1828,7 +1977,7 @@ async function handleResponsesInner(
1828
1977
  // Every real attempt response — including an intermediate 5xx the
1829
1978
  // retry wrapper replaces — proves the host was reached (#914 review).
1830
1979
  .then(res => {
1831
- resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url)));
1980
+ settleObservedHostResponse();
1832
1981
  return res;
1833
1982
  });
1834
1983
  },
@@ -1887,7 +2036,7 @@ async function handleResponsesInner(
1887
2036
  }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider),
1888
2037
  route.provider.authMode === "forward")
1889
2038
  .then(res => {
1890
- resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url)));
2039
+ settleObservedHostResponse();
1891
2040
  return res;
1892
2041
  });
1893
2042
  },
@@ -2052,13 +2201,18 @@ async function handleResponsesInner(
2052
2201
  if (isEventStream && upstreamResponse.body) {
2053
2202
  const repairConfig = route.provider.responsesItemIdRepair;
2054
2203
  const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
2055
- const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig) || snapshotRepairEnabled;
2204
+ const githubCopilotRepairEnabled = route.providerName === "github-copilot";
2205
+ const responseModelRewrite = parsed._responseModelId !== undefined
2206
+ && parsed._responseModelId !== parsed.modelId
2207
+ ? createResponsesModelPayloadRewrite(parsed._responseModelId)
2208
+ : undefined;
2056
2209
  // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
2057
2210
  const payloadRewrites = [
2058
2211
  createImageGenCallRestoreRewrite(imageGenCallAliases),
2059
2212
  hasResponsesItemIdRepair(repairConfig)
2060
2213
  ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
2061
2214
  : undefined,
2215
+ responseModelRewrite,
2062
2216
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
2063
2217
  // #893: sparse-snapshot gateways get field backfills AND lifecycle event
2064
2218
  // injection at the block level, after payload rewrites. Defaults come
@@ -2071,12 +2225,21 @@ async function handleResponsesInner(
2071
2225
  return undefined;
2072
2226
  }
2073
2227
  })();
2074
- const clientBlockRewrite = snapshotRepairEnabled
2075
- ? composeSseBlockRewrites(
2076
- payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites)),
2077
- createResponsesSnapshotBlockRewrite(snapshotDefaultsRequest, translatorBudget),
2078
- )
2228
+ const blockRewrites = [
2229
+ payloadRewrites.length > 0
2230
+ ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
2231
+ : undefined,
2232
+ githubCopilotRepairEnabled
2233
+ ? createGithubCopilotResponsesBlockRewrite(translatorBudget)
2234
+ : undefined,
2235
+ snapshotRepairEnabled
2236
+ ? createResponsesSnapshotBlockRewrite(snapshotDefaultsRequest, translatorBudget)
2237
+ : undefined,
2238
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
2239
+ const clientBlockRewrite = blockRewrites.length > 0
2240
+ ? composeSseBlockRewrites(...blockRewrites)
2079
2241
  : undefined;
2242
+ const needsClientRewrite = clientBlockRewrite !== undefined;
2080
2243
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
2081
2244
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
2082
2245
  // lost). The eager single reader applies the same rewrites inline.
@@ -2086,6 +2249,8 @@ async function handleResponsesInner(
2086
2249
  needsClientRewrite,
2087
2250
  config.streamMode ?? "auto",
2088
2251
  );
2252
+ const inlineEagerRewrite = needsClientRewrite
2253
+ && (win32EagerRewrite || eagerPath?.useEagerRelay === true);
2089
2254
  if (eagerPath?.useEagerRelay || win32EagerRewrite) {
2090
2255
  const turnAc = new AbortController();
2091
2256
  linkAbortSignal(upstream, turnAc.signal);
@@ -2117,6 +2282,7 @@ async function handleResponsesInner(
2117
2282
  logCtx,
2118
2283
  onCompletedResponse: rememberPassthroughResponse,
2119
2284
  onFirstOutput: options.onFirstOutput,
2285
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
2120
2286
  });
2121
2287
  const eagerBody = relaySseEagerBounded(upstreamResponse.body, turnAc, {
2122
2288
  inspectChunk: chunk => inspector.feed(chunk),
@@ -2125,9 +2291,6 @@ async function handleResponsesInner(
2125
2291
  // Stream lifetime follows the protocol terminal even when this request
2126
2292
  // has no outcome callback configured (reported() would stay false).
2127
2293
  sawTerminal: () => inspector.terminalSeen(),
2128
- ...(win32EagerRewrite
2129
- ? { rewritePayload: composeSsePayloadRewrites(...payloadRewrites) }
2130
- : {}),
2131
2294
  ...(clientBlockRewrite
2132
2295
  ? { rewriteBlocks: clientBlockRewrite }
2133
2296
  : {}),
@@ -2144,11 +2307,11 @@ async function handleResponsesInner(
2144
2307
  },
2145
2308
  onClientCancel: () => options.onNativePassthroughCancel?.(),
2146
2309
  onDone: () => unregisterTurn(turnAc),
2147
- }, win32EagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
2310
+ }, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
2148
2311
  // When selected, this relay closes response.completed even if upstream
2149
- // keeps the connection alive. Windows rewrite traffic applies its
2150
- // payload transform inline never via the Bun#32111-unsafe
2151
- // tee()+JS-pull chain (#864).
2312
+ // keeps the connection alive. Windows forced-rewrite traffic and Darwin
2313
+ // explicit eager traffic apply client rewrites inline rather than via
2314
+ // the tee()+JS-pull chain.
2152
2315
  if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
2153
2316
  return markEagerRelaySseResponse(
2154
2317
  markNativePassthroughSseResponse(new Response(eagerBody, {
@@ -2166,6 +2329,7 @@ async function handleResponsesInner(
2166
2329
  clientGoneSignal: clientGone.signal,
2167
2330
  drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
2168
2331
  upstream,
2332
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
2169
2333
  };
2170
2334
  if (recordTerminalOutcomes) {
2171
2335
  // A real terminal was parsed from the (teed) inspection stream — record it as the outcome
@@ -2218,8 +2382,8 @@ async function handleResponsesInner(
2218
2382
  // Windows was handled by the eager terminal-aware branch above. Remaining
2219
2383
  // tee traffic can use the JS relay to close on a protocol terminal and to
2220
2384
  // convert a mid-stream reset into a clean response.failed event.
2221
- const rewrittenBody = clientBlockRewrite !== undefined || payloadRewrites.length > 0
2222
- ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite ?? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites)), translatorBudget)
2385
+ const rewrittenBody = clientBlockRewrite !== undefined
2386
+ ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
2223
2387
  : nativeBody;
2224
2388
  const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
2225
2389
  return markNativePassthroughSseResponse(new Response(clientBody, {
@@ -2250,14 +2414,19 @@ async function handleResponsesInner(
2250
2414
  }
2251
2415
  const clientJson = (() => {
2252
2416
  const restored = restoreImageGenCallsInJson(text, imageGenCallAliases);
2253
- if (!hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)) return restored;
2254
- let outbound: unknown;
2255
- try {
2256
- outbound = JSON.parse(request.body);
2257
- } catch {
2258
- outbound = undefined;
2259
- }
2260
- return repairResponsesSnapshotJson(restored, outbound);
2417
+ const repaired = (() => {
2418
+ if (!hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)) return restored;
2419
+ let outbound: unknown;
2420
+ try {
2421
+ outbound = JSON.parse(request.body);
2422
+ } catch {
2423
+ outbound = undefined;
2424
+ }
2425
+ return repairResponsesSnapshotJson(restored, outbound);
2426
+ })();
2427
+ return parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
2428
+ ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
2429
+ : repaired;
2261
2430
  })();
2262
2431
  // #875: the transport-neutral reliability policy forced a bounded JSON
2263
2432
  // upstream for a client that asked for SSE. Reframe the completed JSON
@@ -2319,6 +2488,12 @@ async function handleResponsesInner(
2319
2488
  status: upstreamResponse.status,
2320
2489
  headers,
2321
2490
  });
2491
+ } finally {
2492
+ if (hostAdmissionLease) {
2493
+ releaseUpstreamHostAdmission(hostAdmissionLease);
2494
+ releaseCodexAuthContextProbeLease(authCtx);
2495
+ }
2496
+ }
2322
2497
  }
2323
2498
 
2324
2499
  // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
@@ -2560,7 +2735,7 @@ async function handleResponsesInner(
2560
2735
  eventSource = preflight.stream;
2561
2736
  }
2562
2737
  const sseStream = bridgeToResponsesSSE(
2563
- eventSource, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2738
+ eventSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2564
2739
  () => {
2565
2740
  runTurnAbort.abort();
2566
2741
  queue.close();
@@ -2612,7 +2787,7 @@ async function handleResponsesInner(
2612
2787
  }
2613
2788
  }
2614
2789
  let providerState: OcxProviderContinuationState | undefined;
2615
- const json = buildResponseJSON(events, parsed.modelId, {
2790
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
2616
2791
  translatorBudget,
2617
2792
  replayCacheScope: parsed._clientThreadId ?? "global",
2618
2793
  hideThinkingSummary: parsed.options.hideThinkingSummary,
@@ -3254,7 +3429,7 @@ async function handleResponsesInner(
3254
3429
  : initialEventStream;
3255
3430
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3256
3431
  const sseStream = bridgeToResponsesSSE(
3257
- eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
3432
+ eventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
3258
3433
  () => upstream.abort(), 2_000,
3259
3434
  {
3260
3435
  translatorBudget,
@@ -3314,7 +3489,7 @@ async function handleResponsesInner(
3314
3489
  }
3315
3490
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3316
3491
  let providerState: OcxProviderContinuationState | undefined;
3317
- const json = buildResponseJSON(events, parsed.modelId, {
3492
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
3318
3493
  translatorBudget,
3319
3494
  replayCacheScope: parsed._clientThreadId ?? "global",
3320
3495
  hideThinkingSummary: parsed.options.hideThinkingSummary,
@@ -3344,6 +3519,12 @@ async function handleResponsesInner(
3344
3519
  }
3345
3520
 
3346
3521
  return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
3522
+ } finally {
3523
+ if (pendingHostAdmissionLease) {
3524
+ releaseUpstreamHostAdmission(pendingHostAdmissionLease);
3525
+ releaseCodexAuthContextProbeLease(authCtx);
3526
+ }
3527
+ }
3347
3528
  }
3348
3529
 
3349
3530
 
@@ -10,6 +10,8 @@ interface ResponsesItemIdRepairState {
10
10
  readonly repairInvalidIds: boolean;
11
11
  readonly placeholders: Record<RepairableItemType, ReadonlySet<string>>;
12
12
  readonly outputIds: Record<RepairableItemType, Map<number, string>>;
13
+ /** JSON [outputIndex, rawId] -> canonical id, for exact item_id rewrites on part/delta events. */
14
+ readonly rawIds: Map<string, string>;
13
15
  readonly scope: string;
14
16
  readonly budget?: TranslatorBudget;
15
17
  }
@@ -63,6 +65,7 @@ function createRepairState(config: ResponsesItemIdRepairConfig, budget?: Transla
63
65
  message: new Map<number, string>(),
64
66
  reasoning: new Map<number, string>(),
65
67
  },
68
+ rawIds: new Map<string, string>(),
66
69
  scope: randomUUID().replace(/-/g, ""),
67
70
  budget,
68
71
  };
@@ -97,6 +100,9 @@ function rememberMappedId(
97
100
  if (!mapped) return null;
98
101
  state.budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([outputIndex, rawId, mapped])).byteLength, { kind: "item_ids" });
99
102
  state.outputIds[type].set(outputIndex, mapped);
103
+ // Keyed by (index, rawId): an upstream that reuses one placeholder id across
104
+ // several items must not collapse them into the last item's canonical id.
105
+ if (rawId !== mapped) state.rawIds.set(JSON.stringify([outputIndex, rawId]), mapped);
100
106
  return mapped;
101
107
  }
102
108
 
@@ -120,11 +126,24 @@ function rewriteItemIdField(
120
126
  ): { event: Record<string, unknown>; changed: boolean } {
121
127
  const eventType = typeof event.type === "string" ? ITEM_ID_EVENT_TYPES[event.type] : undefined;
122
128
  if (!eventType) return { event, changed: false };
129
+ const currentId = typeof event.item_id === "string" ? event.item_id : undefined;
130
+ // content_part.* events are shared between message and reasoning items (DeepSeek's
131
+ // streamed reasoning wraps its text in content parts), so the static event-type map
132
+ // can point at the wrong id table. The rewrite is therefore exact-only when the
133
+ // event carries an item_id: it fires when (output_index, item_id) names an id the
134
+ // item stream already repaired, and otherwise leaves the event alone — an unknown
135
+ // id belongs to an item this repair never touched (function_call, already-canonical
136
+ // ids), and guessing by index could borrow a sibling item's identity. The index
137
+ // table serves only events with NO item_id, where repairMissingTerminalIds
138
+ // explicitly opts into the positional guess (the pre-existing contract).
139
+ if (currentId !== undefined) {
140
+ const mapped = state.rawIds.get(JSON.stringify([outputIndex, currentId]));
141
+ if (!mapped || currentId === mapped) return { event, changed: false };
142
+ return { event: { ...event, item_id: mapped }, changed: true };
143
+ }
144
+ if (!state.repairMissingTerminalIds) return { event, changed: false };
123
145
  const mapped = state.outputIds[eventType].get(outputIndex);
124
146
  if (!mapped) return { event, changed: false };
125
- const currentId = typeof event.item_id === "string" ? event.item_id : undefined;
126
- if (currentId === mapped) return { event, changed: false };
127
- if (currentId === undefined && !state.repairMissingTerminalIds) return { event, changed: false };
128
147
  return { event: { ...event, item_id: mapped }, changed: true };
129
148
  }
130
149
 
@@ -0,0 +1,29 @@
1
+ import type { SsePayloadRewrite } from "./sse-payload-rewrite";
2
+
3
+ function rewriteResponseObjectModel(value: unknown, responseModelId: string): boolean {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5
+ const response = value as Record<string, unknown>;
6
+ if (typeof response.model !== "string" || response.model === responseModelId) return false;
7
+ response.model = responseModelId;
8
+ return true;
9
+ }
10
+
11
+ /** Rewrite only existing Responses model metadata; unrelated and malformed payloads stay byte-identical. */
12
+ export function rewriteResponsesModelJson(json: string, responseModelId: string): string {
13
+ let parsed: unknown;
14
+ try {
15
+ parsed = JSON.parse(json);
16
+ } catch {
17
+ return json;
18
+ }
19
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return json;
20
+ const record = parsed as Record<string, unknown>;
21
+ const rootChanged = rewriteResponseObjectModel(record, responseModelId);
22
+ const nestedChanged = rewriteResponseObjectModel(record.response, responseModelId);
23
+ const changed = rootChanged || nestedChanged;
24
+ return changed ? JSON.stringify(record) : json;
25
+ }
26
+
27
+ export function createResponsesModelPayloadRewrite(responseModelId: string): SsePayloadRewrite {
28
+ return payload => rewriteResponsesModelJson(payload, responseModelId);
29
+ }