@bitkyc08/opencodex 2.91.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-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-BuLy7hL1.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/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/request-strips.ts +60 -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/outbound.ts +11 -4
- package/src/cli/capabilities-agents-routing.ts +3 -3
- package/src/cli/codex-cli-update.ts +8 -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/launcher-context.ts +49 -2
- package/src/cli/link.ts +45 -32
- package/src/cli/provider.ts +2 -1
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +79 -11
- package/src/client/link-relay.ts +74 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +20 -2
- package/src/client/runtime.ts +20 -2
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- 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/pool-refresh-backoff.ts +3 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -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/types.ts +19 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +194 -122
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/diagnostics.ts +193 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/status-projection.ts +39 -0
- 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 +15 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/store.ts +4 -1
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- 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/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 +25 -14
- 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/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 +3 -3
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +14 -1
- 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/index/link-relay-sessions.ts +9 -5
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/link-routes.ts +5 -2
- package/src/server/management/provider-routes.ts +1 -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/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +10 -2
- package/src/server/responses/core-combo.ts +53 -31
- 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/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/request-prepare.ts +10 -5
- 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/sse-payload-rewrite.ts +14 -1
- package/src/service/definition.ts +20 -8
- package/src/types/config.ts +6 -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/index-DGXS51sp.js +0 -94
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
|
@@ -128,6 +128,11 @@ export interface HandleResponsesOptions {
|
|
|
128
128
|
* which is why it refused the whole env_key flow instead of serving it.
|
|
129
129
|
*/
|
|
130
130
|
admission?: DataPlaneAdmission;
|
|
131
|
+
/**
|
|
132
|
+
* Internal caller identity captured before Combo strips provisional credentials. An existing
|
|
133
|
+
* holder with no principal preserves keyless replay; never populate it from client data.
|
|
134
|
+
*/
|
|
135
|
+
replayPrincipal?: Readonly<{ clientPrincipalId: string | undefined }>;
|
|
131
136
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
132
137
|
onRequestBodyRead?: () => void;
|
|
133
138
|
/** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
|
|
@@ -344,6 +344,7 @@ function routeReasoningReplayIdentity(args: {
|
|
|
344
344
|
export function mandatoryResponsesReasoningReplayUnavailable(args: {
|
|
345
345
|
body: unknown;
|
|
346
346
|
clientThreadId: string | undefined;
|
|
347
|
+
clientPrincipalId?: string;
|
|
347
348
|
providerName: string;
|
|
348
349
|
provider: OcxProviderConfig;
|
|
349
350
|
adapterName: string;
|
|
@@ -359,7 +360,7 @@ export function mandatoryResponsesReasoningReplayUnavailable(args: {
|
|
|
359
360
|
) return false;
|
|
360
361
|
|
|
361
362
|
const current = routeReasoningReplayIdentity(args);
|
|
362
|
-
return reasoningReplayServingIdentityChanged({ clientThreadId, current });
|
|
363
|
+
return reasoningReplayServingIdentityChanged({ clientThreadId, clientPrincipalId: args.clientPrincipalId, current });
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getCombo, targetKey, type JevCandidate } from "../../combos";
|
|
2
|
+
import { isCodexReasoningEffort } from "../../reasoning-effort";
|
|
3
|
+
import { routeConcreteModel } from "../../router";
|
|
4
|
+
import { supportedLadderFor } from "../effort-policy";
|
|
5
|
+
import type { OcxComboDefaultEffort, OcxConfig } from "../../types";
|
|
6
|
+
|
|
7
|
+
/** A JEV target's effort choices plus, when quota signals are on, its resolved native identity. */
|
|
8
|
+
export interface JevChoiceIdentity {
|
|
9
|
+
reasoningEfforts: OcxComboDefaultEffort[];
|
|
10
|
+
quotaIdentity?: JevCandidate["quotaIdentity"];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
|
|
14
|
+
export function jevChoiceEfforts(
|
|
15
|
+
config: OcxConfig,
|
|
16
|
+
target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
|
|
17
|
+
quotaSignals: boolean,
|
|
18
|
+
): JevChoiceIdentity | undefined {
|
|
19
|
+
let ladder: string[] | undefined;
|
|
20
|
+
// null tells the quota reader the route did not resolve, so it reads unknown rather than the configured alias.
|
|
21
|
+
let quotaIdentity: JevCandidate["quotaIdentity"] = quotaSignals ? null : undefined;
|
|
22
|
+
try {
|
|
23
|
+
const route = routeConcreteModel(config, targetKey(target));
|
|
24
|
+
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
25
|
+
if (quotaSignals) quotaIdentity = { provider: route.providerName, model: route.modelId };
|
|
26
|
+
} catch {
|
|
27
|
+
// Preserve the existing routing-failure surface. Unknown capability becomes the explicit
|
|
28
|
+
// no-effort choice rather than broadening JEV's effort allowlist.
|
|
29
|
+
ladder = undefined;
|
|
30
|
+
}
|
|
31
|
+
const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
|
|
32
|
+
const configuredEfforts = target.reasoningEfforts;
|
|
33
|
+
const reasoningEfforts = configuredEfforts === undefined
|
|
34
|
+
? supportedEfforts
|
|
35
|
+
: configuredEfforts.filter(effort => supportedEfforts.includes(effort));
|
|
36
|
+
// An explicit allowlist is restrictive. If catalog capabilities drift until no configured
|
|
37
|
+
// effort remains supported, omit the target instead of silently broadening JEV's choices.
|
|
38
|
+
return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : { reasoningEfforts, quotaIdentity };
|
|
39
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
@@ -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";
|
|
@@ -603,14 +606,16 @@ export async function prepareResponsesRequest(
|
|
|
603
606
|
// would become enough to read another caller's retained search result. Without a principal
|
|
604
607
|
// bridgeSearchReplayScope yields no scope, so nothing is recorded or restored for it. The
|
|
605
608
|
// field is always rewritten so an absent principal also clears one a reused holder carried.
|
|
606
|
-
const clientPrincipalId =
|
|
609
|
+
const clientPrincipalId = options.replayPrincipal
|
|
610
|
+
? options.replayPrincipal.clientPrincipalId
|
|
611
|
+
: resolveContextPrincipal(req, config, options.admission);
|
|
607
612
|
parsed._reasoningReplayScope = { ...parsed._reasoningReplayScope, clientPrincipalId };
|
|
608
613
|
}
|
|
609
614
|
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
610
|
-
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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);
|
|
614
619
|
// A rewritten turn logs the selector the client asked for, so the usage log never
|
|
615
620
|
// claims the operator typed the fallback model themselves.
|
|
616
621
|
logCtx.requestedModel = options.mainHardLockFallbackOverride?.sourceModel
|
|
@@ -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,
|