@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.
Files changed (148) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
  3. package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
  4. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  5. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  8. package/package.json +4 -3
  9. package/src/adapters/google-vertex-endpoint.ts +9 -1
  10. package/src/adapters/google-vertex-key-project.ts +223 -0
  11. package/src/adapters/google.ts +13 -3
  12. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  13. package/src/adapters/openai-chat.ts +2 -2
  14. package/src/adapters/openai-responses/request-strips.ts +60 -0
  15. package/src/adapters/registry.ts +5 -0
  16. package/src/adapters/workbuddy-schema.ts +61 -0
  17. package/src/adapters/workbuddy.ts +68 -0
  18. package/src/bridge/response-json.ts +1 -1
  19. package/src/bridge/sse.ts +1 -1
  20. package/src/chat/outbound.ts +11 -4
  21. package/src/cli/capabilities-agents-routing.ts +3 -3
  22. package/src/cli/codex-cli-update.ts +8 -0
  23. package/src/cli/combo-result.ts +6 -0
  24. package/src/cli/combo-stats.ts +3 -0
  25. package/src/cli/combo.ts +66 -42
  26. package/src/cli/launcher-context.ts +49 -2
  27. package/src/cli/link.ts +45 -32
  28. package/src/cli/provider.ts +2 -1
  29. package/src/cli/usage-compare.ts +68 -12
  30. package/src/client/link-ingress.ts +1 -1
  31. package/src/client/link-readiness.ts +97 -0
  32. package/src/client/link-relay-transport.ts +79 -11
  33. package/src/client/link-relay.ts +74 -15
  34. package/src/client/link-tunnel.ts +45 -18
  35. package/src/client/machine-listener.ts +20 -2
  36. package/src/client/runtime.ts +20 -2
  37. package/src/codex/account-store.ts +17 -3
  38. package/src/codex/app-server-restart-service.ts +6 -5
  39. package/src/codex/catalog/effort.ts +9 -2
  40. package/src/codex/catalog/gather-capture.ts +4 -2
  41. package/src/codex/catalog/provider-models.ts +14 -8
  42. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  43. package/src/codex/cli-install-provenance.ts +418 -0
  44. package/src/codex/cli-installation-targets.ts +5 -1
  45. package/src/codex/cli-update-plan.ts +43 -16
  46. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  47. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  48. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  49. package/src/codex/pool-refresh-backoff.ts +3 -1
  50. package/src/codex/user-identity.ts +35 -6
  51. package/src/codex/windows-current-sid.ts +162 -0
  52. package/src/combos/jev-quota-activation.ts +5 -1
  53. package/src/combos/jev-quota-route.ts +1 -1
  54. package/src/combos/jev-quota.ts +9 -1
  55. package/src/combos/jev-session-pin-config.ts +51 -0
  56. package/src/combos/jev.ts +6 -0
  57. package/src/combos/types.ts +19 -0
  58. package/src/config/provider-validation.ts +7 -0
  59. package/src/config/schema/leaf-validators.ts +1 -0
  60. package/src/generated/compatibility-version.json +194 -122
  61. package/src/lib/decision-quota-sink.ts +10 -0
  62. package/src/lib/translator-budget.ts +4 -4
  63. package/src/lib/upstream-retry.ts +4 -1
  64. package/src/link/diagnostics.ts +193 -0
  65. package/src/link/relay-auth.ts +6 -0
  66. package/src/link/status-projection.ts +39 -0
  67. package/src/link/supervisor.ts +45 -12
  68. package/src/link/tunnel-history.ts +109 -0
  69. package/src/link/tunnel-state.ts +80 -17
  70. package/src/messaging/remote-files.ts +24 -1
  71. package/src/oauth/generic-account-failover.ts +91 -7
  72. package/src/oauth/index.ts +15 -0
  73. package/src/oauth/model-discovery-auth.ts +5 -0
  74. package/src/oauth/store.ts +4 -1
  75. package/src/oauth/token-guardian.ts +7 -0
  76. package/src/oauth/types.ts +9 -0
  77. package/src/oauth/workbuddy.ts +139 -0
  78. package/src/providers/antigravity-quota-identity.ts +37 -0
  79. package/src/providers/base-url-choices.ts +8 -5
  80. package/src/providers/derive.ts +23 -3
  81. package/src/providers/model-discovery.ts +2 -0
  82. package/src/providers/model-rename-fields.ts +1 -0
  83. package/src/providers/quota/account-cache.ts +3 -2
  84. package/src/providers/quota/antigravity.ts +25 -14
  85. package/src/providers/quota/report-cache.ts +13 -4
  86. package/src/providers/quota/vendor-probes-key.ts +60 -0
  87. package/src/providers/quota/workbuddy.ts +83 -0
  88. package/src/providers/quota-decision-antigravity.ts +64 -0
  89. package/src/providers/quota-decision-publication.ts +11 -0
  90. package/src/providers/quota-decision-snapshot.ts +21 -4
  91. package/src/providers/quota-types.ts +2 -0
  92. package/src/providers/quota.ts +3 -1
  93. package/src/providers/registry/entries-core.ts +9 -0
  94. package/src/providers/registry/entries-extended.ts +20 -7
  95. package/src/providers/registry/model-ids.ts +3 -0
  96. package/src/providers/registry/model-seeds.ts +27 -28
  97. package/src/providers/registry/types.ts +19 -1
  98. package/src/providers/resolved-model-policy.ts +4 -4
  99. package/src/providers/routed-config.ts +15 -0
  100. package/src/providers/stale-model-roster-migration.ts +83 -15
  101. package/src/providers/workbuddy-credit-balance.ts +81 -0
  102. package/src/providers/workbuddy-models.ts +37 -0
  103. package/src/providers/workbuddy.ts +60 -0
  104. package/src/responses/reasoning-replay-cache.ts +18 -14
  105. package/src/router.ts +2 -0
  106. package/src/routing/analytics.ts +2 -3
  107. package/src/routing/fallback-policy.ts +3 -3
  108. package/src/server/auth-cors.ts +1 -0
  109. package/src/server/chat-completions.ts +14 -1
  110. package/src/server/chat-native-reasoning.ts +10 -3
  111. package/src/server/chat-native.ts +13 -2
  112. package/src/server/claude-messages.ts +3 -0
  113. package/src/server/index/link-relay-sessions.ts +9 -5
  114. package/src/server/index/optional-listeners.ts +4 -0
  115. package/src/server/local-management-read-client.ts +3 -1
  116. package/src/server/management/combo-routes.ts +10 -4
  117. package/src/server/management/link-routes.ts +5 -2
  118. package/src/server/management/provider-routes.ts +1 -1
  119. package/src/server/port-reclaim.ts +51 -9
  120. package/src/server/request-log-conversation.ts +49 -0
  121. package/src/server/request-log.ts +2 -0
  122. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  123. package/src/server/responses/compact.ts +10 -2
  124. package/src/server/responses/core-combo.ts +53 -31
  125. package/src/server/responses/core-lifetime.ts +2 -2
  126. package/src/server/responses/core-normalize.ts +6 -3
  127. package/src/server/responses/core-opaque-recovery.ts +1 -0
  128. package/src/server/responses/core-options.ts +5 -0
  129. package/src/server/responses/core-replay.ts +2 -1
  130. package/src/server/responses/jev-combo-choice.ts +39 -0
  131. package/src/server/responses/jev-session-pin.ts +203 -0
  132. package/src/server/responses/passthrough-delivery.ts +4 -0
  133. package/src/server/responses/request-prepare.ts +10 -5
  134. package/src/server/responses/request-transport.ts +25 -6
  135. package/src/server/responses/run-turn-execution.ts +2 -4
  136. package/src/server/responses/sidecar-execution.ts +2 -4
  137. package/src/server/responses-phase-inference.ts +267 -0
  138. package/src/server/sse-payload-rewrite.ts +14 -1
  139. package/src/service/definition.ts +20 -8
  140. package/src/types/config.ts +6 -0
  141. package/src/types/provider.ts +9 -1
  142. package/src/types/request.ts +1 -0
  143. package/src/types/wire.ts +1 -0
  144. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  145. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  146. package/src/usage/jev-stats.ts +54 -14
  147. package/gui/dist/assets/index-DGXS51sp.js +0 -94
  148. 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 = resolveContextPrincipal(req, config, options.admission);
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
- if (!logCtx.conversationId) {
612
- logCtx.conversationId = resolvedConversationId;
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 && proposedAccountId === oauthSelection?.accountId
984
- ? null : proposedAccountId;
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
- if (!logCtx.conversationId && parsed._cursorConversationId) {
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
- if (!logCtx.conversationId && parsed._cursorConversationId) {
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
- if (!logCtx.conversationId && parsed._cursorConversationId) {
596
- logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
597
- }
595
+ assignNativeLogConversationId(logCtx, normalizeLogConversationId(parsed._cursorConversationId));
598
596
  transportState.bindKeyUsageFromBridge(usage);
599
597
  },
600
598
  on429: rotateSidecarProviderOn429,