@bitkyc08/opencodex 2.58.0 → 2.60.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 +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resolveCodexModelEntitlements,
|
|
33
33
|
invalidateCodexModelEntitlementsForAccount,
|
|
34
34
|
entitledCodexAccountIdsForModel,
|
|
35
|
+
recordCodexModelDenialEvidence,
|
|
35
36
|
} from "../../codex/model-entitlements";
|
|
36
37
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
37
38
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
@@ -161,43 +162,93 @@ export function normalizeCodexUnsupportedModelDetail(value: string): string {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The model id an authenticated Codex refusal names, or `undefined` when the body is not that
|
|
167
|
+
* refusal.
|
|
168
|
+
*
|
|
169
|
+
* Extracted rather than string-compared so the caller can learn WHICH model was refused. The
|
|
170
|
+
* route and the wire can legitimately disagree: `applyCodexAccountGatedWireNormalization`
|
|
171
|
+
* rewrites `gpt-daybreak-blue-latest` to `gpt-5.6-sol` before dispatch, so upstream names the
|
|
172
|
+
* model it was actually sent. Building the expected sentence from `route.modelId` alone made
|
|
173
|
+
* that comparison fail for the one model that is still account-gated, which silently disabled
|
|
174
|
+
* both the alternate-account retry and the same-account ladder built for exactly that case.
|
|
175
|
+
*
|
|
176
|
+
* The envelope is unchanged and stays exact: a top-level `detail` string, whitespace-collapsed
|
|
177
|
+
* and case-folded, matching the whole sentence with nothing before or after it. No prose is
|
|
178
|
+
* inferred and no other 400 shape is admitted, because a 400 is also what a malformed request
|
|
179
|
+
* earns and that must never read as an entitlement fact.
|
|
180
|
+
*/
|
|
181
|
+
export function codexUnsupportedModelFromDetail(
|
|
165
182
|
status: number,
|
|
166
183
|
bodyText: string,
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
if (status !== 400) return false;
|
|
184
|
+
): string | undefined {
|
|
185
|
+
if (status !== 400) return undefined;
|
|
170
186
|
try {
|
|
171
187
|
const payload = JSON.parse(bodyText) as unknown;
|
|
172
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return
|
|
188
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
173
189
|
const detail = (payload as { detail?: unknown }).detail;
|
|
174
|
-
if (typeof detail !== "string") return
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
if (typeof detail !== "string") return undefined;
|
|
191
|
+
const matched = /^the '([^']{1,256})' model is not supported when using codex with a chatgpt account\.$/u
|
|
192
|
+
.exec(normalizeCodexUnsupportedModelDetail(detail));
|
|
193
|
+
return matched?.[1];
|
|
178
194
|
} catch {
|
|
179
|
-
return
|
|
195
|
+
return undefined;
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
|
|
184
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The refused model id when this response is the exact unsupported-model refusal for this
|
|
202
|
+
* request, read from a bounded clone.
|
|
203
|
+
*
|
|
204
|
+
* Same admission rules {@link shouldRetryCodexPoolAccountModel400} always applied, which is now
|
|
205
|
+
* a predicate over this: a truncated or non-display-safe body proves nothing and is refused.
|
|
206
|
+
* Returning the id lets the caller record the denial against the model upstream actually named.
|
|
207
|
+
*/
|
|
208
|
+
export async function codexPoolAccountModel400Denial(
|
|
185
209
|
response: Response,
|
|
186
210
|
modelId: string,
|
|
187
211
|
signal?: AbortSignal,
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
wireModelId?: string,
|
|
213
|
+
): Promise<string | undefined> {
|
|
214
|
+
if (response.status !== 400) return undefined;
|
|
190
215
|
try {
|
|
191
216
|
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
if (!body.displaySafe || body.truncated) return undefined;
|
|
218
|
+
return isAllowListedCodexAccountModel400(response.status, body.text, modelId, wireModelId)
|
|
219
|
+
? codexUnsupportedModelFromDetail(response.status, body.text)
|
|
220
|
+
: undefined;
|
|
195
221
|
} catch {
|
|
196
|
-
return
|
|
222
|
+
return undefined;
|
|
197
223
|
}
|
|
198
224
|
}
|
|
199
225
|
|
|
200
226
|
|
|
227
|
+
export function isAllowListedCodexAccountModel400(
|
|
228
|
+
status: number,
|
|
229
|
+
bodyText: string,
|
|
230
|
+
modelId: string,
|
|
231
|
+
wireModelId?: string,
|
|
232
|
+
): boolean {
|
|
233
|
+
const refused = codexUnsupportedModelFromDetail(status, bodyText);
|
|
234
|
+
if (refused === undefined) return false;
|
|
235
|
+
return [modelId, wireModelId].some(candidate => (
|
|
236
|
+
candidate !== undefined
|
|
237
|
+
&& refused === normalizeCodexUnsupportedModelDetail(candidate)
|
|
238
|
+
));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
export async function shouldRetryCodexPoolAccountModel400(
|
|
243
|
+
response: Response,
|
|
244
|
+
modelId: string,
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
wireModelId?: string,
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
return await codexPoolAccountModel400Denial(response, modelId, signal, wireModelId) !== undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
201
252
|
/** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
|
|
202
253
|
export function codexQuotaFailureMessage(body: string): string | undefined {
|
|
203
254
|
try {
|
|
@@ -277,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
277
328
|
/** Sanitized caller input, before any selected Pool credential was materialized. */
|
|
278
329
|
callerAuthHeaders: Headers;
|
|
279
330
|
config: OcxConfig;
|
|
280
|
-
|
|
331
|
+
/** Actual routed result narrowed to the fields this retry consumes. */
|
|
332
|
+
route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
|
|
281
333
|
parsed: OcxParsedRequest;
|
|
282
334
|
logCtx: RequestLogContext;
|
|
283
335
|
options: {
|
|
@@ -630,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
630
682
|
"pool",
|
|
631
683
|
);
|
|
632
684
|
const retryAdapter = resolveAdapter(
|
|
633
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
|
|
685
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
|
|
634
686
|
config.cacheRetention,
|
|
635
687
|
route.providerName,
|
|
636
688
|
);
|
|
@@ -778,15 +830,29 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
778
830
|
}
|
|
779
831
|
retrySendCount += 1;
|
|
780
832
|
args.onResponse?.(upstreamResponse, retryAuthCtx, request);
|
|
833
|
+
// The alternate account can refuse the same model, and that refusal is evidence about the
|
|
834
|
+
// account that produced it. Read BEFORE the ladder's own break, so the ordinary
|
|
835
|
+
// single-retry path -- every flagship model, which is the #4906 case -- records it too
|
|
836
|
+
// rather than only the gated ladder below. Without this the pool learns nothing from a
|
|
837
|
+
// refusal and the next request repeats the same selection.
|
|
838
|
+
const retryModelDenial = await codexPoolAccountModel400Denial(
|
|
839
|
+
upstreamResponse,
|
|
840
|
+
route.modelId,
|
|
841
|
+
options.abortSignal,
|
|
842
|
+
parsed.modelId,
|
|
843
|
+
);
|
|
844
|
+
if (retryModelDenial !== undefined) {
|
|
845
|
+
recordCodexModelDenialEvidence(
|
|
846
|
+
retryAuthCtx.accountId,
|
|
847
|
+
retryModelDenial,
|
|
848
|
+
retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
781
851
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
782
852
|
// Caller-owned main is an alternate-account replay and can never enter the bounded
|
|
783
853
|
// same-stored-account 400 loop above. Keep that invariant explicit for the account-id reads.
|
|
784
854
|
if (retryAuthCtx.kind === "main") break;
|
|
785
|
-
if (
|
|
786
|
-
upstreamResponse,
|
|
787
|
-
route.modelId,
|
|
788
|
-
options.abortSignal,
|
|
789
|
-
)) break;
|
|
855
|
+
if (retryModelDenial === undefined) break;
|
|
790
856
|
invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
|
|
791
857
|
let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
|
|
792
858
|
try {
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import { formatErrorResponse } from "../../bridge";
|
|
30
30
|
import {
|
|
31
31
|
expandPreviousResponseInput,
|
|
32
|
-
previousResponseScopeMismatch,
|
|
33
32
|
previousResponseReplayFailure,
|
|
34
33
|
previousResponseProviderState,
|
|
35
34
|
} from "../../responses/state";
|
|
@@ -201,11 +200,12 @@ export async function executeComboResponses(
|
|
|
201
200
|
// imageInput is disabled (and so targets see the full replayed input).
|
|
202
201
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
203
202
|
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
206
|
-
console.warn("[opencodex]
|
|
203
|
+
const replayFailure = previousResponseReplayFailure(body);
|
|
204
|
+
if (replayFailure?.reason === "scope_mismatch") {
|
|
205
|
+
console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
|
|
207
206
|
}
|
|
208
|
-
|
|
207
|
+
// Local replay failures require full client replay.
|
|
208
|
+
if (replayFailure) {
|
|
209
209
|
return formatErrorResponse(
|
|
210
210
|
400,
|
|
211
211
|
"previous_response_not_found",
|
|
@@ -235,7 +235,7 @@ export async function executeComboResponses(
|
|
|
235
235
|
sourceBody: body,
|
|
236
236
|
previousResponseInputExpanded: body !== rawBody
|
|
237
237
|
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
|
|
238
|
-
providerContinuation:
|
|
238
|
+
providerContinuation: body !== rawBody && requestedPreviousId
|
|
239
239
|
? previousResponseProviderState(requestedPreviousId)
|
|
240
240
|
: undefined,
|
|
241
241
|
recoveredPlaintext: false,
|
|
@@ -521,7 +521,7 @@ export async function executeComboResponses(
|
|
|
521
521
|
// The live config can change while the child is streaming. Never retain credentials.
|
|
522
522
|
const currentCombo = getCombo(config, comboId);
|
|
523
523
|
const provider = config.providers[completedTarget.provider];
|
|
524
|
-
if (Object.hasOwn(config.providers, completedTarget.provider)
|
|
524
|
+
if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
|
|
525
525
|
&& provider && provider.disabled !== true
|
|
526
526
|
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
527
527
|
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|
|
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
13
13
|
import type { RequestLogContext } from "../request-log";
|
|
14
14
|
import type { HandleResponsesOptions } from "./core-options";
|
|
15
15
|
import { prepareEffortNormalization } from "../effort-policy";
|
|
16
|
-
import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
|
|
17
16
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
18
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
19
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
|
|
|
80
79
|
route.modelId,
|
|
81
80
|
provider,
|
|
82
81
|
inboundWire,
|
|
82
|
+
route.staticPolicy,
|
|
83
83
|
).adapter === "openai-responses";
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
126
126
|
const responseModelId = parsed.modelId;
|
|
127
127
|
const preserveAnthropicResponseModel = route.providerName === "anthropic"
|
|
128
128
|
|| route.provider.adapter === "anthropic";
|
|
129
|
+
const finalSelectedModelId = route.modelId;
|
|
130
|
+
const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
|
|
129
131
|
|
|
130
132
|
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
|
|
131
133
|
if (route.modelId !== parsed.modelId) {
|
|
@@ -136,17 +138,21 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
136
138
|
}
|
|
137
139
|
// Transport-neutral reliability policy (#875): applies to any Responses
|
|
138
140
|
// upstream whose final adapter is openai-responses, not only WS turns.
|
|
139
|
-
const responsesUpstreamStreaming =
|
|
141
|
+
const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
|
|
142
|
+
|
|
143
|
+
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
144
|
+
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
145
|
+
const routedProvider = route.provider;
|
|
146
|
+
const wireProvider = resolveWireProtocolOverride(
|
|
140
147
|
route.providerName,
|
|
141
|
-
route.provider,
|
|
142
148
|
route.modelId,
|
|
149
|
+
routedProvider,
|
|
150
|
+
inboundWire,
|
|
151
|
+
route.staticPolicy,
|
|
143
152
|
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
148
|
-
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req));
|
|
149
|
-
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
153
|
+
route.provider = resolveOpenCodeGoTransport(wireProvider,
|
|
154
|
+
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
|
|
155
|
+
routedProvider);
|
|
150
156
|
parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
|
|
151
157
|
enabled: config.plaintextV2AgentMessages === true,
|
|
152
158
|
inboundWire,
|
|
@@ -161,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
161
167
|
|| (!summary && route.provider.showThinkingSummary !== true);
|
|
162
168
|
}
|
|
163
169
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
164
|
-
logCtx.model = route.modelId;
|
|
170
|
+
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
165
171
|
logCtx.provider = route.providerName;
|
|
166
172
|
logCtx.providerAdapter = route.provider.adapter;
|
|
167
173
|
logCtx.routeDecision = route.routeDecision;
|
|
@@ -187,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
187
193
|
(parsed._rawBody as Record<string, unknown>).store = false;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
191
|
-
const finalSelectedModelId = route.modelId;
|
|
192
|
-
|
|
193
|
-
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
194
|
-
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
195
196
|
if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
|
|
196
197
|
logCtx.resolvedModel = route.modelId;
|
|
197
198
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
|
|
|
88
88
|
try {
|
|
89
89
|
const payload = JSON.parse(bodyText) as unknown;
|
|
90
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
91
|
+
if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
91
92
|
const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
|
|
92
93
|
if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
93
94
|
if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
|
|
|
11
11
|
import type { InboundWire } from "../../providers/registry";
|
|
12
12
|
import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
13
13
|
import type { CallerDirectAuth } from "../../providers/caller-authorization";
|
|
14
|
+
import type { CompactionRoutingOverride } from "./compaction-routing";
|
|
14
15
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
15
16
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
16
17
|
import type { RequestLogContext } from "../request-log";
|
|
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
|
|
|
51
52
|
admission?: DataPlaneAdmission;
|
|
52
53
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
53
54
|
onRequestBodyRead?: () => void;
|
|
55
|
+
/** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
|
|
56
|
+
onRequestBodyParsed?: (body: unknown) => void;
|
|
54
57
|
forceEmptyResponseId?: boolean;
|
|
55
58
|
/** Internal, connection-owned control channel; never reconstructed from headers. */
|
|
56
59
|
nativeControl?: NativeResponseControl;
|
|
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
|
|
|
105
108
|
callerDirectAuth?: CallerDirectAuth | null;
|
|
106
109
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
107
110
|
comboAttempt?: boolean;
|
|
111
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
108
112
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
109
113
|
comboReplaySnapshot?: {
|
|
110
114
|
sourceBody: unknown;
|
|
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
export function looksLikeBackendCiphertext(payload: string): boolean {
|
|
101
|
+
// Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
|
|
102
|
+
// structure instead of granting ciphertext authority to any long base64-like model output.
|
|
103
|
+
// Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
|
|
104
|
+
// lowered by the compatibility path below rather than poisoning every later native replay.
|
|
105
|
+
return isStructurallyValidFernetToken(payload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
|
|
110
|
+
* known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
|
|
111
|
+
* the canonical backend later applies the stricter structural classifier.
|
|
112
|
+
*/
|
|
113
|
+
function looksLikeUnknownOpaqueSlot(payload: string): boolean {
|
|
101
114
|
return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
|
|
|
480
493
|
|
|
481
494
|
|
|
482
495
|
|
|
483
|
-
export function sanitizeEncryptedContentInPlace(
|
|
496
|
+
export function sanitizeEncryptedContentInPlace(
|
|
497
|
+
input: unknown,
|
|
498
|
+
options: { preserveUnknownOpaqueSlots?: boolean } = {},
|
|
499
|
+
): number {
|
|
484
500
|
if (!Array.isArray(input)) return 0;
|
|
485
501
|
let rewritten = 0;
|
|
486
502
|
const protectedFragments = new WeakSet<object>();
|
|
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
|
512
528
|
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
513
529
|
) {
|
|
514
530
|
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
515
|
-
|
|
531
|
+
const preserveUnknown = options.preserveUnknownOpaqueSlots === true
|
|
532
|
+
&& looksLikeUnknownOpaqueSlot(payload);
|
|
533
|
+
if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
|
|
516
534
|
const parts = encryptedSlotParts(payload);
|
|
517
535
|
frame.node.splice(frame.index, 1, ...parts);
|
|
518
536
|
rewritten += 1;
|
|
@@ -11,6 +11,7 @@ import type { WsData } from "../ws-bridge";
|
|
|
11
11
|
import { waitForProviderRequestSlot } from "../../providers/request-pacing";
|
|
12
12
|
import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
|
|
13
13
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
14
|
+
import { configuredOutboundFetch } from "../../lib/proxy-env";
|
|
14
15
|
|
|
15
16
|
export { withUpstreamHttpVersion };
|
|
16
17
|
|
|
@@ -44,6 +45,31 @@ export function safeOriginLabel(url: string): string {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Check whether a target host should bypass Bun's keep-alive pool reuse.
|
|
50
|
+
* Configured via the `OCX_FRESH_CONNECTION_HOSTS` environment variable (comma-separated).
|
|
51
|
+
*/
|
|
52
|
+
export function wantsFreshConnection(
|
|
53
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
54
|
+
hostsEnv = process.env.OCX_FRESH_CONNECTION_HOSTS,
|
|
55
|
+
): boolean {
|
|
56
|
+
if (!hostsEnv) return false;
|
|
57
|
+
try {
|
|
58
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
59
|
+
const host = new URL(rawUrl).hostname.toLowerCase();
|
|
60
|
+
const targets = hostsEnv
|
|
61
|
+
.split(",")
|
|
62
|
+
.map(h => h.trim().toLowerCase().replace(/^\.+/, ""))
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
for (const target of targets) {
|
|
65
|
+
if (host === target || host.endsWith(`.${target}`)) return true;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* unparseable target URL keeps default connection behavior */
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
|
|
48
74
|
|
|
49
75
|
export interface PaceAwareFetch {
|
|
@@ -53,6 +79,38 @@ export interface PaceAwareFetch {
|
|
|
53
79
|
|
|
54
80
|
export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
|
|
55
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Apply the physical-send connection policy to whichever fetch actually performs the send.
|
|
84
|
+
*
|
|
85
|
+
* The executor `providerFetch` builds is not the only physical boundary. A `dispatchOverride`
|
|
86
|
+
* that revalidates credentials re-reads `route.provider.fetch` at send time -- reselection can
|
|
87
|
+
* install a different provider transport after this wrapper was constructed -- and then calls
|
|
88
|
+
* that fetch directly instead of the supplied executor. Keeping the policy inside the executor
|
|
89
|
+
* alone therefore left every provider-scoped transport reusing a pooled socket for a host the
|
|
90
|
+
* operator had named in `OCX_FRESH_CONNECTION_HOSTS` (#4992). The policy belongs around the
|
|
91
|
+
* selected fetch so it follows the selection rather than the construction.
|
|
92
|
+
*
|
|
93
|
+
* Idempotent on purpose: an override that hands the send back to the supplied executor passes
|
|
94
|
+
* through here twice, and both passes derive the same headers from the same wire URL.
|
|
95
|
+
*/
|
|
96
|
+
export function sendWithConnectionPolicy(
|
|
97
|
+
physicalFetch: typeof globalThis.fetch,
|
|
98
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
99
|
+
init?: RequestInit,
|
|
100
|
+
): Promise<Response> {
|
|
101
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
102
|
+
const fresh = wantsFreshConnection(input);
|
|
103
|
+
if (fresh) {
|
|
104
|
+
headers.set("Connection", "close");
|
|
105
|
+
}
|
|
106
|
+
return physicalFetch(input, {
|
|
107
|
+
...init,
|
|
108
|
+
headers,
|
|
109
|
+
redirect: "manual",
|
|
110
|
+
...(fresh ? { keepalive: false } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
export interface ProviderFetchOptions {
|
|
57
115
|
nativeControl?: NativeResponseControl;
|
|
58
116
|
providerName?: string;
|
|
@@ -72,7 +130,11 @@ export function providerFetch(
|
|
|
72
130
|
runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
|
|
73
131
|
options: ProviderFetchOptions = {},
|
|
74
132
|
): ProviderFetch {
|
|
75
|
-
const
|
|
133
|
+
const configuredFetch = Object.assign(
|
|
134
|
+
(input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => configuredOutboundFetch(input, init),
|
|
135
|
+
{ preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) },
|
|
136
|
+
) as typeof globalThis.fetch;
|
|
137
|
+
const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? configuredFetch;
|
|
76
138
|
const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
|
|
77
139
|
base.preconnect?.(...args);
|
|
78
140
|
};
|
|
@@ -80,11 +142,15 @@ export function providerFetch(
|
|
|
80
142
|
// Return the original 3xx so the response owner retains its retry/health/relay contract.
|
|
81
143
|
const dispatch = Object.assign(
|
|
82
144
|
(input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
|
|
83
|
-
base
|
|
145
|
+
sendWithConnectionPolicy(base, input, init),
|
|
84
146
|
{ preconnect },
|
|
85
147
|
) as typeof globalThis.fetch;
|
|
86
148
|
const httpFetch = Object.assign(
|
|
87
149
|
async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
150
|
+
// The hook inspects the outgoing headers and refuses the send by throwing; it is not a
|
|
151
|
+
// mutator, and the copy it receives is deliberately not threaded onward. `Connection`
|
|
152
|
+
// is decided inside `dispatch`, which runs after this, so the fresh-connection policy
|
|
153
|
+
// wins regardless of what any caller or hook put in the header.
|
|
88
154
|
options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
|
|
89
155
|
const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
|
|
90
156
|
return options.dispatchOverride
|
|
@@ -30,21 +30,18 @@ import type { ResponsesTerminalStatus } from "../../bridge";
|
|
|
30
30
|
import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./ws-upstream";
|
|
31
31
|
import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
|
|
32
32
|
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
33
|
-
import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant } from "../../codex/auth-context";
|
|
33
|
+
import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant, releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
|
|
34
34
|
import { consumeComboFailure } from "./core-combo-failure";
|
|
35
35
|
import { readDisplaySafeErrorText } from "./core-errors";
|
|
36
36
|
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
37
37
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
38
|
-
import {
|
|
39
|
-
providerModelResponsesTerminalRepair,
|
|
40
|
-
providerModelResponsesUpstreamStreaming,
|
|
41
|
-
} from "../../providers/registry";
|
|
42
38
|
import {
|
|
43
39
|
resolvePassthroughWebSearchBridgeAuth,
|
|
44
40
|
planPassthroughWebSearchBridge,
|
|
45
41
|
createPassthroughWebSearchBridgeStream,
|
|
46
42
|
createPassthroughWebSearchBridgeExecutor,
|
|
47
43
|
} from "../../web-search/passthrough-bridge";
|
|
44
|
+
import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
|
|
48
45
|
import { fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
|
|
49
46
|
import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
|
|
50
47
|
import { requiresVisionPreprocessing } from "../../vision";
|
|
@@ -82,7 +79,10 @@ import {
|
|
|
82
79
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
83
80
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
84
81
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
85
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
createGrokResponsesControlFrameBlockRewrite,
|
|
84
|
+
createGrokResponsesTimestampBlockRewrite,
|
|
85
|
+
} from "../grok-responses-control-frame";
|
|
86
86
|
import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
|
|
87
87
|
import {
|
|
88
88
|
createPlaintextV2AgentMessageCallRestoreRewrite,
|
|
@@ -98,6 +98,24 @@ import {
|
|
|
98
98
|
normalizeDefaultNamespaceInJson,
|
|
99
99
|
} from "../responses-undeclared-tool-guard";
|
|
100
100
|
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Platform override for the two relay-path policy calls below. Tests only.
|
|
104
|
+
*
|
|
105
|
+
* The eager relay is reachable only on win32 and darwin, so a Linux shard cannot exercise it
|
|
106
|
+
* without claiming to be one of them. Overwriting `process.platform` globally does that, and a
|
|
107
|
+
* great deal more: every filesystem, ACL and state-directory decision in the process follows it,
|
|
108
|
+
* and the spend-ledger owner lowercases its home on win32, which on a case-sensitive filesystem
|
|
109
|
+
* names a DIFFERENT directory. A row that did that stopped being able to reserve its send and
|
|
110
|
+
* delivered no terminal at all, reporting as a relay defect. This narrows the claim to the two
|
|
111
|
+
* calls that actually choose the relay path.
|
|
112
|
+
*/
|
|
113
|
+
let relayPlatformForTests: NodeJS.Platform | undefined;
|
|
114
|
+
|
|
115
|
+
/** Internal test contract, not operator configuration: no config key reaches this. */
|
|
116
|
+
export function setRelayPlatformForTests(platform: NodeJS.Platform | undefined): void {
|
|
117
|
+
relayPlatformForTests = platform;
|
|
118
|
+
}
|
|
101
119
|
import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
|
|
102
120
|
import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
|
|
103
121
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
@@ -339,11 +357,7 @@ export async function deliverPassthroughResponse(
|
|
|
339
357
|
// before relay starts. Waiting for SSE completion would retain request state across the whole
|
|
340
358
|
// stream; a later body failure does not undo that this destination accepted and served the turn.
|
|
341
359
|
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
342
|
-
const terminalRepairPolicy =
|
|
343
|
-
route.providerName,
|
|
344
|
-
route.provider,
|
|
345
|
-
route.modelId,
|
|
346
|
-
);
|
|
360
|
+
const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
|
|
347
361
|
// #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
|
|
348
362
|
// and this branch relays that declaration on the assumption the destination executes it.
|
|
349
363
|
// A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
|
|
@@ -402,12 +416,20 @@ export async function deliverPassthroughResponse(
|
|
|
402
416
|
describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
|
|
403
417
|
sidecar: config.webSearchSidecar,
|
|
404
418
|
}),
|
|
419
|
+
// Scope the executed-search memo to this exact upstream (#4587). The Responses adapter
|
|
420
|
+
// derives the same scope from the same base URL before the NEXT turn is dispatched, so
|
|
421
|
+
// a replayed hosted cell can be turned back into the destination's own call and result.
|
|
422
|
+
destinationScope: bridgeSearchReplayScope(route.provider.baseUrl),
|
|
405
423
|
// Appending a search result can push the continuation past the ceiling the first leg
|
|
406
424
|
// was admitted under, so the same limit is re-applied before every later send.
|
|
407
425
|
checkOutboundBody: (continuationBody: string) => {
|
|
408
426
|
const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
|
|
409
427
|
return result.admitted ? undefined : describeOutboundBodyRefusal(result);
|
|
410
428
|
},
|
|
429
|
+
// Resolution can acquire the account's sole cooldown-recovery probe before the routed
|
|
430
|
+
// provider reveals whether it will request search. Hand an unused lease back on every
|
|
431
|
+
// terminal path; after an executed search, the outcome recorder has already settled it.
|
|
432
|
+
onFinalize: () => releaseCodexAuthContextProbeLease(openAiSidecar?.authContext),
|
|
411
433
|
signal: upstream.signal,
|
|
412
434
|
})
|
|
413
435
|
: upstreamResponse.body;
|
|
@@ -489,6 +511,9 @@ export async function deliverPassthroughResponse(
|
|
|
489
511
|
grokClientCompatibilityEnabled
|
|
490
512
|
? createGrokResponsesControlFrameBlockRewrite()
|
|
491
513
|
: undefined,
|
|
514
|
+
grokClientCompatibilityEnabled
|
|
515
|
+
? createGrokResponsesTimestampBlockRewrite()
|
|
516
|
+
: undefined,
|
|
492
517
|
grokClientCompatibilityEnabled
|
|
493
518
|
? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
|
|
494
519
|
: undefined,
|
|
@@ -520,12 +545,13 @@ export async function deliverPassthroughResponse(
|
|
|
520
545
|
? composeSseBlockRewrites(...blockRewrites)
|
|
521
546
|
: undefined;
|
|
522
547
|
const needsClientRewrite = clientBlockRewrite !== undefined;
|
|
548
|
+
const relayPlatform = relayPlatformForTests ?? process.platform;
|
|
523
549
|
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
|
|
524
550
|
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
|
|
525
551
|
// lost). The eager single reader applies the same rewrites inline.
|
|
526
|
-
const win32EagerRewrite = isWin32EagerRewrite(
|
|
552
|
+
const win32EagerRewrite = isWin32EagerRewrite(relayPlatform, needsClientRewrite);
|
|
527
553
|
const eagerPath = selectEagerPath(
|
|
528
|
-
|
|
554
|
+
relayPlatform,
|
|
529
555
|
needsClientRewrite,
|
|
530
556
|
config.streamMode ?? "auto",
|
|
531
557
|
);
|
|
@@ -796,7 +822,7 @@ export async function deliverPassthroughResponse(
|
|
|
796
822
|
// stream that never closes. Non-streaming clients keep the plain JSON.
|
|
797
823
|
if (clientRequestedStream === true
|
|
798
824
|
&& options.inboundTransport !== "websocket"
|
|
799
|
-
&&
|
|
825
|
+
&& route.staticPolicy.model.responsesUpstreamStreaming === false
|
|
800
826
|
&& route.provider.adapter === "openai-responses") {
|
|
801
827
|
let completed: Record<string, unknown> | undefined;
|
|
802
828
|
try {
|
|
@@ -843,7 +869,7 @@ export async function deliverPassthroughResponse(
|
|
|
843
869
|
// WS turns reframe this JSON into events in the bridge, which is the
|
|
844
870
|
// other relay-free path — normalize ids so both bounded-JSON paths agree.
|
|
845
871
|
const outboundJson = options.inboundTransport === "websocket"
|
|
846
|
-
&&
|
|
872
|
+
&& route.staticPolicy.model.responsesUpstreamStreaming === false
|
|
847
873
|
&& hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
|
|
848
874
|
? (() => {
|
|
849
875
|
try {
|