@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.
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2075
|
-
|
|
2076
|
-
payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
2077
|
-
|
|
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
|
-
},
|
|
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
|
|
2150
|
-
//
|
|
2151
|
-
// tee()+JS-pull chain
|
|
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
|
|
2222
|
-
? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite
|
|
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
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
+
}
|