@bitkyc08/opencodex 2.63.0 → 2.64.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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-CpuDF3ci.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/index-SggB6t3z.js +86 -0
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
import { loadConfig } from "../config";
|
|
13
13
|
import { isWildcardHostname } from "../codex/loopback-target";
|
|
14
14
|
import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
|
|
15
|
+
import {
|
|
16
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
17
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
18
|
+
createLocalAttestationChallenge,
|
|
19
|
+
verifyLocalAttestationProof,
|
|
20
|
+
} from "../lib/local-management-attestation";
|
|
15
21
|
import { directLocalHttpFetch } from "./direct-local-http";
|
|
16
22
|
|
|
17
23
|
export interface HealthzIdentity {
|
|
@@ -29,7 +35,11 @@ export interface HealthzIdentity {
|
|
|
29
35
|
role?: unknown;
|
|
30
36
|
restartCapability?: unknown;
|
|
31
37
|
providerReloadCapability?: unknown;
|
|
38
|
+
asideSyncCapability?: unknown;
|
|
32
39
|
guiPairCapability?: unknown;
|
|
40
|
+
/** Present on a package-tree-fenced 503: the version on disk an in-place respawn will run. */
|
|
41
|
+
installedVersion?: unknown;
|
|
42
|
+
error?: unknown;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export type EndpointLiveness = "live" | "dead" | "unknown";
|
|
@@ -60,6 +70,18 @@ export interface LivenessIo {
|
|
|
60
70
|
*/
|
|
61
71
|
deadlineAt?: number;
|
|
62
72
|
nowFn?: () => number;
|
|
73
|
+
/**
|
|
74
|
+
* Also accept a proxy whose `/healthz` is fenced by the package-tree guard (#5496).
|
|
75
|
+
*
|
|
76
|
+
* Opt-in, because such a proxy is ours but not healthy: `ocx restart` and the stop paths
|
|
77
|
+
* must find it, while ensure, update health waits and replacement waits must not count it.
|
|
78
|
+
* A fenced body is never trusted by itself. It is accepted only when this home's runtime
|
|
79
|
+
* record names the same pid and port and the listener proves possession of that record's
|
|
80
|
+
* attestation secret for a fresh challenge.
|
|
81
|
+
*/
|
|
82
|
+
acceptPackageTreeFenced?: boolean;
|
|
83
|
+
/** Test seam for the fenced-identity challenge. */
|
|
84
|
+
createChallengeFn?: () => string;
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
/** Default per-probe fetch ceiling shared by liveness and readiness probes. */
|
|
@@ -117,6 +139,12 @@ export interface LiveProxy {
|
|
|
117
139
|
* on. Absent for a proxy whose healthz body predates the field.
|
|
118
140
|
*/
|
|
119
141
|
role?: string;
|
|
142
|
+
/**
|
|
143
|
+
* The proxy answered with an attested package-tree fence (#5496): it is this home's process,
|
|
144
|
+
* but it is refusing traffic until it restarts. Only set for callers that opted in through
|
|
145
|
+
* `LivenessIo.acceptPackageTreeFenced`.
|
|
146
|
+
*/
|
|
147
|
+
packageTreeFenced?: true;
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
/**
|
|
@@ -155,6 +183,64 @@ export function isOpencodexHealthz(body: HealthzIdentity | null): boolean {
|
|
|
155
183
|
return body.status === "ok" && typeof body.version === "string" && typeof body.uptime === "number";
|
|
156
184
|
}
|
|
157
185
|
|
|
186
|
+
/**
|
|
187
|
+
* True for the exact 503 body the package-tree guard serves on `/healthz`. Shape only: this is
|
|
188
|
+
* what the listener CLAIMS, and whoever holds the port can claim it. Callers must attest it.
|
|
189
|
+
*/
|
|
190
|
+
export function isPackageTreeFencedHealthz(body: HealthzIdentity | null | undefined): boolean {
|
|
191
|
+
if (!body || body.service !== "opencodex" || body.status !== "restart_required") return false;
|
|
192
|
+
const error = body.error as { code?: unknown } | null | undefined;
|
|
193
|
+
return typeof error === "object" && error !== null && error.code === "package_tree_changed"
|
|
194
|
+
&& typeof body.pid === "number" && Number.isSafeInteger(body.pid) && body.pid > 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Prove that a fenced listener is the process this home's runtime record describes: the record
|
|
199
|
+
* must name the same pid and port and carry an attestation secret, and the listener must answer a
|
|
200
|
+
* fresh challenge with a proof bound to that secret, pid and port. The pid in the 503 body only
|
|
201
|
+
* selects which record to check; it is never accepted on its own.
|
|
202
|
+
*/
|
|
203
|
+
async function attestFencedIdentity(
|
|
204
|
+
url: string,
|
|
205
|
+
port: number,
|
|
206
|
+
pid: number,
|
|
207
|
+
io: LivenessIo,
|
|
208
|
+
fetchFn: LivenessFetch,
|
|
209
|
+
timeoutMs: number,
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
|
|
212
|
+
let record: ReturnType<NonNullable<LivenessIo["readRuntimeFn"]>>;
|
|
213
|
+
try {
|
|
214
|
+
record = readRuntimeFn(pid);
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
// The typed seam omits the secret; the production record (readRuntimePort) carries it.
|
|
219
|
+
const secret: unknown = record ? Reflect.get(record, "attestationSecret") : undefined;
|
|
220
|
+
if (!record || record.pid !== pid || record.port !== port || typeof secret !== "string") return false;
|
|
221
|
+
const challenge = (io.createChallengeFn ?? createLocalAttestationChallenge)();
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetchFn(url, {
|
|
224
|
+
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
|
|
225
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
226
|
+
});
|
|
227
|
+
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
228
|
+
// The second answer must still be the same fenced (or by now healthy) process.
|
|
229
|
+
if (!isOpencodexHealthz(body) && !isPackageTreeFencedHealthz(body)) return false;
|
|
230
|
+
if (body?.pid !== pid) return false;
|
|
231
|
+
return verifyLocalAttestationProof(secret, challenge, pid, port, res.headers.get(LOCAL_ATTESTATION_PROOF_HEADER));
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A bounded version string safe to carry beyond the untrusted health response. */
|
|
238
|
+
export function isHealthzVersion(value: unknown): value is string {
|
|
239
|
+
return typeof value === "string"
|
|
240
|
+
&& value.length <= 64
|
|
241
|
+
&& /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
242
|
+
}
|
|
243
|
+
|
|
158
244
|
/**
|
|
159
245
|
* "Nothing is listening" is narrower than "the probe failed". Only a connect-phase refusal
|
|
160
246
|
* proves the endpoint is free; a timeout, reset, or other transport failure leaves the
|
|
@@ -228,7 +314,7 @@ export async function proxyIdentityAt(
|
|
|
228
314
|
port: number,
|
|
229
315
|
opts: { hostname?: string; expectedPid?: number } = {},
|
|
230
316
|
io: LivenessIo = {},
|
|
231
|
-
): Promise<{ pid: number | null; version?: string; role?: string } | null> {
|
|
317
|
+
): Promise<{ pid: number | null; version?: string; role?: string; packageTreeFenced?: true } | null> {
|
|
232
318
|
const fetchFn = io.fetchFn ?? directLocalHttpFetch;
|
|
233
319
|
const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
234
320
|
const nowFn = io.nowFn ?? Date.now;
|
|
@@ -243,16 +329,33 @@ export async function proxyIdentityAt(
|
|
|
243
329
|
if (remainingMs <= 0) return null;
|
|
244
330
|
const timeoutMs = Math.min(baseTimeoutMs, remainingMs);
|
|
245
331
|
try {
|
|
246
|
-
const
|
|
332
|
+
const url = `http://${probeHostname(opts.hostname)}:${port}/healthz`;
|
|
333
|
+
const res = await fetchFn(url, {
|
|
247
334
|
signal: AbortSignal.timeout(timeoutMs),
|
|
248
335
|
});
|
|
336
|
+
if (!res.ok && io.acceptPackageTreeFenced && res.status === 503) {
|
|
337
|
+
const fenced = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
338
|
+
if (!isPackageTreeFencedHealthz(fenced)) return null;
|
|
339
|
+
const fencedPid = fenced!.pid as number;
|
|
340
|
+
if (opts.expectedPid !== undefined && fencedPid !== opts.expectedPid) return null;
|
|
341
|
+
const attestMs = io.deadlineAt === undefined ? baseTimeoutMs : Math.min(baseTimeoutMs, io.deadlineAt - nowFn());
|
|
342
|
+
if (attestMs <= 0) return null;
|
|
343
|
+
if (!(await attestFencedIdentity(url, port, fencedPid, io, fetchFn, attestMs))) return null;
|
|
344
|
+
const fencedVersion = isHealthzVersion(fenced?.version) ? fenced!.version as string : undefined;
|
|
345
|
+
return {
|
|
346
|
+
pid: fencedPid,
|
|
347
|
+
...(fencedVersion === undefined ? {} : { version: fencedVersion }),
|
|
348
|
+
packageTreeFenced: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
249
351
|
if (!res.ok) return null;
|
|
250
352
|
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
251
353
|
if (!isOpencodexHealthz(body)) return null;
|
|
252
354
|
const pid = typeof body?.pid === "number" ? body.pid : null;
|
|
253
355
|
if (opts.expectedPid !== undefined && pid !== null && pid !== opts.expectedPid) return null;
|
|
254
|
-
//
|
|
255
|
-
|
|
356
|
+
// Whoever holds the port controls this response. Only carry bounded semver text into
|
|
357
|
+
// diagnostics; dropping anything else prevents terminal controls reaching human output.
|
|
358
|
+
const version = isHealthzVersion(body?.version) ? body.version : undefined;
|
|
256
359
|
// Same guard for the role, for the same reason: absent on a standalone/hub proxy and on
|
|
257
360
|
// a legacy body, and never coerced from a non-string.
|
|
258
361
|
const role = typeof body?.role === "string" ? body.role : undefined;
|
|
@@ -325,6 +428,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
325
428
|
source: "runtime",
|
|
326
429
|
...(identity.version === undefined ? {} : { version: identity.version }),
|
|
327
430
|
...(identity.role === undefined ? {} : { role: identity.role }),
|
|
431
|
+
...(identity.packageTreeFenced ? { packageTreeFenced: true as const } : {}),
|
|
328
432
|
};
|
|
329
433
|
}
|
|
330
434
|
}
|
|
@@ -349,6 +453,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
349
453
|
source: "runtime",
|
|
350
454
|
...(identity.version === undefined ? {} : { version: identity.version }),
|
|
351
455
|
...(identity.role === undefined ? {} : { role: identity.role }),
|
|
456
|
+
...(identity.packageTreeFenced ? { packageTreeFenced: true as const } : {}),
|
|
352
457
|
};
|
|
353
458
|
}
|
|
354
459
|
}
|
|
@@ -365,6 +470,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
365
470
|
source: "config",
|
|
366
471
|
...(identity.version === undefined ? {} : { version: identity.version }),
|
|
367
472
|
...(identity.role === undefined ? {} : { role: identity.role }),
|
|
473
|
+
...(identity.packageTreeFenced ? { packageTreeFenced: true as const } : {}),
|
|
368
474
|
};
|
|
369
475
|
}
|
|
370
476
|
return null;
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
normalizeRequestFailureAttribution,
|
|
44
44
|
normalizeRequestSpend,
|
|
45
45
|
readRecentUsageEntries,
|
|
46
|
+
modelIdentityLogFields, recordObservedServedModel,
|
|
46
47
|
usageForFinalLog,
|
|
47
48
|
usageStatusForFinalLog,
|
|
48
49
|
usageTotalTokens,
|
|
@@ -168,8 +169,15 @@ export interface RequestLogContext {
|
|
|
168
169
|
/** Final-attempt tier summary; attempt rows remain the accounting source of truth. */
|
|
169
170
|
tierOutcome?: AttemptTierOutcome;
|
|
170
171
|
resolvedModel?: string;
|
|
172
|
+
/** Upstream served model, retained beside resolvedModel so an upstream reroute stays visible. */
|
|
173
|
+
servedModel?: string;
|
|
174
|
+
/** The exact model id sent upstream; recorded when a route/virtual rewrite makes it differ
|
|
175
|
+
* from the client-facing `model`, so a served-model mismatch can be judged against the wire. */
|
|
176
|
+
wireModel?: string;
|
|
171
177
|
/** Internal: client-facing response metadata must not replace the physical routed model. */
|
|
172
178
|
preserveResolvedModelFromRoute?: boolean;
|
|
179
|
+
/** Internal: client-facing selector written into response.model; never an upstream observation. */
|
|
180
|
+
responseModelEcho?: string;
|
|
173
181
|
usage?: OcxUsage;
|
|
174
182
|
usageLogInputTokens?: number;
|
|
175
183
|
/**
|
|
@@ -179,6 +187,8 @@ export interface RequestLogContext {
|
|
|
179
187
|
* is reserved up front and settlement corrects it.
|
|
180
188
|
*/
|
|
181
189
|
spendOutputCeilingTokens?: number;
|
|
190
|
+
/** Pre-send input estimate reserved for spend only; unlike usageLogInputTokens it never enters usage. */
|
|
191
|
+
spendInputEstimateTokens?: number;
|
|
182
192
|
/** Settles this request's durable spend entries from `addFinalRequestLog`. */
|
|
183
193
|
spendTracker?: RequestSpendSettlement;
|
|
184
194
|
attempts?: PersistedUsageAttempt[];
|
|
@@ -295,6 +305,10 @@ export interface RequestLogEntry {
|
|
|
295
305
|
responseServiceTier?: string;
|
|
296
306
|
tierOutcome?: AttemptTierOutcome;
|
|
297
307
|
resolvedModel?: string;
|
|
308
|
+
/** Model the upstream actually served (openai-model header or response body). */
|
|
309
|
+
servedModel?: string;
|
|
310
|
+
/** The exact model id sent upstream when it differs from the client-facing `model`. */
|
|
311
|
+
wireModel?: string;
|
|
298
312
|
status: number;
|
|
299
313
|
durationMs: number;
|
|
300
314
|
errorCode?: string;
|
|
@@ -444,7 +458,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
444
458
|
: {}),
|
|
445
459
|
...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}),
|
|
446
460
|
...(entry.tierOutcome ? { tierOutcome: entry.tierOutcome } : {}),
|
|
447
|
-
...(entry
|
|
461
|
+
...modelIdentityLogFields(entry),
|
|
448
462
|
status: entry.status,
|
|
449
463
|
durationMs: entry.durationMs,
|
|
450
464
|
...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
@@ -556,11 +570,17 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
556
570
|
// line-oriented viewer — while `usage.jsonl` looked clean, which is the worst shape for a
|
|
557
571
|
// sanitization bug because the safe surface is the one you check.
|
|
558
572
|
const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
|
|
573
|
+
const servedModel = modelIdentityLogFields(entry).servedModel;
|
|
559
574
|
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
|
|
560
|
-
const retained: RequestLogEntry = shadowCallRewrittenFrom === entry.shadowCallRewrittenFrom
|
|
575
|
+
const retained: RequestLogEntry = shadowCallRewrittenFrom === entry.shadowCallRewrittenFrom
|
|
576
|
+
&& servedModel === entry.servedModel && entry.claudeCompatibility === undefined
|
|
561
577
|
? entry
|
|
562
578
|
: { ...entry, ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}) };
|
|
563
579
|
if (!shadowCallRewrittenFrom && retained !== entry) delete retained.shadowCallRewrittenFrom;
|
|
580
|
+
if (!servedModel && retained !== entry) {
|
|
581
|
+
delete retained.servedModel;
|
|
582
|
+
if (retained.resolvedModel === entry.servedModel) delete retained.resolvedModel;
|
|
583
|
+
}
|
|
564
584
|
if (claudeCompatibility) retained.claudeCompatibility = claudeCompatibility;
|
|
565
585
|
else if (retained !== entry) delete retained.claudeCompatibility;
|
|
566
586
|
entry = retained;
|
|
@@ -598,6 +618,8 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
598
618
|
: {}),
|
|
599
619
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
600
620
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
621
|
+
...(entry.servedModel ? { servedModel: entry.servedModel } : {}),
|
|
622
|
+
...(entry.wireModel ? { wireModel: entry.wireModel } : {}),
|
|
601
623
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
602
624
|
...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
|
|
603
625
|
...(entry.shadowCallRewrittenFrom
|
|
@@ -881,12 +903,7 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
881
903
|
? (payload as { response?: unknown }).response
|
|
882
904
|
: payload;
|
|
883
905
|
if (!source || typeof source !== "object") return;
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
!logCtx.preserveResolvedModelFromRoute
|
|
887
|
-
&& typeof model === "string"
|
|
888
|
-
&& model.trim()
|
|
889
|
-
) logCtx.resolvedModel = model;
|
|
906
|
+
recordObservedServedModel(logCtx, (source as { model?: unknown }).model);
|
|
890
907
|
const serviceTier = (source as { service_tier?: unknown }).service_tier;
|
|
891
908
|
if (typeof serviceTier === "string" && serviceTier.trim()) {
|
|
892
909
|
const sanitized = sanitizeLogMetadataString(serviceTier);
|
|
@@ -1519,7 +1536,7 @@ export function addFinalRequestLog(
|
|
|
1519
1536
|
...((attempts?.at(-1)?.tierOutcome ?? logCtx.tierOutcome)
|
|
1520
1537
|
? { tierOutcome: attempts?.at(-1)?.tierOutcome ?? { ...logCtx.tierOutcome! } }
|
|
1521
1538
|
: {}),
|
|
1522
|
-
...(logCtx
|
|
1539
|
+
...modelIdentityLogFields(logCtx),
|
|
1523
1540
|
status: effectiveStatus,
|
|
1524
1541
|
durationMs,
|
|
1525
1542
|
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
@@ -38,6 +38,7 @@ export const REQUEST_METRICS_RECOVERY_CLASSES = Object.freeze([
|
|
|
38
38
|
"payload",
|
|
39
39
|
"empty_completion",
|
|
40
40
|
"effort_downgrade",
|
|
41
|
+
"fast_downgrade",
|
|
41
42
|
"other",
|
|
42
43
|
] as const);
|
|
43
44
|
|
|
@@ -143,6 +144,10 @@ const CAUSE_METRICS_CLASS = {
|
|
|
143
144
|
} as const satisfies Record<RequestFailureCause, RequestMetricsRecoveryClass>;
|
|
144
145
|
|
|
145
146
|
function recoveryClass(kind: AttemptRecoveryKind): RequestMetricsRecoveryClass {
|
|
147
|
+
// Both recoveries answer a rejected parameter, but an operator acts on them differently:
|
|
148
|
+
// an effort downgrade is a model/effort mismatch, a fast downgrade is a missing Anthropic
|
|
149
|
+
// fast-mode entitlement. Keep `effort_downgrade` meaning exactly what it always meant.
|
|
150
|
+
if (kind === "anthropic-fast-downgrade") return "fast_downgrade";
|
|
146
151
|
return CAUSE_METRICS_CLASS[causeForRecoveryKind(kind)];
|
|
147
152
|
}
|
|
148
153
|
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from "../../oauth/anthropic-routing";
|
|
41
41
|
import {
|
|
42
42
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
43
|
+
hasEligibleGenericOAuthFailoverTarget,
|
|
43
44
|
isGenericOAuthFailoverEnabled,
|
|
44
45
|
rotateGenericOAuthAccountOn429,
|
|
45
46
|
failoverAccountSnapshot,
|
|
@@ -431,9 +432,11 @@ export function createAdapterContinuations(
|
|
|
431
432
|
route.modelId,
|
|
432
433
|
)
|
|
433
434
|
: null;
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
if (!hop.allowed
|
|
435
|
+
// A roster quorum ignores cooldowns, so only attribute a budget refusal when the
|
|
436
|
+
// non-mutating selector confirms that an alternate account could serve this model now.
|
|
437
|
+
if (!hop.allowed && hasEligibleGenericOAuthFailoverTarget(
|
|
438
|
+
route.providerName, transportState.genericFailoverAccountId, Date.now(), route.modelId,
|
|
439
|
+
)) noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
|
|
437
440
|
if (!nextAccountId) hop.permit?.release();
|
|
438
441
|
if (nextAccountId) {
|
|
439
442
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
consoleGoUploadRejectionBody,
|
|
63
63
|
CONSOLE_GO_UPLOAD_RETRY_DELAY_MS,
|
|
64
64
|
reasoningEffortRejectionText,
|
|
65
|
+
anthropicFastRefused,
|
|
65
66
|
} from "./core-opaque-recovery";
|
|
66
67
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
67
68
|
import {
|
|
@@ -81,6 +82,7 @@ import {
|
|
|
81
82
|
} from "../../lib/errors";
|
|
82
83
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
83
84
|
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
85
|
+
import { chargeWorkflowSends } from "../../lib/workflow-budget";
|
|
84
86
|
|
|
85
87
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
86
88
|
export async function prepareAdapterExchange(
|
|
@@ -136,6 +138,7 @@ export async function prepareAdapterExchange(
|
|
|
136
138
|
| "sendBudgetExhausted"
|
|
137
139
|
| "reserveCredentialHop"
|
|
138
140
|
| "pendingHopPermit"
|
|
141
|
+
| "workflowRootId"
|
|
139
142
|
>,
|
|
140
143
|
) {
|
|
141
144
|
const { options, config, logCtx, req } = requestContext;
|
|
@@ -385,6 +388,8 @@ export async function prepareAdapterExchange(
|
|
|
385
388
|
// below for the same reason the two guards above do: a guard declared inside it is reset by
|
|
386
389
|
// every `continue recovery`, which would let one turn walk the whole ladder down.
|
|
387
390
|
const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
|
|
391
|
+
// At most one Anthropic fast-mode downgrade per request, for the same reason.
|
|
392
|
+
const anthropicFastDowngradeGuard: { attempted: boolean } = { attempted: false };
|
|
388
393
|
/**
|
|
389
394
|
* Rebuild the request from the current parsed input (and any image-tier bias) and refetch
|
|
390
395
|
* it once, tagging the attempt with the given recovery kind. Rebuilds are deterministic
|
|
@@ -400,6 +405,15 @@ export async function prepareAdapterExchange(
|
|
|
400
405
|
*/
|
|
401
406
|
onDispatch?: () => void,
|
|
402
407
|
): Promise<Response | { failed: Response }> => {
|
|
408
|
+
// The repair permit books the request ledger, but only the transient helper reports
|
|
409
|
+
// that send to the root workflow. The other two dispatch paths confirm it here, at
|
|
410
|
+
// their physical-send boundary, without booking the request ledger again.
|
|
411
|
+
let fastDowngradeWorkflowCharged = false;
|
|
412
|
+
const chargeFastDowngradeWorkflowSend = (): void => {
|
|
413
|
+
if (recovery !== "anthropic-fast-downgrade" || fastDowngradeWorkflowCharged) return;
|
|
414
|
+
fastDowngradeWorkflowCharged = true;
|
|
415
|
+
chargeWorkflowSends(sendBudgetState.workflowRootId, 1);
|
|
416
|
+
};
|
|
403
417
|
let retryRequest: AdapterRequest;
|
|
404
418
|
if (transportState.sameTargetRequest !== undefined && transportState.sameTargetParsed === parsed && transportState.sameTargetToken === transportState.transportToken) {
|
|
405
419
|
// Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
|
|
@@ -450,7 +464,10 @@ export async function prepareAdapterExchange(
|
|
|
450
464
|
abortSignal: upstream.signal,
|
|
451
465
|
timeoutMs: connectMs,
|
|
452
466
|
sendBudget: adapterDispatchBudget,
|
|
453
|
-
onPhysicalSend: send =>
|
|
467
|
+
onPhysicalSend: send => {
|
|
468
|
+
noteAdapterPhysicalSend(retryEstimate, send);
|
|
469
|
+
chargeFastDowngradeWorkflowSend();
|
|
470
|
+
},
|
|
454
471
|
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
455
472
|
stream: parsed.stream,
|
|
456
473
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
@@ -491,6 +508,7 @@ export async function prepareAdapterExchange(
|
|
|
491
508
|
// Same boundary on the helper path: the thunk is what reaches the wire, and it
|
|
492
509
|
// can be refused above before it does. use() past the first attempt is a no-op.
|
|
493
510
|
onDispatch?.();
|
|
511
|
+
if (!refetchTransientPolicy) chargeFastDowngradeWorkflowSend();
|
|
494
512
|
return fetchWithHeaderTimeout(retryRequest.url,
|
|
495
513
|
applyUpstreamRecoveryInit({
|
|
496
514
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
@@ -638,6 +656,52 @@ export async function prepareAdapterExchange(
|
|
|
638
656
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
639
657
|
}
|
|
640
658
|
|
|
659
|
+
// Anthropic fast mode refused (no usage credits, organization not enabled, model outside
|
|
660
|
+
// the lane, fast pool empty): resend once at standard speed, as Claude Code does. This
|
|
661
|
+
// sits before every 429 arm because the refusal says nothing about the account's standard
|
|
662
|
+
// lane; waiting, cooling, or rotating on it would punish a healthy credential. The
|
|
663
|
+
// resend is reserved and confirmed exactly like the generic OAuth hop below, and the
|
|
664
|
+
// decision lives on this request, so every later rebuild of it stays standard.
|
|
665
|
+
if (await anthropicFastRefused(
|
|
666
|
+
upstreamResponse,
|
|
667
|
+
transportState.sameTargetRequest,
|
|
668
|
+
transportState.activeAdapter.name,
|
|
669
|
+
anthropicFastDowngradeGuard.attempted,
|
|
670
|
+
upstream.signal,
|
|
671
|
+
)) {
|
|
672
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
673
|
+
const hop = reserveCredentialHop(
|
|
674
|
+
"repair",
|
|
675
|
+
`${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
|
|
676
|
+
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
677
|
+
);
|
|
678
|
+
if (hop.allowed) {
|
|
679
|
+
anthropicFastDowngradeGuard.attempted = true;
|
|
680
|
+
parsed.options.tierDecision = { kind: "drop" };
|
|
681
|
+
parsed.options.serviceTier = undefined;
|
|
682
|
+
if (parsed.options.tierObservation) {
|
|
683
|
+
parsed.options.tierObservation = { ...parsed.options.tierObservation, upstreamDeclinedFast: true };
|
|
684
|
+
}
|
|
685
|
+
invalidateSameTargetRequest();
|
|
686
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
687
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
688
|
+
let result: Response | { failed: Response };
|
|
689
|
+
try {
|
|
690
|
+
result = await rebuildAndRefetch("anthropic-fast-downgrade", () => {
|
|
691
|
+
if (!adapterOwnsDispatch) hop.permit?.use();
|
|
692
|
+
});
|
|
693
|
+
} finally {
|
|
694
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
695
|
+
}
|
|
696
|
+
if ("failed" in result) {
|
|
697
|
+
hop.permit?.release();
|
|
698
|
+
return result.failed;
|
|
699
|
+
}
|
|
700
|
+
upstreamResponse = result;
|
|
701
|
+
continue recovery;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
641
705
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
|
|
642
706
|
// 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
|
|
643
707
|
// so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
|
|
@@ -51,6 +51,7 @@ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSide
|
|
|
51
51
|
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
52
52
|
import {
|
|
53
53
|
applyCodexAuthContextToProvider,
|
|
54
|
+
callerCodexWorkspaceAccountId,
|
|
54
55
|
createCodexReserveDispatchGuard,
|
|
55
56
|
unwrapUpstreamRetryEvidenceError,
|
|
56
57
|
CodexMainProfileDrainingError,
|
|
@@ -186,6 +187,7 @@ import {
|
|
|
186
187
|
handleResponses,
|
|
187
188
|
preAuthUpstreamHostCircuitKey,
|
|
188
189
|
poolCredentialRefreshIncompleteResponse,
|
|
190
|
+
shouldRetryCodexScopedQuotaOnAlternate,
|
|
189
191
|
upstreamHostCircuitOpenResponse,
|
|
190
192
|
usesCodexForwardPoolAuth,
|
|
191
193
|
} from "./core";
|
|
@@ -709,6 +711,7 @@ export async function handleResponsesCompact(
|
|
|
709
711
|
captureOpenAiVirtualWirePolicy(route, virtual);
|
|
710
712
|
logCtx.model = virtual.selectedModelId;
|
|
711
713
|
logCtx.resolvedModel = virtual.wireModelId;
|
|
714
|
+
logCtx.wireModel = virtual.wireModelId;
|
|
712
715
|
} else {
|
|
713
716
|
logCtx.resolvedModel = route.modelId;
|
|
714
717
|
}
|
|
@@ -1211,9 +1214,36 @@ export async function handleResponsesCompact(
|
|
|
1211
1214
|
if (alternate && req.signal.aborted) {
|
|
1212
1215
|
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
1213
1216
|
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
1217
|
+
void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
|
|
1214
1218
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1215
1219
|
}
|
|
1216
|
-
|
|
1220
|
+
// The same scope binding the regular path applies: an organization-scoped
|
|
1221
|
+
// exhaustion refuses every credential in that workspace, so a proven
|
|
1222
|
+
// same-workspace alternate pays a cold prompt prefix for no new capacity.
|
|
1223
|
+
// Suppression is not silence — the buffered recorder below still attributes
|
|
1224
|
+
// the 429/402 to the account that produced it.
|
|
1225
|
+
const sharedWorkspaceScope = alternate != null
|
|
1226
|
+
&& !await shouldRetryCodexScopedQuotaOnAlternate(
|
|
1227
|
+
upstream,
|
|
1228
|
+
authCtx.chatgptAccountId,
|
|
1229
|
+
alternate.authCtx.kind === "pool" || alternate.authCtx.kind === "main-pool"
|
|
1230
|
+
? alternate.authCtx.chatgptAccountId
|
|
1231
|
+
: callerCodexWorkspaceAccountId(req.headers),
|
|
1232
|
+
req.signal,
|
|
1233
|
+
);
|
|
1234
|
+
// The scope check reads the rejection body asynchronously — the same window the
|
|
1235
|
+
// comment above covers. Re-check before the branch below records A, cancels its
|
|
1236
|
+
// body, and sends B for a caller that is gone.
|
|
1237
|
+
if (alternate && req.signal.aborted) {
|
|
1238
|
+
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
1239
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
1240
|
+
void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
|
|
1241
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1242
|
+
}
|
|
1243
|
+
if (alternate && sharedWorkspaceScope) {
|
|
1244
|
+
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
1245
|
+
}
|
|
1246
|
+
if (alternate && !sharedWorkspaceScope) {
|
|
1217
1247
|
// Same order the regular path uses (core.ts:349-357): a 429/402 carries the
|
|
1218
1248
|
// quota snapshot that produced it, so refresh A's cache before recording its
|
|
1219
1249
|
// rejection. Skipping this leaves quota-strategy routing and the dashboard
|
|
@@ -3,6 +3,8 @@ import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
|
3
3
|
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
4
4
|
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
5
|
import { resolveComboId } from "../../combos/identifiers";
|
|
6
|
+
import { resolvePolicyProfileId } from "../../routing/profile";
|
|
7
|
+
import { parseSyntheticRowId } from "../fast-row";
|
|
6
8
|
import { recallComboForLane } from "./combo-session-recall";
|
|
7
9
|
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
8
10
|
|
|
@@ -83,7 +85,7 @@ export function applyCompactionRoutingOverride(
|
|
|
83
85
|
if (trigger === undefined) return null;
|
|
84
86
|
|
|
85
87
|
const sourceModel = raw.model;
|
|
86
|
-
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
|
|
88
|
+
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceSelectorOf(config, sourceModel));
|
|
87
89
|
const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
|
|
88
90
|
raw.model = override.model.trim();
|
|
89
91
|
if (override.reasoningEffort !== undefined) {
|
|
@@ -92,19 +94,42 @@ export function applyCompactionRoutingOverride(
|
|
|
92
94
|
return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The selector a synthetic-row grammar actually routed on. `--fast`/`--effort` suffixes are
|
|
99
|
+
* decoration applied at ingress; identity checks must see the base id or a decorated
|
|
100
|
+
* virtual selector (`alias--fast`) slips past them.
|
|
101
|
+
*/
|
|
102
|
+
function sourceSelectorOf(config: OcxConfig, sourceModel: string): string {
|
|
103
|
+
const { fastRow, effortRow } = parseSyntheticRowId(sourceModel, config);
|
|
104
|
+
return fastRow?.baseId ?? effortRow?.baseId ?? sourceModel;
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
/** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
|
|
96
108
|
export function compactionRoutingKeepsProviderIdentity(
|
|
97
109
|
config: OcxConfig,
|
|
98
110
|
override: CompactionRoutingOverride,
|
|
99
111
|
route: RouteResult,
|
|
100
112
|
): boolean {
|
|
101
|
-
|
|
113
|
+
// `sourceModel` is the selector as the client sent it, so a synthetic `--fast` or
|
|
114
|
+
// effort suffix can still be attached. The base id is what the conversation routed on,
|
|
115
|
+
// and only the base can match the combo/policy guards below.
|
|
116
|
+
const sourceSelector = sourceSelectorOf(config, override.sourceModel);
|
|
117
|
+
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, sourceSelector)) return false;
|
|
118
|
+
// A policy selector does not identify one stable serving backend: its route depends on
|
|
119
|
+
// request evidence and live candidate state that this post-rewrite check no longer has.
|
|
120
|
+
// Treat it as crossing identity rather than reconstructing it through concrete routing,
|
|
121
|
+
// which deliberately bypasses policy evaluation and may fall through to defaultProvider.
|
|
122
|
+
if (resolvePolicyProfileId(config, sourceSelector) !== null) return false;
|
|
102
123
|
let source: RouteResult;
|
|
103
124
|
try {
|
|
104
|
-
source = routeConcreteModel(config,
|
|
125
|
+
source = routeConcreteModel(config, sourceSelector);
|
|
105
126
|
} catch {
|
|
106
127
|
return false;
|
|
107
128
|
}
|
|
129
|
+
// The default-provider branch is where every unrecognized selector lands — including a
|
|
130
|
+
// policy/combo alias that was renamed or deleted since the conversation began. Such a
|
|
131
|
+
// selector cannot prove which backend served it, so it can never match an identity.
|
|
132
|
+
if (source.routeReason === "default-provider") return false;
|
|
108
133
|
return source.providerName === route.providerName
|
|
109
134
|
&& source.codexAccountMode === route.codexAccountMode
|
|
110
135
|
&& source.codexAccountNamespace === route.codexAccountNamespace;
|