@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -108,6 +108,11 @@ export type CodexWsStageRecord = Omit<CodexWsFailureStage, "requestBytes"> & {
|
|
|
108
108
|
const codexWsStageByResponse = new WeakMap<Response, CodexWsStageRecord>();
|
|
109
109
|
|
|
110
110
|
export function markCodexWsStage(response: Response, record: CodexWsStageRecord): void {
|
|
111
|
+
const current = codexWsStageByResponse.get(response);
|
|
112
|
+
if (current) {
|
|
113
|
+
Object.assign(current, record);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
111
116
|
codexWsStageByResponse.set(response, record);
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../../combos";
|
|
31
31
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
32
32
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
33
|
-
import { dottedToolName, modelInList, namespacedToolName, toolChoiceToolPredicate } from "../../types";
|
|
33
|
+
import { dottedToolName, modelInList, namespacedToolName, NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES, toolChoiceToolPredicate } from "../../types";
|
|
34
34
|
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
35
35
|
import {
|
|
36
36
|
forceRefreshOAuthAccessSnapshot,
|
|
@@ -83,7 +83,6 @@ import {
|
|
|
83
83
|
catalogModelSupportsServiceTier,
|
|
84
84
|
finishRequestAttempt,
|
|
85
85
|
inspectResponseLogJson,
|
|
86
|
-
noteAttemptSend,
|
|
87
86
|
readConfiguredCodexServiceTier,
|
|
88
87
|
requestLogSpeedLabel,
|
|
89
88
|
sealRequestAttemptIdentity,
|
|
@@ -152,6 +151,36 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
152
151
|
dottedAliasOwners.set(t.name, null);
|
|
153
152
|
}
|
|
154
153
|
}
|
|
154
|
+
// Bare echo alias (`name` with no namespace spelling, #4679): some providers — observed
|
|
155
|
+
// on the muse family via Command Code — echo a namespaced tool by its bare name. The
|
|
156
|
+
// bare spelling is only a safe alias while it names ONE tool and cannot be read as
|
|
157
|
+
// another identity's canonical or dotted spelling.
|
|
158
|
+
// Code-mode helper spellings never gain a bare alias (#4679 review), whatever namespace
|
|
159
|
+
// declares them and whatever put the alias there. The list is owned by `src/types/tools.ts`,
|
|
160
|
+
// beside the names it protects, because the copy that used to live here drifted to a single
|
|
161
|
+
// namespace and had to be widened twice.
|
|
162
|
+
const bareAliasOwners = new Map<string, string | null>();
|
|
163
|
+
for (const t of authorizedTools) {
|
|
164
|
+
// Bare (no-namespace) declarations participate as owners too: a namespaced tool whose
|
|
165
|
+
// bare name equals a bare-declared function must not gain the bare alias, mirroring how
|
|
166
|
+
// the tool_choice bare path refuses ambiguous owners across the whole request catalog.
|
|
167
|
+
const identity = JSON.stringify([t.namespace ?? null, t.name]);
|
|
168
|
+
const owner = bareAliasOwners.get(t.name);
|
|
169
|
+
if (owner === undefined) bareAliasOwners.set(t.name, identity);
|
|
170
|
+
else if (owner !== identity) bareAliasOwners.set(t.name, null);
|
|
171
|
+
}
|
|
172
|
+
for (const t of authorizedTools) {
|
|
173
|
+
const canonical = namespacedToolName(t.namespace, t.name);
|
|
174
|
+
const owner = bareAliasOwners.get(canonical);
|
|
175
|
+
if (owner !== undefined && owner !== JSON.stringify([t.namespace, t.name])) {
|
|
176
|
+
bareAliasOwners.set(canonical, null);
|
|
177
|
+
}
|
|
178
|
+
const dotted = dottedToolName(t.namespace, t.name);
|
|
179
|
+
const dottedOwner = bareAliasOwners.get(dotted);
|
|
180
|
+
if (dottedOwner !== undefined && dottedOwner !== JSON.stringify([t.namespace, t.name])) {
|
|
181
|
+
bareAliasOwners.set(dotted, null);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
155
184
|
for (const t of authorizedTools) {
|
|
156
185
|
// Upstream output is untrusted: only restore calls for tools the caller authorized.
|
|
157
186
|
const wireName = namespacedToolName(t.namespace, t.name);
|
|
@@ -175,6 +204,30 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
175
204
|
toolNsMap.set(dottedName, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
|
|
176
205
|
if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(dottedName, t.parameters);
|
|
177
206
|
}
|
|
207
|
+
// Bare echo alias (`name` with no namespace spelling, #4679): same tool identity as
|
|
208
|
+
// the flattened wire name, so a provider that drops the namespace prefix still
|
|
209
|
+
// restores against this entry. Ambiguous bare names were resolved to null above;
|
|
210
|
+
// skipping them falls back to the spellings every provider can still echo.
|
|
211
|
+
// Code-mode helper spellings never gain a bare alias, from ANY namespace (#4792 review).
|
|
212
|
+
// Scoping this to `collaboration` was too narrow to be a boundary: a declaration such as
|
|
213
|
+
// `mcp__remote.exec` donated bare `exec` to the declared set, and normalizeDeclaredToolName
|
|
214
|
+
// then rewrote an undeclared `apply_patch`, `exec_command` or `write_stdin` onto it
|
|
215
|
+
// (src/types/tools.ts). No namespace may turn helper normalization on for a catalog that
|
|
216
|
+
// never declared the code-mode shell.
|
|
217
|
+
//
|
|
218
|
+
// The namespaced tool stays usable AS ITSELF: `ns__name` is added unconditionally above
|
|
219
|
+
// and `ns.name` whenever it is unambiguous, so only the namespace-dropping echo fallback
|
|
220
|
+
// is withdrawn, and only for these six spellings.
|
|
221
|
+
if (
|
|
222
|
+
bareAliasOwners.get(t.name) === JSON.stringify([t.namespace, t.name])
|
|
223
|
+
&& !NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES.has(t.name)
|
|
224
|
+
) {
|
|
225
|
+
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
226
|
+
declaredToolNames.add(t.name);
|
|
227
|
+
budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([t.name, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
|
|
228
|
+
toolNsMap.set(t.name, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
|
|
229
|
+
if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(t.name, t.parameters);
|
|
230
|
+
}
|
|
178
231
|
}
|
|
179
232
|
if (t.freeform) {
|
|
180
233
|
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
@@ -201,6 +254,20 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
201
254
|
// Some routed providers echo a bare tool_choice selector instead of the flattened catalog
|
|
202
255
|
// name. Accept only selectors the client actually sent and only when the full request catalog
|
|
203
256
|
// contains one tool with that logical name.
|
|
257
|
+
//
|
|
258
|
+
// A helper spelling selected this way is split rather than refused (#4819). The two things a
|
|
259
|
+
// bare alias does are separable, and passthrough already relies on that: identity RESTORATION
|
|
260
|
+
// runs before authorization there, rewriting the echoed bare name to the namespaced identity
|
|
261
|
+
// the caller declared, and the guard then authorizes `ns__name`. DECLARATION is the part that
|
|
262
|
+
// is unsafe, because a declared-name set carrying bare `exec` is what makes
|
|
263
|
+
// `normalizeDeclaredToolName` rewrite an undeclared `apply_patch`, `exec_command` or
|
|
264
|
+
// `write_stdin` onto the selected tool (src/types/tools.ts).
|
|
265
|
+
//
|
|
266
|
+
// So a helper spelling gets the `toolNsMap` entry and not the `declaredToolNames` entry. The
|
|
267
|
+
// caller nominated exactly one tool by name, `bareNameCounts` proves nothing else answers to
|
|
268
|
+
// it, and restoring it authorizes nothing the request did not already declare. The echo path
|
|
269
|
+
// above withholds both, because a bare echo is a guess rather than a nomination and #4679
|
|
270
|
+
// pinned that shape (`tests/responses/bare-echo-alias.test.ts`).
|
|
204
271
|
const choice = parsed.options.toolChoice;
|
|
205
272
|
const bareChoiceNames = new Set(
|
|
206
273
|
choice && typeof choice === "object"
|
|
@@ -213,8 +280,11 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
213
280
|
}
|
|
214
281
|
for (const t of authorizedTools) {
|
|
215
282
|
if (!t.namespace || !bareChoiceNames.has(t.name) || bareNameCounts.get(t.name) !== 1 || declaredToolNames.has(t.name)) continue;
|
|
216
|
-
|
|
217
|
-
|
|
283
|
+
// Restore the identity; declare the name only when it is not a helper spelling.
|
|
284
|
+
if (!NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES.has(t.name)) {
|
|
285
|
+
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
286
|
+
declaredToolNames.add(t.name);
|
|
287
|
+
}
|
|
218
288
|
budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([t.name, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
|
|
219
289
|
toolNsMap.set(t.name, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
|
|
220
290
|
if (t.parameters && typeof t.parameters === "object") {
|
|
@@ -8,14 +8,46 @@ interface ComboRecallEntry {
|
|
|
8
8
|
target: Pick<OcxComboTarget, "provider" | "model">;
|
|
9
9
|
responseModel: string;
|
|
10
10
|
at: number;
|
|
11
|
+
/** UTF-8 size of `responseModel`, the only client-influenced field of unbounded length. */
|
|
12
|
+
bytes: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const RECALL_CAPACITY = 256;
|
|
14
16
|
const RECALL_TTL_MS = 30 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* A model id is provider-reported and arrives on the response, so nothing upstream of here
|
|
19
|
+
* bounds its length. Lane keys are already SHA-256 digests, so the model string is the only
|
|
20
|
+
* field that can grow, and 256 lanes alone do not bound the bytes they hold.
|
|
21
|
+
*/
|
|
22
|
+
const RECALL_MODEL_BYTES_MAX = 1024;
|
|
23
|
+
const RECALL_TOTAL_BYTES_MAX = 64 * 1024;
|
|
15
24
|
const recall = new Map<string, ComboRecallEntry>();
|
|
25
|
+
let recallBytes = 0;
|
|
16
26
|
let lastReconciledGeneration = 0;
|
|
17
27
|
let liveOwners: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames"> | undefined;
|
|
18
28
|
|
|
29
|
+
/** Every removal path goes through here so the byte counter can never drift from the map. */
|
|
30
|
+
function deleteEntry(lane: string): boolean {
|
|
31
|
+
const entry = recall.get(lane);
|
|
32
|
+
if (!entry) return false;
|
|
33
|
+
recall.delete(lane);
|
|
34
|
+
recallBytes -= entry.bytes;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* UTF-8 size of a remembered model id, or null when it is too large to retain.
|
|
40
|
+
*
|
|
41
|
+
* The code-unit test runs first and is the part that matters: a UTF-8 encoding is never smaller
|
|
42
|
+
* than the code-unit count, so an oversized string is rejected without encoding it, and the
|
|
43
|
+
* bound cannot be defeated by paying the allocation it exists to prevent.
|
|
44
|
+
*/
|
|
45
|
+
function boundedModelBytes(responseModel: string): number | null {
|
|
46
|
+
if (responseModel.length > RECALL_MODEL_BYTES_MAX) return null;
|
|
47
|
+
const bytes = Buffer.byteLength(responseModel, "utf8");
|
|
48
|
+
return bytes > RECALL_MODEL_BYTES_MAX ? null : bytes;
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
function ownsEntry(context: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames">, entry: ComboRecallEntry): boolean {
|
|
20
52
|
return context.comboIds.has(entry.comboId)
|
|
21
53
|
&& context.providerNames.has(entry.target.provider)
|
|
@@ -32,14 +64,29 @@ export function rememberComboForLane(
|
|
|
32
64
|
if (!lane || !comboId || !responseModel.trim()) return;
|
|
33
65
|
// Reject even a same-named recreated owner: its previous in-flight turn is obsolete.
|
|
34
66
|
if (writerGeneration < Math.max(lastReconciledGeneration, captureConfigGeneration())) return;
|
|
35
|
-
|
|
67
|
+
// An unretainable model id DECLINES the write; it must not clear the lane. Every other
|
|
68
|
+
// rejection above returns the same way, and clearing here would let a late completion erase
|
|
69
|
+
// a newer selection that this function has no ordering information to compare against.
|
|
70
|
+
const bytes = boundedModelBytes(responseModel);
|
|
71
|
+
if (bytes === null) return;
|
|
72
|
+
const entry = {
|
|
73
|
+
comboId,
|
|
74
|
+
target: { provider: target.provider, model: target.model },
|
|
75
|
+
responseModel,
|
|
76
|
+
at: Date.now(),
|
|
77
|
+
bytes,
|
|
78
|
+
};
|
|
36
79
|
if (liveOwners && !ownsEntry(liveOwners, entry)) return;
|
|
37
|
-
|
|
80
|
+
deleteEntry(lane);
|
|
38
81
|
recall.set(lane, entry);
|
|
39
|
-
|
|
82
|
+
recallBytes += bytes;
|
|
83
|
+
// Insertion order is recency order, because every write re-inserts its lane at the back.
|
|
84
|
+
// Evicting from the front therefore drops the least recently written lane, never this one:
|
|
85
|
+
// a single entry is capped well below the aggregate budget, so it always fits.
|
|
86
|
+
while (recall.size > RECALL_CAPACITY || recallBytes > RECALL_TOTAL_BYTES_MAX) {
|
|
40
87
|
const oldest = recall.keys().next().value;
|
|
41
|
-
if (oldest === undefined) break;
|
|
42
|
-
|
|
88
|
+
if (oldest === undefined || oldest === lane) break;
|
|
89
|
+
deleteEntry(oldest);
|
|
43
90
|
}
|
|
44
91
|
}
|
|
45
92
|
|
|
@@ -57,12 +104,25 @@ export function recallComboForLane(
|
|
|
57
104
|
|| !Object.hasOwn(config.providers, entry.target.provider)
|
|
58
105
|
|| !provider || provider.disabled === true
|
|
59
106
|
|| !combo?.targets.some(target => targetKey(target) === targetKey(entry.target))) {
|
|
60
|
-
|
|
107
|
+
deleteEntry(lane);
|
|
61
108
|
return undefined;
|
|
62
109
|
}
|
|
63
110
|
return entry.responseModel === model ? entry.comboId : undefined;
|
|
64
111
|
}
|
|
65
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Periodic expiry. Without it a lane that is never read again and never touched by a config
|
|
115
|
+
* reconciliation holds its entry for the life of the process: the existing TTL is only
|
|
116
|
+
* evaluated on read or on generation change.
|
|
117
|
+
*/
|
|
118
|
+
export function sweepExpiredComboRecall(now: number): number {
|
|
119
|
+
let removed = 0;
|
|
120
|
+
for (const [lane, entry] of recall) {
|
|
121
|
+
if (now - entry.at >= RECALL_TTL_MS && deleteEntry(lane)) removed += 1;
|
|
122
|
+
}
|
|
123
|
+
return removed;
|
|
124
|
+
}
|
|
125
|
+
|
|
66
126
|
export function reconcileComboRecall(context: GenerationContext): number {
|
|
67
127
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
68
128
|
lastReconciledGeneration = context.generation;
|
|
@@ -74,8 +134,7 @@ export function reconcileComboRecall(context: GenerationContext): number {
|
|
|
74
134
|
let removed = 0;
|
|
75
135
|
for (const [lane, entry] of recall) {
|
|
76
136
|
if (!ownsEntry(context, entry) || Date.now() - entry.at >= RECALL_TTL_MS) {
|
|
77
|
-
|
|
78
|
-
removed += 1;
|
|
137
|
+
if (deleteEntry(lane)) removed += 1;
|
|
79
138
|
}
|
|
80
139
|
}
|
|
81
140
|
return removed;
|
|
@@ -84,6 +143,7 @@ export function reconcileComboRecall(context: GenerationContext): number {
|
|
|
84
143
|
/** Test-only reset, alongside the combo rotation/cooldown resets. */
|
|
85
144
|
export function clearComboRecallForTests(): void {
|
|
86
145
|
recall.clear();
|
|
146
|
+
recallBytes = 0;
|
|
87
147
|
lastReconciledGeneration = 0;
|
|
88
148
|
liveOwners = undefined;
|
|
89
149
|
}
|
|
@@ -51,7 +51,9 @@ import {
|
|
|
51
51
|
materializeCodexUpstreamAuthAsync,
|
|
52
52
|
isCodexAuthContextUsable,
|
|
53
53
|
resolveCodexAuthContext,
|
|
54
|
+
codexPoolAffinityKey,
|
|
54
55
|
codexProbeLeaseId,
|
|
56
|
+
codexTransientProbeGrant,
|
|
55
57
|
codexProbeQuotaScope,
|
|
56
58
|
releaseCodexAuthContextProbeLease,
|
|
57
59
|
stripCodexRuntimeProviderFields,
|
|
@@ -67,6 +69,13 @@ import {
|
|
|
67
69
|
recordCodexUpstreamOutcome,
|
|
68
70
|
type CodexUpstreamOutcome,
|
|
69
71
|
} from "../../codex/routing";
|
|
72
|
+
import {
|
|
73
|
+
applyAccountChangeConversationStateScrub,
|
|
74
|
+
conversationStateBindingFromAuth,
|
|
75
|
+
accountChangeFileReferenceRefusal,
|
|
76
|
+
rememberServingConversationStateIssuer,
|
|
77
|
+
conversationCarriesUploadedFiles,
|
|
78
|
+
} from "./account-change-state";
|
|
70
79
|
import {
|
|
71
80
|
TokenRefreshError,
|
|
72
81
|
forceRefreshCodexPoolToken,
|
|
@@ -76,6 +85,7 @@ import {
|
|
|
76
85
|
fetchWithResetRetry,
|
|
77
86
|
fetchWithTransientRetry,
|
|
78
87
|
applyUpstreamRecoveryInit,
|
|
88
|
+
isNonReplayableResponse,
|
|
79
89
|
SendBudgetExhaustedError,
|
|
80
90
|
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
81
91
|
type UpstreamSendRecovery,
|
|
@@ -134,7 +144,6 @@ import {
|
|
|
134
144
|
catalogModelSupportsServiceTier,
|
|
135
145
|
finishRequestAttempt,
|
|
136
146
|
inspectResponseLogJson,
|
|
137
|
-
noteAttemptSend,
|
|
138
147
|
readConfiguredCodexServiceTier,
|
|
139
148
|
requestLogSpeedLabel,
|
|
140
149
|
sealRequestAttemptIdentity,
|
|
@@ -682,6 +691,13 @@ export async function handleResponsesCompact(
|
|
|
682
691
|
// Combo-resolved targets skip native compact so failover can advance through the
|
|
683
692
|
// combo target list when the picked model returns 429/5xx — the routed path below
|
|
684
693
|
// dispatches through handleResponses → handleComboResponses with full failover.
|
|
694
|
+
//
|
|
695
|
+
// One holder for the WHOLE logical compact, declared above the native branch because the
|
|
696
|
+
// routed fallback below is not a different request: a native attempt that 404s, or a quota
|
|
697
|
+
// failure that hands off, continues here. The routed turn used to call handleResponses with
|
|
698
|
+
// no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
|
|
699
|
+
// had already spent some of the first one.
|
|
700
|
+
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
685
701
|
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo) {
|
|
686
702
|
if (req.signal.aborted) {
|
|
687
703
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
@@ -722,6 +738,14 @@ export async function handleResponsesCompact(
|
|
|
722
738
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
723
739
|
signal: req.signal,
|
|
724
740
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
741
|
+
// #4778: the same retention the regular Responses path passes at its final auth. The
|
|
742
|
+
// post-429 guard below only declines to move AFTER this resolution has already bound
|
|
743
|
+
// an account, so without the bit here a quota-driven rebind could have carried the
|
|
744
|
+
// conversation off its issuing account before that guard is ever consulted -- and an
|
|
745
|
+
// uploaded file is readable only by the account that received it. Answered from `raw`,
|
|
746
|
+
// the same object and the same predicate the guard below uses, so the two can never
|
|
747
|
+
// disagree about which conversations are in scope.
|
|
748
|
+
retainAccountForUploadedFiles: conversationCarriesUploadedFiles(raw),
|
|
725
749
|
});
|
|
726
750
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
727
751
|
const selected = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
|
|
@@ -773,10 +797,32 @@ export async function handleResponsesCompact(
|
|
|
773
797
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
774
798
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
775
799
|
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
800
|
+
{
|
|
801
|
+
const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
|
|
802
|
+
if (binding) {
|
|
803
|
+
// Refused rather than scrubbed: an uploaded file is content the caller attached, not
|
|
804
|
+
// continuation state the turn can do without.
|
|
805
|
+
const refusal = accountChangeFileReferenceRefusal({
|
|
806
|
+
body: raw,
|
|
807
|
+
bindingKey: binding.bindingKey,
|
|
808
|
+
servingAccountId: binding.accountId,
|
|
809
|
+
});
|
|
810
|
+
if (refusal) return refusal;
|
|
811
|
+
applyAccountChangeConversationStateScrub({
|
|
812
|
+
body: raw,
|
|
813
|
+
bindingKey: binding.bindingKey,
|
|
814
|
+
servingAccountId: binding.accountId,
|
|
815
|
+
logCtx,
|
|
816
|
+
});
|
|
817
|
+
applyAccountChangeConversationStateScrub({
|
|
818
|
+
body: compactBody,
|
|
819
|
+
bindingKey: binding.bindingKey,
|
|
820
|
+
servingAccountId: binding.accountId,
|
|
821
|
+
logCtx,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
776
825
|
const compactUrl = `${base}/responses/compact`;
|
|
777
|
-
// One holder for this logical compact, inherited by the handoff child so a second model
|
|
778
|
-
// does not start over with a fresh four.
|
|
779
|
-
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
780
826
|
const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;
|
|
781
827
|
const actualCompactHostKey = upstreamHostHealthKey(
|
|
782
828
|
route.providerName,
|
|
@@ -842,6 +888,7 @@ export async function handleResponsesCompact(
|
|
|
842
888
|
// replacement (#2887). Also covers the replay's own second 401.
|
|
843
889
|
...(ctx.kind === "pool" ? { credentialGeneration: ctx.generation } : {}),
|
|
844
890
|
probeQuotaScope: codexProbeQuotaScope(ctx),
|
|
891
|
+
transientProbe: codexTransientProbeGrant(ctx),
|
|
845
892
|
writerGeneration: ctx.kind === "pool" || ctx.kind === "main-pool"
|
|
846
893
|
? ctx.writerGeneration
|
|
847
894
|
: undefined,
|
|
@@ -1041,6 +1088,10 @@ export async function handleResponsesCompact(
|
|
|
1041
1088
|
// — reporting exhausted retries while another pool account sat idle (#913).
|
|
1042
1089
|
if (
|
|
1043
1090
|
(upstream.status === 429 || upstream.status === 402)
|
|
1091
|
+
// A replay refusal this proxy synthesized carries 429 for the client's benefit only.
|
|
1092
|
+
// It is not pool quota evidence, and the alternate account below is another send of a
|
|
1093
|
+
// compact turn that may already have been processed.
|
|
1094
|
+
&& !isNonReplayableResponse(upstream)
|
|
1044
1095
|
&& !storedPool401ReplayAttempted
|
|
1045
1096
|
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
1046
1097
|
&& !authCtx.fixedAccount
|
|
@@ -1055,15 +1106,22 @@ export async function handleResponsesCompact(
|
|
|
1055
1106
|
].filter(Boolean);
|
|
1056
1107
|
// Build the alternate COMPLETELY before cancelling the first body: if construction
|
|
1057
1108
|
// throws, the first rejection is still intact and can be returned to the client.
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1109
|
+
// The same reasoning refuses an uploaded-file move here: no alternate can read a file the
|
|
1110
|
+
// issuing account received, so which one is chosen is irrelevant and asking before the
|
|
1111
|
+
// resolution costs nothing. It also reuses the guarantee the comment above depends on --
|
|
1112
|
+
// the first body is still uncancelled -- so the client gets the original rejection rather
|
|
1113
|
+
// than an inaccessible-file error from account B (#4710).
|
|
1114
|
+
const alternate = conversationCarriesUploadedFiles(raw)
|
|
1115
|
+
? undefined
|
|
1116
|
+
: await resolveAlternateCompactContext({
|
|
1117
|
+
req,
|
|
1118
|
+
admission,
|
|
1119
|
+
config,
|
|
1120
|
+
route,
|
|
1121
|
+
selectedModelId,
|
|
1122
|
+
excludeAccountId: authCtx.accountId,
|
|
1123
|
+
turnAdmissionLease,
|
|
1124
|
+
});
|
|
1067
1125
|
// Resolution can await a credential refresh, so the client may have gone away
|
|
1068
1126
|
// while we were choosing B. Re-check before spending anything: recording A,
|
|
1069
1127
|
// cancelling its body, and sending B are all observable side effects, and B's
|
|
@@ -1096,6 +1154,30 @@ export async function handleResponsesCompact(
|
|
|
1096
1154
|
await upstream.body?.cancel().catch(() => undefined);
|
|
1097
1155
|
outcomeCtx = alternate.authCtx;
|
|
1098
1156
|
logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
|
|
1157
|
+
{
|
|
1158
|
+
const binding = conversationStateBindingFromAuth(
|
|
1159
|
+
alternate.authCtx,
|
|
1160
|
+
(authCtx.kind === "pool" || authCtx.kind === "main-pool")
|
|
1161
|
+
? authCtx.affinityKey
|
|
1162
|
+
: codexPoolAffinityKey(req.headers),
|
|
1163
|
+
);
|
|
1164
|
+
if (binding) {
|
|
1165
|
+
applyAccountChangeConversationStateScrub({
|
|
1166
|
+
body: raw,
|
|
1167
|
+
bindingKey: binding.bindingKey,
|
|
1168
|
+
servingAccountId: binding.accountId,
|
|
1169
|
+
priorAccountId: authCtx.accountId,
|
|
1170
|
+
logCtx,
|
|
1171
|
+
});
|
|
1172
|
+
applyAccountChangeConversationStateScrub({
|
|
1173
|
+
body: compactBody,
|
|
1174
|
+
bindingKey: binding.bindingKey,
|
|
1175
|
+
servingAccountId: binding.accountId,
|
|
1176
|
+
priorAccountId: authCtx.accountId,
|
|
1177
|
+
logCtx,
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1099
1181
|
try {
|
|
1100
1182
|
upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
|
|
1101
1183
|
} catch (err) {
|
|
@@ -1142,8 +1224,14 @@ export async function handleResponsesCompact(
|
|
|
1142
1224
|
const bufferedErrorText = buffered.ok
|
|
1143
1225
|
? ""
|
|
1144
1226
|
: await buffered.clone().text().catch(() => "");
|
|
1145
|
-
|
|
1146
|
-
|
|
1227
|
+
// The client-facing 429 of a synthesized replay refusal says nothing about this
|
|
1228
|
+
// account's quota. Pool accounting keeps reading it as the transport failure it is,
|
|
1229
|
+
// which is also what it recorded before the status was corrected for the client.
|
|
1230
|
+
const replayRefused = isNonReplayableResponse(upstream);
|
|
1231
|
+
const explicitQuotaStatus = !replayRefused
|
|
1232
|
+
&& (buffered.status === 429 || buffered.status === 402);
|
|
1233
|
+
const bodyInferredQuota = !replayRefused
|
|
1234
|
+
&& !buffered.ok
|
|
1147
1235
|
&& !explicitQuotaStatus
|
|
1148
1236
|
&& isRateLimitOrQuotaFailureMessage(bufferedErrorText);
|
|
1149
1237
|
const quotaFailure = explicitQuotaStatus || bodyInferredQuota;
|
|
@@ -1156,13 +1244,18 @@ export async function handleResponsesCompact(
|
|
|
1156
1244
|
// A body-confirmed quota failure can arrive behind a generic 5xx. Record it as
|
|
1157
1245
|
// quota evidence; otherwise preserve the real upstream status so a local buffering
|
|
1158
1246
|
// failure after a 200 cannot soft-avoid a healthy account or rotate a thread.
|
|
1159
|
-
recordCompactPoolOutcome(
|
|
1247
|
+
recordCompactPoolOutcome(
|
|
1248
|
+
outcomeCtx,
|
|
1249
|
+
bodyInferredQuota ? 429 : replayRefused ? 502 : upstream.status,
|
|
1250
|
+
{ retryAfter, resetAt },
|
|
1251
|
+
);
|
|
1160
1252
|
// Lift usage and response metadata from the buffered upstream JSON into the
|
|
1161
1253
|
// request log; the routed branch gets the same through handleResponses. The
|
|
1162
1254
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
1163
1255
|
if (buffered.ok) {
|
|
1164
1256
|
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
1165
1257
|
forgetCompactHandoffRoute(req);
|
|
1258
|
+
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1166
1259
|
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
1167
1260
|
const fallbackModel = compactHandoffRoute(req, raw.model);
|
|
1168
1261
|
if (fallbackModel && !req.signal.aborted) {
|
|
@@ -1223,7 +1316,10 @@ export async function handleResponsesCompact(
|
|
|
1223
1316
|
body: JSON.stringify(internalBody),
|
|
1224
1317
|
});
|
|
1225
1318
|
linkRequestSessionLane(req, internalReq);
|
|
1226
|
-
|
|
1319
|
+
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1320
|
+
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1321
|
+
// fallback spend four more.
|
|
1322
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
|
|
1227
1323
|
if (!response.ok) return response;
|
|
1228
1324
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
1229
1325
|
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ResponsesRequestContext } from "./core-options";
|
|
2
|
+
import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
|
|
3
|
+
import { emptyCompletionRetryEnabled } from "./empty-completion-guard";
|
|
4
|
+
|
|
5
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
6
|
+
export function createResponsesCompletionPolicy(
|
|
7
|
+
requestContext: Pick<ResponsesRequestContext, "config" | "options">,
|
|
8
|
+
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
9
|
+
) {
|
|
10
|
+
const { config, options } = requestContext;
|
|
11
|
+
const { routedCompaction } = sidecarState;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Empty-completion guard (codex-router PR #145 port): a 200 that completes with no output
|
|
15
|
+
// text and no tool call is a failure the client cannot see — it silently records the turn as
|
|
16
|
+
// done. The guard holds pre-content adapter events, suppresses the terminal of an empty
|
|
17
|
+
// turn, retries the IDENTICAL request once, and surfaces a stated error when the retry is
|
|
18
|
+
// empty or fails. This is a top-level config opt-in; OCX_EMPTY_COMPLETION_RETRY=0 is a
|
|
19
|
+
// disable-only emergency override. Compaction turns and combo attempts keep their own
|
|
20
|
+
// machinery (the combo preflight already handles empty streams). Native Chat-to-Chat
|
|
21
|
+
// requests return from handleChatCompletions before entering Responses core, so they are
|
|
22
|
+
// intentionally outside this guard and retain their existing one-send wire behavior.
|
|
23
|
+
const emptyCompletionGuardEnabled =
|
|
24
|
+
emptyCompletionRetryEnabled(config)
|
|
25
|
+
&& !options.comboAttempt
|
|
26
|
+
&& !routedCompaction;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
emptyCompletionGuardEnabled,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ResponsesCompletionPolicy = Exclude<ReturnType<typeof createResponsesCompletionPolicy>, Response>;
|