@bitkyc08/opencodex 2.90.0 → 2.92.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 +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in JEV session pin (#7525).
|
|
3
|
+
*
|
|
4
|
+
* A JEV Combo decides a target per request, so consecutive turns of one conversation can land on
|
|
5
|
+
* different providers and each switch starts with a cold provider prompt cache. With
|
|
6
|
+
* `sessionPin` set, a session keeps the target JEV chose for it while the session stays active.
|
|
7
|
+
*
|
|
8
|
+
* Identity. A pin is keyed by the admitted caller (the salted context principal of the matched
|
|
9
|
+
* data-plane key, or `loopback`), the Combo id and the explicit session lane from
|
|
10
|
+
* `sessionLaneIdFromRequest` (thread, session and parent-thread headers). A request without an
|
|
11
|
+
* explicit session identity never pins, and a body field such as `prompt_cache_key` is never an
|
|
12
|
+
* identity on its own.
|
|
13
|
+
*
|
|
14
|
+
* Lifetime. A pin is written only after the target JEV applied for this session completes a
|
|
15
|
+
* response, and it is refreshed by every later completion on that same target, including tool-loop
|
|
16
|
+
* continuations. It is released when the session stays idle past `ttlMs`, when any config
|
|
17
|
+
* generation change happens after it was written, when its target is not among the request's
|
|
18
|
+
* current JEV choices (disabled, removed, cooling down, quota exhausted, metered or otherwise
|
|
19
|
+
* ineligible), when its effort is no longer offered, when opted-in quota evidence reads
|
|
20
|
+
* `nearly_exhausted`, and when any target of a pinned session fails; a request that was already in
|
|
21
|
+
* flight when that failure was recorded cannot write the pin back. A cancelled send writes and
|
|
22
|
+
* releases nothing. Storage is process-local and bounded by `JEV_SESSION_PIN_CAPACITY`.
|
|
23
|
+
*/
|
|
24
|
+
import type { JevCandidate, JevDecision, JevDecisionBackend } from "../../combos/jev";
|
|
25
|
+
import { jevQuotaSignalForTarget } from "../../combos/jev-quota";
|
|
26
|
+
import { JEV_QUOTA_DEFAULT_TIERS, normalizeJevQuotaTiers, type JevQuotaTiers } from "../../combos/jev-quota-config";
|
|
27
|
+
import type { JevSessionPinRelease } from "../../combos/jev-session-pin-config";
|
|
28
|
+
import type { NormalizedComboConfig } from "../../combos/types";
|
|
29
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
30
|
+
import type { OcxComboDefaultEffort, OcxConfig } from "../../types";
|
|
31
|
+
import type { DataPlaneAdmission } from "../auth-cors";
|
|
32
|
+
|
|
33
|
+
export const JEV_SESSION_PIN_ROUTE_REASON = "jev-session-pin";
|
|
34
|
+
export const JEV_SESSION_PIN_CAPACITY = 512;
|
|
35
|
+
|
|
36
|
+
interface PinEntry {
|
|
37
|
+
targetKey: string;
|
|
38
|
+
effort: OcxComboDefaultEffort | null;
|
|
39
|
+
backend: JevDecisionBackend;
|
|
40
|
+
at: number;
|
|
41
|
+
generation: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pins = new Map<string, PinEntry>();
|
|
45
|
+
/**
|
|
46
|
+
* Failure fence per session key. `forget()` stamps the key with a fresh sequence number; a request
|
|
47
|
+
* that started before that stamp may no longer write the pin back, so a concurrent completion on
|
|
48
|
+
* the same session cannot restore a target another request just saw fail. Bounded like the pins;
|
|
49
|
+
* because an evicted stamp is unknowable, a writer whose key has no stamp is also refused once any
|
|
50
|
+
* stamp was evicted after it started.
|
|
51
|
+
*/
|
|
52
|
+
const failureFences = new Map<string, number>();
|
|
53
|
+
let failureSequence = 0;
|
|
54
|
+
/** `failureSequence` at the most recent fence eviction. */
|
|
55
|
+
let fenceEvictedAt = 0;
|
|
56
|
+
|
|
57
|
+
function callerNamespace(admission: DataPlaneAdmission | undefined): string {
|
|
58
|
+
if (!admission) return "unadmitted";
|
|
59
|
+
if (admission.kind === "loopback") return "loopback";
|
|
60
|
+
if (admission.contextPrincipalId) return `principal:${admission.contextPrincipalId}`;
|
|
61
|
+
return admission.kind === "configured" ? `key:${admission.keyId}` : admission.kind;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function write(key: string, entry: PinEntry): void {
|
|
65
|
+
// Insertion order is recency order; evicting from the front drops the least recent session.
|
|
66
|
+
pins.delete(key);
|
|
67
|
+
pins.set(key, entry);
|
|
68
|
+
while (pins.size > JEV_SESSION_PIN_CAPACITY) {
|
|
69
|
+
const oldest = pins.keys().next().value;
|
|
70
|
+
if (oldest === undefined) break;
|
|
71
|
+
pins.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fenceFailure(key: string): void {
|
|
76
|
+
failureFences.delete(key);
|
|
77
|
+
failureFences.set(key, ++failureSequence);
|
|
78
|
+
while (failureFences.size > JEV_SESSION_PIN_CAPACITY) {
|
|
79
|
+
const oldest = failureFences.keys().next().value;
|
|
80
|
+
if (oldest === undefined) break;
|
|
81
|
+
failureFences.delete(oldest);
|
|
82
|
+
fenceEvictedAt = failureSequence;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface JevSessionPinReuse {
|
|
87
|
+
/** The pinned decision to apply instead of asking the decision backend. */
|
|
88
|
+
decision?: JevDecision;
|
|
89
|
+
/** Set when a pin existed and was released instead of reused. */
|
|
90
|
+
released?: JevSessionPinRelease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface JevSessionPinScope {
|
|
94
|
+
/**
|
|
95
|
+
* Reuse the session's pin against this request's current JEV choices, or report its release.
|
|
96
|
+
* `quota` is passed only when the Combo opted into advisory quota evidence; it is read from
|
|
97
|
+
* loaded snapshots exactly as the decision payload would read it.
|
|
98
|
+
*/
|
|
99
|
+
reuse(
|
|
100
|
+
choices: readonly JevCandidate[],
|
|
101
|
+
quota?: { config: OcxConfig; tiers?: JevQuotaTiers },
|
|
102
|
+
): JevSessionPinReuse;
|
|
103
|
+
/**
|
|
104
|
+
* Record a completed response. With the request's applied JEV decision it (re)writes the pin
|
|
105
|
+
* when the completed target is the decided one; without one it only refreshes a pin that
|
|
106
|
+
* already names the completed target.
|
|
107
|
+
*/
|
|
108
|
+
complete(completedTargetKey: string, decision: JevDecision | undefined, writerGeneration: number): void;
|
|
109
|
+
/** Drop the session's pin after a real target failure. */
|
|
110
|
+
forget(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The pin scope for one Combo request, or undefined when this request can never pin. */
|
|
114
|
+
export function jevSessionPinScope(args: {
|
|
115
|
+
combo: Pick<NormalizedComboConfig, "strategy" | "sessionPin">;
|
|
116
|
+
comboId: string;
|
|
117
|
+
admission: DataPlaneAdmission | undefined;
|
|
118
|
+
lane: string | undefined;
|
|
119
|
+
now?: () => number;
|
|
120
|
+
}): JevSessionPinScope | undefined {
|
|
121
|
+
const ttlMs = args.combo.sessionPin?.ttlMs;
|
|
122
|
+
if (args.combo.strategy !== "jev" || ttlMs === undefined || !args.lane) return undefined;
|
|
123
|
+
const key = `${callerNamespace(args.admission)}\u0000${args.comboId}\u0000${args.lane}`;
|
|
124
|
+
const now = args.now ?? Date.now;
|
|
125
|
+
const fence = failureFences.get(key) ?? 0;
|
|
126
|
+
const startedAtSequence = failureSequence;
|
|
127
|
+
const release = (reason: JevSessionPinRelease): JevSessionPinReuse => {
|
|
128
|
+
pins.delete(key);
|
|
129
|
+
return { released: reason };
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
reuse(choices, quota) {
|
|
133
|
+
const entry = pins.get(key);
|
|
134
|
+
if (!entry) return {};
|
|
135
|
+
if (entry.generation !== captureConfigGeneration()) return release("config_changed");
|
|
136
|
+
const at = now();
|
|
137
|
+
if (at < entry.at || at - entry.at >= ttlMs) return release("expired");
|
|
138
|
+
const candidate = choices.find(choice => choice.key === entry.targetKey);
|
|
139
|
+
if (!candidate) return release("target_unavailable");
|
|
140
|
+
if (entry.effort !== null && !candidate.reasoningEfforts.includes(entry.effort)) {
|
|
141
|
+
return release("effort_unavailable");
|
|
142
|
+
}
|
|
143
|
+
if (quota && candidate.quotaIdentity !== null) {
|
|
144
|
+
const identity = candidate.quotaIdentity ?? candidate;
|
|
145
|
+
const tiers = normalizeJevQuotaTiers(quota.tiers) ?? JEV_QUOTA_DEFAULT_TIERS;
|
|
146
|
+
const signal = jevQuotaSignalForTarget(quota.config, identity.provider, identity.model, at, tiers);
|
|
147
|
+
if (signal.tier === "nearly_exhausted") return release("quota_nearly_exhausted");
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
decision: {
|
|
151
|
+
backend: entry.backend,
|
|
152
|
+
targetKey: entry.targetKey,
|
|
153
|
+
effort: entry.effort,
|
|
154
|
+
gate: "apply",
|
|
155
|
+
latencyMs: 0,
|
|
156
|
+
pinned: true,
|
|
157
|
+
pinReason: "session",
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
complete(completedTargetKey, decision, writerGeneration) {
|
|
162
|
+
// A config change while the response was in flight makes this completion obsolete.
|
|
163
|
+
if (writerGeneration !== captureConfigGeneration()) return;
|
|
164
|
+
// So does a failure on this session reported after this request started.
|
|
165
|
+
const currentFence = failureFences.get(key);
|
|
166
|
+
if ((currentFence ?? 0) !== fence) return;
|
|
167
|
+
if (currentFence === undefined && fenceEvictedAt > startedAtSequence) return;
|
|
168
|
+
if (!decision) {
|
|
169
|
+
const entry = pins.get(key);
|
|
170
|
+
if (entry && entry.targetKey === completedTargetKey && entry.generation === writerGeneration) {
|
|
171
|
+
write(key, { ...entry, at: now() });
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Only an applied decision pins; a fail-open fallback leaves the next turn to decide again.
|
|
176
|
+
if (decision.gate !== "apply" || decision.targetKey !== completedTargetKey) return;
|
|
177
|
+
write(key, {
|
|
178
|
+
targetKey: decision.targetKey,
|
|
179
|
+
effort: decision.effort,
|
|
180
|
+
backend: decision.backend,
|
|
181
|
+
at: now(),
|
|
182
|
+
generation: writerGeneration,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
forget() {
|
|
186
|
+
pins.delete(key);
|
|
187
|
+
fenceFailure(key);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Test-only reset, alongside the Combo recall and rotation resets. */
|
|
193
|
+
export function clearJevSessionPinsForTests(): void {
|
|
194
|
+
pins.clear();
|
|
195
|
+
failureFences.clear();
|
|
196
|
+
failureSequence = 0;
|
|
197
|
+
fenceEvictedAt = 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Test-only view of the retained pin count. */
|
|
201
|
+
export function jevSessionPinCountForTests(): number {
|
|
202
|
+
return pins.size;
|
|
203
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import { resolveComboId } from "../../combos";
|
|
3
|
-
import { isBareOpenAiFamilyModel, previewRouteModel } from "../../router";
|
|
3
|
+
import { isBareOpenAiFamilyModel, previewRouteModel, routeConcreteModel } from "../../router";
|
|
4
4
|
import { resolveChosenTarget } from "./shadow-target-availability";
|
|
5
|
-
import { getRoutingProfile, resolvePolicyProfileId } from "../../routing/profile";
|
|
5
|
+
import { getRoutingProfile, resolvePolicyProfileId, type NormalizedRoutingProfile } from "../../routing/profile";
|
|
6
6
|
import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
|
|
7
7
|
import {
|
|
8
8
|
getMainAccountHardLockStatus,
|
|
@@ -12,6 +12,7 @@ import { codexQuotaScopeForModel } from "../../codex/routing/health-store";
|
|
|
12
12
|
import { getEligiblePoolAccounts } from "../../codex/routing/selection";
|
|
13
13
|
import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
14
14
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
15
|
+
import { FallbackRefusedError, guardFallbackHop, type FallbackAdmission, type FallbackTarget } from "../../routing/fallback-policy";
|
|
15
16
|
/** Resolves to a synchronous ownership proof, or null when the caller does not own the locked main. */
|
|
16
17
|
export type MainHardLockCallerCheck = (sourceModel: string) => Promise<(() => boolean) | null>;
|
|
17
18
|
|
|
@@ -81,6 +82,7 @@ export async function applyMainHardLockFallbackOverride(
|
|
|
81
82
|
config: OcxConfig,
|
|
82
83
|
now?: number,
|
|
83
84
|
callerCheck?: MainHardLockCallerCheck,
|
|
85
|
+
admission?: FallbackAdmission,
|
|
84
86
|
): Promise<MainHardLockFallbackOverride | null> {
|
|
85
87
|
const parsed = rawModelOf(body);
|
|
86
88
|
if (!parsed) return null;
|
|
@@ -120,6 +122,21 @@ export async function applyMainHardLockFallbackOverride(
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
// OCX-11: the reroute is a proactive model hop; refused, the request is answered with the
|
|
126
|
+
// fallback refusal and the body is left untouched.
|
|
127
|
+
const hop = guardFallbackHop(config, {
|
|
128
|
+
site: "hard-lock",
|
|
129
|
+
route: parsed.model,
|
|
130
|
+
from: { provider: OPENAI_CODEX_PROVIDER_ID, model: parsed.model },
|
|
131
|
+
to: hardLockTargetOf(config, parsed.model, fallback.model),
|
|
132
|
+
trigger: "hard_lock",
|
|
133
|
+
admission,
|
|
134
|
+
...(now !== undefined ? { now } : {}),
|
|
135
|
+
});
|
|
136
|
+
if (!hop.ok) throw new FallbackRefusedError(hop.response);
|
|
137
|
+
// The rewrite is this site's admission: the body now names the fallback model.
|
|
138
|
+
hop.commit();
|
|
139
|
+
|
|
123
140
|
parsed.raw.model = fallback.model;
|
|
124
141
|
if (fallback.reasoningEffort !== undefined) {
|
|
125
142
|
const reasoning = parsed.raw.reasoning;
|
|
@@ -132,3 +149,38 @@ export async function applyMainHardLockFallbackOverride(
|
|
|
132
149
|
}
|
|
133
150
|
return { sourceModel: parsed.model, targetModel: fallback.model };
|
|
134
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The hop's destination for the fallback policy, read from config only. A routing profile picks
|
|
155
|
+
* its candidate from request-history evidence, and evaluating it here would open that index under
|
|
156
|
+
* OPENCODEX_HOME just to classify the hop (on Windows the open handle then outlives the request's
|
|
157
|
+
* home); the profile's own dispatch still evaluates it. Its declared candidates are classified
|
|
158
|
+
* instead, as the hop the profile could take: a candidate on another provider makes it a provider
|
|
159
|
+
* hop. Combos, account namespaces and concrete targets resolve from config alone.
|
|
160
|
+
*/
|
|
161
|
+
function hardLockTargetOf(config: OcxConfig, sourceModel: string, model: string): FallbackTarget {
|
|
162
|
+
const policyId = resolvePolicyProfileId(config, model);
|
|
163
|
+
const profile = policyId ? getRoutingProfile(config, policyId) : undefined;
|
|
164
|
+
if (profile) return profileHopTarget(config, sourceModel, profile);
|
|
165
|
+
try {
|
|
166
|
+
const route = previewRouteModel(config, model);
|
|
167
|
+
return { provider: route.providerName, model: route.modelId };
|
|
168
|
+
} catch {
|
|
169
|
+
const slash = model.indexOf("/");
|
|
170
|
+
return slash > 0 ? { provider: model.slice(0, slash), model: model.slice(slash + 1) } : { provider: "", model };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function profileHopTarget(config: OcxConfig, sourceModel: string, profile: NormalizedRoutingProfile): FallbackTarget {
|
|
175
|
+
const targets = profile.candidates.map((candidate): FallbackTarget => {
|
|
176
|
+
try {
|
|
177
|
+
const route = routeConcreteModel(config, candidate.provider + "/" + candidate.model);
|
|
178
|
+
return { provider: route.providerName, model: route.modelId };
|
|
179
|
+
} catch {
|
|
180
|
+
return { provider: candidate.provider, model: candidate.model };
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return targets.find(target => target.provider !== OPENAI_CODEX_PROVIDER_ID)
|
|
184
|
+
?? targets.find(target => target.model !== sourceModel)
|
|
185
|
+
?? { provider: OPENAI_CODEX_PROVIDER_ID, model: sourceModel };
|
|
186
|
+
}
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
createResponsesItemIdPayloadRewrite,
|
|
81
81
|
repairResponsesJsonItemIds,
|
|
82
82
|
} from "../responses-item-id-repair";
|
|
83
|
+
import { createResponsesPhaseInferenceBlockRewrite } from "../responses-phase-inference";
|
|
83
84
|
import {
|
|
84
85
|
payloadRewriteAsBlockRewrite,
|
|
85
86
|
composeSsePayloadRewrites,
|
|
@@ -851,6 +852,9 @@ export async function deliverPassthroughResponse(
|
|
|
851
852
|
snapshotRepairEnabled
|
|
852
853
|
? createResponsesSnapshotBlockRewrite(nativeExchange.outboundRequestBody, translatorBudget)
|
|
853
854
|
: undefined,
|
|
855
|
+
route.provider.messagePhaseInference === true
|
|
856
|
+
? createResponsesPhaseInferenceBlockRewrite(translatorBudget)
|
|
857
|
+
: undefined,
|
|
854
858
|
responseEffects.plaintextV2AgentMessageToolNames.size > 0
|
|
855
859
|
? payloadRewriteAsBlockRewrite(createPlaintextV2AgentMessageCallRestoreRewrite(
|
|
856
860
|
responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
|
|
@@ -13,6 +13,7 @@ import { resolvePolicyProfileId } from "../../routing/profile";
|
|
|
13
13
|
import { parseSyntheticRowId } from "../fast-row";
|
|
14
14
|
import { routeConcreteModel } from "../../router";
|
|
15
15
|
import { isPolicyCandidateRefusal, policyWireDestination, type PolicyRequestScope } from "./policy-request-scope";
|
|
16
|
+
import { fallbackTriggerForStatus, guardFallbackHop } from "../../routing/fallback-policy";
|
|
16
17
|
|
|
17
18
|
type CoreHandler = typeof handleResponsesCore;
|
|
18
19
|
type CoreOptions = Parameters<CoreHandler>[3];
|
|
@@ -25,6 +26,23 @@ function candidateKey(candidate: Pick<RouteCandidateTrace, "provider" | "model">
|
|
|
25
26
|
return `${candidate.provider}\u0000${candidate.model}`;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* OCX-11: the destination a candidate physically dispatches to, after provider aliases and
|
|
31
|
+
* blockedModelRedirects, so the fallback guard compares where requests really go. A candidate
|
|
32
|
+
* that does not resolve keeps its own selector.
|
|
33
|
+
*/
|
|
34
|
+
function resolvedPolicyTarget(
|
|
35
|
+
config: OcxConfig,
|
|
36
|
+
candidate: Pick<RouteCandidateTrace, "provider" | "model">,
|
|
37
|
+
): { provider: string; model: string } {
|
|
38
|
+
try {
|
|
39
|
+
const routed = routeConcreteModel(config, `${candidate.provider}/${candidate.model}`);
|
|
40
|
+
return { provider: routed.providerName, model: routed.modelId };
|
|
41
|
+
} catch {
|
|
42
|
+
return { provider: candidate.provider, model: candidate.model };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
function staysWithinPolicyAfterRedirect(
|
|
29
47
|
config: OcxConfig,
|
|
30
48
|
policyEligibility: ReadonlySet<string>,
|
|
@@ -219,12 +237,32 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
219
237
|
policyScope.triedDestinations.add(policyScope.preparedDestination ?? policyWireDestination({
|
|
220
238
|
providerName: initialTrace.selected.provider, modelId: initialTrace.selected.model,
|
|
221
239
|
}));
|
|
240
|
+
let current = resolvedPolicyTarget(config, initialTrace.selected);
|
|
241
|
+
const policyRoute = `policy/${initialTrace.profile!.id}`;
|
|
222
242
|
|
|
223
243
|
while (!storedPool401ReplayDispatched && await shouldHopPolicyCandidate(response, req.signal)) {
|
|
224
244
|
if (req.signal.aborted) return response;
|
|
225
245
|
const next = rankPolicyFallbackCandidates(initialTrace, tried)
|
|
226
246
|
.find(candidate => staysWithinPolicyAfterRedirect(config, policyEligibility, candidate, policyScope.triedDestinations));
|
|
227
247
|
if (!next) return response;
|
|
248
|
+
// OCX-11: the next candidate is a hop; the fallback policy decides before it is sent, on the
|
|
249
|
+
// redirected destination the guard above already validated.
|
|
250
|
+
const nextTarget = resolvedPolicyTarget(config, next);
|
|
251
|
+
const hop = guardFallbackHop(config, {
|
|
252
|
+
site: "routing-profile",
|
|
253
|
+
route: policyRoute,
|
|
254
|
+
aliases: [initialRequestedModel ?? initialTrace.requestedModel],
|
|
255
|
+
from: current,
|
|
256
|
+
to: nextTarget,
|
|
257
|
+
trigger: fallbackTriggerForStatus(response.status),
|
|
258
|
+
admission: options.admission,
|
|
259
|
+
failureStatus: response.status,
|
|
260
|
+
retryAfter: response.headers.get("retry-after"),
|
|
261
|
+
});
|
|
262
|
+
if (!hop.ok) {
|
|
263
|
+
try { await response.body?.cancel(); } catch { /* the refusal replaces this body */ }
|
|
264
|
+
return hop.response;
|
|
265
|
+
}
|
|
228
266
|
tried.add(candidateKey(next));
|
|
229
267
|
|
|
230
268
|
// Retain the failed response's log owner until a candidate actually replaces it. This is
|
|
@@ -243,7 +281,12 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
243
281
|
if (!Object.hasOwn(failureLog, key)) Reflect.deleteProperty(logCtx, key);
|
|
244
282
|
}
|
|
245
283
|
Object.assign(logCtx, failureLog);
|
|
246
|
-
} else
|
|
284
|
+
} else {
|
|
285
|
+
// Dispatched: only a candidate that was not locally skipped counts as a taken hop.
|
|
286
|
+
hop.commit();
|
|
287
|
+
response = nextResponse;
|
|
288
|
+
current = nextTarget;
|
|
289
|
+
}
|
|
247
290
|
if (policyScope.preparedDestination) policyScope.triedDestinations.add(policyScope.preparedDestination);
|
|
248
291
|
} catch (error) {
|
|
249
292
|
const overload = requestPacingOverloadResponse(error);
|
|
@@ -30,6 +30,9 @@ import { recallComboForLane } from "./combo-session-recall";
|
|
|
30
30
|
import {
|
|
31
31
|
sessionLaneIdFromRequest,
|
|
32
32
|
conversationIdFromResponsesRequest,
|
|
33
|
+
assignNativeLogConversationId,
|
|
34
|
+
fillHermesLogConversationFallback,
|
|
35
|
+
nativeLogConversationIdFromHeaders,
|
|
33
36
|
sessionIdHeaderFromRequest,
|
|
34
37
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
35
38
|
} from "../request-log-conversation";
|
|
@@ -160,6 +163,7 @@ import {
|
|
|
160
163
|
import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
161
164
|
import { applyCompactionRoutingOverride, compactionRoutingKeepsProviderIdentity } from "./compaction-routing";
|
|
162
165
|
import { applyMainHardLockFallbackOverride } from "./main-hard-lock-routing";
|
|
166
|
+
import { FallbackRefusedError } from "../../routing/fallback-policy";
|
|
163
167
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
164
168
|
import {
|
|
165
169
|
conversationStateBindingFromAuth,
|
|
@@ -367,10 +371,11 @@ export async function prepareResponsesRequest(
|
|
|
367
371
|
&& !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
|
|
368
372
|
try {
|
|
369
373
|
options.mainHardLockFallbackOverride = await applyMainHardLockFallbackOverride(
|
|
370
|
-
body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options),
|
|
374
|
+
body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options), options.admission,
|
|
371
375
|
);
|
|
372
376
|
} catch (error) {
|
|
373
377
|
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
|
|
378
|
+
if (error instanceof FallbackRefusedError) return error.response;
|
|
374
379
|
const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now() });
|
|
375
380
|
if (mapped) return mapped;
|
|
376
381
|
throw error;
|
|
@@ -601,14 +606,16 @@ export async function prepareResponsesRequest(
|
|
|
601
606
|
// would become enough to read another caller's retained search result. Without a principal
|
|
602
607
|
// bridgeSearchReplayScope yields no scope, so nothing is recorded or restored for it. The
|
|
603
608
|
// field is always rewritten so an absent principal also clears one a reused holder carried.
|
|
604
|
-
const clientPrincipalId =
|
|
609
|
+
const clientPrincipalId = options.replayPrincipal
|
|
610
|
+
? options.replayPrincipal.clientPrincipalId
|
|
611
|
+
: resolveContextPrincipal(req, config, options.admission);
|
|
605
612
|
parsed._reasoningReplayScope = { ...parsed._reasoningReplayScope, clientPrincipalId };
|
|
606
613
|
}
|
|
607
614
|
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
608
|
-
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
615
|
+
// absent or synthetically injected (session_id from prompt_cache_key). A Hermes-only id
|
|
616
|
+
// is a log fallback: native resolution replaces it, and it only fills an empty slot.
|
|
617
|
+
assignNativeLogConversationId(logCtx, resolvedConversationId ?? nativeLogConversationIdFromHeaders(req.headers));
|
|
618
|
+
fillHermesLogConversationFallback(logCtx, req.headers);
|
|
612
619
|
// A rewritten turn logs the selector the client asked for, so the usage log never
|
|
613
620
|
// claims the operator typed the fallback model themselves.
|
|
614
621
|
logCtx.requestedModel = options.mainHardLockFallbackOverride?.sourceModel
|
|
@@ -1005,7 +1012,9 @@ export async function prepareResponsesRequest(
|
|
|
1005
1012
|
subagentFallbackModelEligibleAccountIdsForModel,
|
|
1006
1013
|
fallbackChain,
|
|
1007
1014
|
candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
|
|
1015
|
+
options.admission,
|
|
1008
1016
|
);
|
|
1017
|
+
if (fallback?.refusal) return fallback.refusal;
|
|
1009
1018
|
if (fallback) {
|
|
1010
1019
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
1011
1020
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -1219,11 +1228,15 @@ export async function prepareResponsesRequest(
|
|
|
1219
1228
|
recoverySelectionOptions,
|
|
1220
1229
|
subagentFallbackAccountPreview,
|
|
1221
1230
|
subagentFallbackModelEligibleAccountIdsForModel,
|
|
1231
|
+
undefined,
|
|
1232
|
+
undefined,
|
|
1233
|
+
options.admission,
|
|
1222
1234
|
);
|
|
1223
1235
|
} finally {
|
|
1224
1236
|
recoverySelectionAdmission?.release();
|
|
1225
1237
|
}
|
|
1226
1238
|
})();
|
|
1239
|
+
if (fallback?.refusal) return fallback.refusal;
|
|
1227
1240
|
if (fallback) {
|
|
1228
1241
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
1229
1242
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -41,12 +41,14 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST,
|
|
43
43
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
44
|
+
antigravityModelRefusal,
|
|
44
45
|
eligibleFailoverAccounts,
|
|
45
46
|
forgetGenericFailoverRoster,
|
|
46
47
|
isGenericFailoverProvider,
|
|
47
48
|
isProactivePreferenceEnabled,
|
|
48
49
|
preferredInitialAccount,
|
|
49
50
|
refusalAwareInitialKiroAccount,
|
|
51
|
+
routeAntigravityModel,
|
|
50
52
|
noteGenericPoolSelection,
|
|
51
53
|
} from "../../oauth/generic-account-failover";
|
|
52
54
|
import { admitGenericSessionAccount, bindGenericSessionAccount, genericSessionAccount, genericSessionAccountEligible, genericSessionAffinityEnabled, preferredGenericSessionAccount } from "../../oauth/generic-session-affinity";
|
|
@@ -79,7 +81,7 @@ import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuo
|
|
|
79
81
|
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
80
82
|
import { formatErrorResponse } from "../../bridge";
|
|
81
83
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
82
|
-
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
84
|
+
import { assignNativeLogConversationId, sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
83
85
|
import { redactSecretString } from "../../lib/redact";
|
|
84
86
|
import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
|
|
85
87
|
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
@@ -183,6 +185,7 @@ export async function prepareResponsesTransport(
|
|
|
183
185
|
|| route.providerName === "orcarouter-oauth"
|
|
184
186
|
// runTurn transport: the replay runs on the first-event preflight in run-turn-execution.
|
|
185
187
|
|| route.providerName === "devin"
|
|
188
|
+
|| route.providerName === "workbuddy-ai"
|
|
186
189
|
|| route.providerName === "dsh-account"
|
|
187
190
|
) && route.provider.authMode === "oauth";
|
|
188
191
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
@@ -244,6 +247,9 @@ export async function prepareResponsesTransport(
|
|
|
244
247
|
if (candidate.provider !== route.providerName) return null;
|
|
245
248
|
if (anthropicInstance) anthropicRouteDecision = currentAnthropicRouteDecision();
|
|
246
249
|
if (anthropicRouteDecision && !routeCandidates(anthropicRouting!.getEligibleAnthropicAccounts(Date.now(), route.modelId), anthropicRouteDecision).includes(candidate.accountId)) return null;
|
|
250
|
+
// #7500: no admission, initial or a recovery/failover rebind, may land on an Antigravity
|
|
251
|
+
// account whose own roster omits the model. Callers abandon the send on null.
|
|
252
|
+
if (antigravityModelRefusal(config, route.providerName, candidate.accountId, route.modelId)) return null;
|
|
247
253
|
const committed = genericAffinityMode
|
|
248
254
|
? admitGenericSessionAccount(config, route.providerName, oauthSelection, candidate, route.modelId, genericAffinityMode)
|
|
249
255
|
: await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
|
|
@@ -485,6 +491,10 @@ export async function prepareResponsesTransport(
|
|
|
485
491
|
parsed._kiroAuthContext = kiroContext;
|
|
486
492
|
if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
|
|
487
493
|
}
|
|
494
|
+
if (route.providerName === "workbuddy-ai") {
|
|
495
|
+
parsed._workbuddyAuthContext = snapshot.workbuddy;
|
|
496
|
+
if (retryParsed !== parsed) retryParsed._workbuddyAuthContext = snapshot.workbuddy;
|
|
497
|
+
}
|
|
488
498
|
if (route.providerName === "zed") {
|
|
489
499
|
// Zed signs with its own user id; `snapshot.accountId` is the local slot hash.
|
|
490
500
|
const zedContext = { userId: snapshot.providerUserId ?? "" };
|
|
@@ -978,10 +988,16 @@ export async function prepareResponsesTransport(
|
|
|
978
988
|
?? refusalAwareId ?? (isGenericFailoverProvider(route.providerName, route.provider)
|
|
979
989
|
? preferredInitialAccount(config, route.providerName, Date.now(), route.modelId)
|
|
980
990
|
: null);
|
|
991
|
+
// #7500: an Antigravity account whose own discovery omits the model is never sent to.
|
|
992
|
+
// A usable listing (or not-yet-discovered) sibling replaces it, or the request is refused.
|
|
993
|
+
const modelRoute = routeAntigravityModel(config, route.providerName,
|
|
994
|
+
proposedAccountId ?? oauthSelection?.accountId, route.modelId);
|
|
995
|
+
if (modelRoute.kind === "refuse") return formatErrorResponse(403, "permission_error", modelRoute.message);
|
|
996
|
+
const routedAccountId = modelRoute.kind === "reroute" ? modelRoute.accountId : proposedAccountId;
|
|
981
997
|
// An unchanged active choice must retain the ordinary resolver's terminal-refresh
|
|
982
998
|
// recovery (not the speculative-preference fallback that resolves the same dead grant).
|
|
983
|
-
const preferredAccountId = genericAffinityMode &&
|
|
984
|
-
? null :
|
|
999
|
+
const preferredAccountId = genericAffinityMode && routedAccountId === oauthSelection?.accountId
|
|
1000
|
+
? null : routedAccountId;
|
|
985
1001
|
let safetyAlternateId: string | null = !affinityAccountId && preferredAccountId === refusalAwareId ? refusalAwareId : null;
|
|
986
1002
|
// Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
|
|
987
1003
|
// rotation site, and rotation sites must apply their credential through
|
|
@@ -1041,6 +1057,10 @@ export async function prepareResponsesTransport(
|
|
|
1041
1057
|
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
1042
1058
|
usedPreferredAccount = false;
|
|
1043
1059
|
}
|
|
1060
|
+
// Resolution fallbacks can still land on an omitting account; answer with the refusal
|
|
1061
|
+
// here, since the commit below would only report it as a changed selection.
|
|
1062
|
+
const unlisted = antigravityModelRefusal(config, route.providerName, resolved.accountId, route.modelId);
|
|
1063
|
+
if (unlisted) return formatErrorResponse(403, "permission_error", unlisted);
|
|
1044
1064
|
const admitted = await commitResolvedOAuthSelection(resolved, safetyAlternateId === null);
|
|
1045
1065
|
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
1046
1066
|
if (safetyAlternateId && admitted.accountId !== safetyAlternateId)
|
|
@@ -1104,6 +1124,7 @@ export async function prepareResponsesTransport(
|
|
|
1104
1124
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
1105
1125
|
parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
|
|
1106
1126
|
}
|
|
1127
|
+
if (route.providerName === "workbuddy-ai") parsed._workbuddyAuthContext = resolved.workbuddy;
|
|
1107
1128
|
if (route.providerName === "zed") {
|
|
1108
1129
|
parsed._zedAuthContext = { userId: resolved.providerUserId ?? "" };
|
|
1109
1130
|
}
|
|
@@ -1198,9 +1219,7 @@ export async function prepareResponsesTransport(
|
|
|
1198
1219
|
codexAuthContext: admissionState.authCtx,
|
|
1199
1220
|
forwardHeaders: requestState.selectedForwardHeaders,
|
|
1200
1221
|
});
|
|
1201
|
-
|
|
1202
|
-
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
1203
|
-
}
|
|
1222
|
+
assignNativeLogConversationId(logCtx, normalizeLogConversationId(parsed._cursorConversationId));
|
|
1204
1223
|
logCtx.providerAdapter = adapter.name;
|
|
1205
1224
|
// Ordinary requests receive one durable attempt only after their final initial
|
|
1206
1225
|
// adapter is resolved. Combo children own their attempt and retries keep it.
|
|
@@ -19,7 +19,7 @@ import { releaseProviderRequestSlot, waitForProviderRequestSlot, RequestPacingQu
|
|
|
19
19
|
import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
20
20
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
21
21
|
import { providerFetch } from "./fetch-helpers";
|
|
22
|
-
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
22
|
+
import { assignNativeLogConversationId, normalizeLogConversationId } from "../request-log-conversation";
|
|
23
23
|
import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
|
|
24
24
|
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE, type OcxErrorPayload } from "../../lib/errors";
|
|
25
25
|
import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
@@ -406,9 +406,7 @@ export async function executeResponsesRunTurn(
|
|
|
406
406
|
releaseProviderRequestSlot(pacingSlot);
|
|
407
407
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
408
408
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
409
|
-
|
|
410
|
-
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
411
|
-
}
|
|
409
|
+
assignNativeLogConversationId(logCtx, normalizeLogConversationId(parsed._cursorConversationId));
|
|
412
410
|
targetQueue.close();
|
|
413
411
|
}
|
|
414
412
|
};
|
|
@@ -51,7 +51,7 @@ import { namespacedToolName } from "../../types";
|
|
|
51
51
|
import { providerFetch } from "./fetch-helpers";
|
|
52
52
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
53
53
|
import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
54
|
-
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
54
|
+
import { assignNativeLogConversationId, normalizeLogConversationId } from "../request-log-conversation";
|
|
55
55
|
import { attemptAccountChanged } from "../request-log-account-rotation";
|
|
56
56
|
import { rememberResponseState } from "../../responses/state";
|
|
57
57
|
import { trackStreamLifetime } from "../lifecycle";
|
|
@@ -592,9 +592,7 @@ export async function executeResponsesSidecars(
|
|
|
592
592
|
// Cursor may assign _cursorConversationId inside the image loop's first runTurn;
|
|
593
593
|
// backfill so Logs can filter/total that opening request (parity with the normal
|
|
594
594
|
// runTurn branch).
|
|
595
|
-
|
|
596
|
-
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
597
|
-
}
|
|
595
|
+
assignNativeLogConversationId(logCtx, normalizeLogConversationId(parsed._cursorConversationId));
|
|
598
596
|
transportState.bindKeyUsageFromBridge(usage);
|
|
599
597
|
},
|
|
600
598
|
on429: rotateSidecarProviderOn429,
|