@bitkyc08/opencodex 2.88.0 → 2.89.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 (205) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
  7. package/gui/dist/assets/{quota-summary-D8y2wTsB.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/coding-agent/protocol.ts +6 -0
  14. package/src/adapters/google-http.ts +3 -2
  15. package/src/adapters/google-vertex-endpoint.ts +67 -0
  16. package/src/adapters/google.ts +35 -29
  17. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  18. package/src/adapters/openai-responses/passthrough.ts +17 -12
  19. package/src/adapters/openai-responses/reasoning.ts +68 -1
  20. package/src/adapters/registry.ts +1 -1
  21. package/src/adapters/upstream-http-error.ts +3 -2
  22. package/src/adapters/zed.ts +7 -4
  23. package/src/bridge/internal.ts +11 -1
  24. package/src/bridge/response-json.ts +2 -2
  25. package/src/bridge/sse.ts +10 -4
  26. package/src/claude/inbound.ts +12 -26
  27. package/src/claude/outbound.ts +5 -0
  28. package/src/cli/account-api.ts +13 -1
  29. package/src/cli/account-extended.ts +17 -5
  30. package/src/cli/account-key-quota.ts +4 -1
  31. package/src/cli/account.ts +1 -1
  32. package/src/cli/capabilities-agents-routing.ts +45 -8
  33. package/src/cli/capabilities-base.ts +5 -5
  34. package/src/cli/capabilities-provider-models.ts +5 -4
  35. package/src/cli/combo-result.ts +8 -2
  36. package/src/cli/combo.ts +17 -0
  37. package/src/cli/help.ts +1 -1
  38. package/src/cli/message-command.ts +4 -1
  39. package/src/cli/message-remote-args.ts +69 -0
  40. package/src/cli/message-remote-command.ts +77 -0
  41. package/src/cli/provider-runtime.ts +69 -1
  42. package/src/cli/registry.ts +3 -3
  43. package/src/codex/account-pause.ts +6 -1
  44. package/src/codex/account-runtime-state.ts +5 -0
  45. package/src/codex/account-store.ts +30 -3
  46. package/src/codex/auth-api/account-list.ts +3 -0
  47. package/src/codex/auth-api/main-account-probe.ts +3 -2
  48. package/src/codex/auth-context.ts +9 -4
  49. package/src/codex/catalog/aggregation.ts +15 -0
  50. package/src/codex/catalog/auto-review.ts +2 -1
  51. package/src/codex/catalog/build-entries.ts +17 -7
  52. package/src/codex/catalog/derive-entry.ts +3 -1
  53. package/src/codex/catalog/metadata.ts +2 -0
  54. package/src/codex/catalog/model-visibility.ts +0 -2
  55. package/src/codex/catalog/ownership.ts +71 -0
  56. package/src/codex/catalog/parsing.ts +12 -3
  57. package/src/codex/catalog/retained-sync.ts +2 -1
  58. package/src/codex/catalog/routed-removal.ts +4 -4
  59. package/src/codex/convergence.ts +2 -1
  60. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  61. package/src/codex/low-quota-protection.ts +57 -31
  62. package/src/codex/main-account-cache.ts +77 -3
  63. package/src/codex/main-account.ts +6 -2
  64. package/src/codex/native-residue.ts +2 -3
  65. package/src/codex/quota.ts +26 -2
  66. package/src/combos/failover.ts +2 -1
  67. package/src/combos/jev-model-backend.ts +30 -7
  68. package/src/combos/jev-model-request.ts +20 -0
  69. package/src/combos/jev-quota-activation.ts +153 -0
  70. package/src/combos/jev-quota-config.ts +62 -0
  71. package/src/combos/jev-quota-route.ts +26 -0
  72. package/src/combos/jev-quota.ts +88 -0
  73. package/src/combos/jev.ts +25 -12
  74. package/src/combos/types.ts +27 -0
  75. package/src/compatibility/index.ts +3 -0
  76. package/src/compatibility/openai-responses.ts +8 -1
  77. package/src/compatibility/opengateway-responses.ts +61 -0
  78. package/src/config/account-auto-pause-revisions.ts +136 -0
  79. package/src/config/diagnostics.ts +13 -0
  80. package/src/config/live-reconcile.ts +9 -0
  81. package/src/config/persist-unlocked.ts +10 -1
  82. package/src/config/removed-opencode-free.ts +135 -0
  83. package/src/config/schema/account-auto-pause.ts +14 -0
  84. package/src/config/schema/config-schema.ts +15 -0
  85. package/src/config/schema/leaf-validators.ts +7 -0
  86. package/src/config.ts +10 -3
  87. package/src/generated/compatibility-version.json +344 -144
  88. package/src/images/loop.ts +32 -9
  89. package/src/lib/account-auto-pause-response.ts +16 -0
  90. package/src/lib/bun-runtime.ts +18 -1
  91. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  92. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  93. package/src/lib/decision-quota-sink.ts +76 -0
  94. package/src/lib/state-store-registrations.ts +25 -0
  95. package/src/link/ssh-argv.ts +3 -0
  96. package/src/messaging/envelope.ts +6 -3
  97. package/src/messaging/remote-auth.ts +83 -0
  98. package/src/messaging/remote-bridge.ts +270 -0
  99. package/src/messaging/remote-contract.ts +108 -0
  100. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  101. package/src/messaging/remote-enrollment.ts +244 -0
  102. package/src/messaging/remote-envelope.ts +40 -0
  103. package/src/messaging/remote-files.ts +117 -0
  104. package/src/messaging/remote-owner.ts +169 -0
  105. package/src/messaging/remote-ports.ts +56 -0
  106. package/src/messaging/remote-process.ts +131 -0
  107. package/src/messaging/remote-replies.ts +98 -0
  108. package/src/messaging/remote-rpc-admission.ts +32 -0
  109. package/src/messaging/remote-send.ts +72 -0
  110. package/src/messaging/remote-store.ts +111 -0
  111. package/src/messaging/remote-tunnels.ts +65 -0
  112. package/src/messaging/rpc.ts +13 -1
  113. package/src/oauth/account-auto-pause-policy.ts +66 -0
  114. package/src/oauth/account-auto-pause.ts +171 -0
  115. package/src/oauth/account-quota-rank.ts +2 -1
  116. package/src/oauth/anthropic-account-refusal.ts +6 -0
  117. package/src/oauth/anthropic-model-family.ts +8 -0
  118. package/src/oauth/anthropic-model-quota.ts +2 -6
  119. package/src/oauth/api-key-pool-preserve.ts +37 -0
  120. package/src/oauth/index.ts +5 -35
  121. package/src/oauth/store.ts +35 -8
  122. package/src/oauth/types.ts +2 -0
  123. package/src/plugins/upstream-hooks.ts +14 -1
  124. package/src/providers/api-key-pause.ts +32 -0
  125. package/src/providers/api-key-resolve.ts +10 -4
  126. package/src/providers/api-key-selection.ts +6 -1
  127. package/src/providers/api-keys.ts +8 -4
  128. package/src/providers/claude-cli-usage.ts +5 -0
  129. package/src/providers/key-failover.ts +11 -11
  130. package/src/providers/model-rename-fields.ts +1 -0
  131. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  132. package/src/providers/quota/account-cache.ts +15 -1
  133. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  134. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  135. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  136. package/src/providers/quota/antigravity.ts +39 -6
  137. package/src/providers/quota/report-cache.ts +14 -0
  138. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  139. package/src/providers/quota-decision-publication.ts +77 -0
  140. package/src/providers/quota-decision-snapshot.ts +145 -0
  141. package/src/providers/quota-routing-cache.ts +3 -0
  142. package/src/providers/quota-types.ts +6 -1
  143. package/src/providers/quota.ts +3 -2
  144. package/src/providers/registry/entries-extended.ts +8 -52
  145. package/src/providers/registry/model-seeds.ts +1 -1
  146. package/src/providers/registry.ts +1 -2
  147. package/src/providers/stale-vision-classification-migration.ts +1 -1
  148. package/src/responses/parser.ts +18 -10
  149. package/src/responses/web-search-replay.ts +138 -0
  150. package/src/routing/compatibility/behavior.ts +4 -1
  151. package/src/server/auth-cors.ts +7 -0
  152. package/src/server/background-lifecycle.ts +2 -1
  153. package/src/server/chat-native.ts +28 -13
  154. package/src/server/claude-messages.ts +2 -1
  155. package/src/server/index/optional-listeners.ts +6 -0
  156. package/src/server/management/combo-routes.ts +17 -2
  157. package/src/server/management/oauth-account-routes.ts +10 -1
  158. package/src/server/management/provider-completion-test.ts +289 -0
  159. package/src/server/management/provider-routes.ts +9 -1
  160. package/src/server/management/route-registry.ts +2 -0
  161. package/src/server/messages-native.ts +18 -4
  162. package/src/server/messages-shared.ts +4 -1
  163. package/src/server/request-log.ts +7 -2
  164. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  165. package/src/server/responses/adapter-dispatch.ts +77 -28
  166. package/src/server/responses/combo-session-recall.ts +25 -0
  167. package/src/server/responses/combo-stream-preflight.ts +3 -1
  168. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  169. package/src/server/responses/compact.ts +8 -5
  170. package/src/server/responses/context-overflow.ts +141 -2
  171. package/src/server/responses/core-codex-account.ts +7 -1
  172. package/src/server/responses/core-combo.ts +31 -5
  173. package/src/server/responses/core.ts +12 -9
  174. package/src/server/responses/empty-completion-guard.ts +2 -0
  175. package/src/server/responses/fetch-helpers.ts +35 -4
  176. package/src/server/responses/jev-model-invoke.ts +13 -14
  177. package/src/server/responses/native-steering-log.ts +2 -2
  178. package/src/server/responses/passthrough-delivery.ts +3 -2
  179. package/src/server/responses/passthrough-dispatch.ts +30 -33
  180. package/src/server/responses/passthrough-observations.ts +38 -0
  181. package/src/server/responses/request-compression.ts +98 -0
  182. package/src/server/responses/request-transport.ts +32 -3
  183. package/src/server/responses/run-turn-execution.ts +2 -0
  184. package/src/server/responses/sidecar-execution.ts +16 -3
  185. package/src/server/responses/terminal-guard.ts +2 -0
  186. package/src/server/startup-action-control.ts +2 -2
  187. package/src/server/startup-health-cache.ts +2 -2
  188. package/src/server/windows-tray-control.ts +2 -2
  189. package/src/service/definition.ts +9 -1
  190. package/src/types/account-auto-pause.ts +15 -0
  191. package/src/types/config.ts +23 -1
  192. package/src/types/provider.ts +9 -5
  193. package/src/types/request.ts +20 -2
  194. package/src/types.ts +1 -0
  195. package/src/usage/cost.ts +17 -1
  196. package/src/usage/expected-prices.ts +3 -1
  197. package/src/usage/jev-stats.ts +4 -0
  198. package/src/usage/log.ts +2 -0
  199. package/src/usage/summary.ts +7 -0
  200. package/src/web-search/loop.ts +37 -11
  201. package/src/web-search/progress-stream.ts +4 -1
  202. package/src/web-search/run-turn-loop.ts +9 -3
  203. package/src/web-search/sidecar-search-replay.ts +96 -0
  204. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  205. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
@@ -36,7 +36,7 @@ import {
36
36
  observeAntigravityReplay,
37
37
  } from "./google-antigravity-replay";
38
38
  import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
39
- import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
39
+ import { selectVertexEndpoint, vertexInferenceUrl, vertexReplayModelKey } from "./google-vertex-endpoint";
40
40
  import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
41
41
  import {
42
42
  isTranslatorBudgetExceededError,
@@ -333,13 +333,20 @@ function geminiToolResultParts(
333
333
  msg: OcxToolResultMessage,
334
334
  wireName: string,
335
335
  wireCallId: string,
336
+ nestToolResultImages: boolean,
336
337
  ): unknown[] {
337
338
  const functionResponse: Record<string, unknown> = {
338
339
  name: wireName,
339
340
  response: { result: geminiToolResultText(msg.content) },
340
341
  id: wireCallId,
341
342
  };
342
- return [{ functionResponse }, ...toolResultImageParts(msg.content)];
343
+ const images = toolResultImageParts(msg.content);
344
+ // Vertex Argon rejects image siblings beside a functionResponse as an unsupported model tail.
345
+ if (nestToolResultImages && images.length > 0) {
346
+ functionResponse.parts = images;
347
+ return [{ functionResponse }];
348
+ }
349
+ return [{ functionResponse }, ...images];
343
350
  }
344
351
 
345
352
  function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
@@ -370,6 +377,7 @@ function messagesToGeminiFormat(
370
377
  identityModelId: string,
371
378
  stripRejectedClaudeSdkParagraph = false,
372
379
  isCloudCodeAssist = false,
380
+ nestToolResultImages = false,
373
381
  ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
374
382
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
375
383
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
@@ -551,13 +559,17 @@ function messagesToGeminiFormat(
551
559
  const responseParts: unknown[] = [];
552
560
  for (const call of toolCalls) {
553
561
  const result = resultsById.get(call.wireCallId);
554
- if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
562
+ if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId, nestToolResultImages));
555
563
  else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
556
564
  }
557
- for (const orphan of orphanResults) {
558
- responseParts.push(...geminiOrphanToolResultParts(orphan));
559
- }
565
+ const orphanParts = orphanResults.flatMap(geminiOrphanToolResultParts);
560
566
  contents.push({ role: "user", parts: responseParts });
567
+ // Orphan images cannot share Argon's response batch or claim another call's ownership.
568
+ if (nestToolResultImages && orphanParts.some(part => typeof part === "object" && part !== null && "inline_data" in part)) {
569
+ contents.push({ role: "user", parts: orphanParts });
570
+ } else {
571
+ responseParts.push(...orphanParts);
572
+ }
561
573
  i = j - 1;
562
574
  }
563
575
  break;
@@ -1026,6 +1038,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1026
1038
  identityModelId,
1027
1039
  stripRejectedClaudeSdkParagraph,
1028
1040
  isCloudCodeAssist,
1041
+ provider.googleMode === "vertex" && parsed.modelId === "gemini-4-argon",
1029
1042
  );
1030
1043
  lastInjectedCallIds = [...replayedCallIds];
1031
1044
  lastReasoningReplayScope = parsed._reasoningReplayScope;
@@ -1192,9 +1205,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1192
1205
  const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1193
1206
  reportToolSchemaLoss(compiled.toolSchemaLossReport);
1194
1207
  restoreGoogleToolName = compiled.restoreToolName;
1195
- const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
1196
- const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
1197
- vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
1208
+ const apiKey = resolveVertexApiKey(provider.apiKey);
1209
+ const vertexEndpoint = selectVertexEndpoint(provider, Boolean(apiKey));
1210
+ const url = vertexInferenceUrl(vertexEndpoint, parsed.modelId, method, streamParam);
1211
+ vertexReplayModel = vertexReplayModelKey(vertexEndpoint, parsed.modelId);
1198
1212
  vertexReplaySession = vertexReplaySessionId(parsed);
1199
1213
  // Compile names before replay so the cache matches the exact provider-visible
1200
1214
  // functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
@@ -1210,27 +1224,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1210
1224
  (compiled.body as { contents: unknown[] }).contents,
1211
1225
  );
1212
1226
  }
1213
- // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
1214
- const apiKey = resolveVertexApiKey(provider.apiKey);
1215
- if (apiKey) {
1216
- const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
1217
- headers["x-goog-api-key"] = apiKey;
1218
- return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1227
+ // Endpoint, location and model path were validated above, before any credential is attached.
1228
+ if (apiKey) headers["x-goog-api-key"] = apiKey;
1229
+ else {
1230
+ // Resolve each marker per request; never share a process-global active account.
1231
+ const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
1232
+ headers["Authorization"] = `Bearer ${await getVertexAccessToken({ markerAccount })}`;
1219
1233
  }
1220
- const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
1221
- if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
1222
- const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
1223
- if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
1224
- const locationError = googleVertexLocationConfigError(location);
1225
- if (locationError) throw new Error(locationError);
1226
- const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
1227
- const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
1228
- // The marker account travels per-request as an argument (never process-global state): a
1229
- // module-level "active marker" written at routing time would race across concurrent
1230
- // requests for different providers and could resolve one call with another's credential.
1231
- const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
1232
- const token = await getVertexAccessToken({ markerAccount });
1233
- headers["Authorization"] = `Bearer ${token}`;
1234
1234
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1235
1235
  }
1236
1236
 
@@ -1254,6 +1254,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1254
1254
  const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1255
1255
  reportToolSchemaLoss(compiled.toolSchemaLossReport);
1256
1256
  restoreGoogleToolName = compiled.restoreToolName;
1257
+ // A functionCall issued by another model (a combo hop, #7298) has no Gemini signature, and
1258
+ // Gemini 3 refuses the turn without one. The same Gemini-only sentinel CCA and Vertex
1259
+ // apply; a real signature carried by history always wins.
1260
+ if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
1261
+ applyAntigravityThoughtSignatureFallback(routedModelId, (compiled.body as { contents: unknown[] }).contents);
1262
+ }
1257
1263
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1258
1264
  },
1259
1265
 
@@ -114,3 +114,30 @@ export function renderCompactionSearchHistoryItems(items: readonly unknown[], re
114
114
  if (omissionIndex >= 0) out[omissionIndex] = omissionNote(omitted);
115
115
  return out;
116
116
  }
117
+
118
+ function isHostedSearchTool(tool: unknown): boolean {
119
+ return isPlainObject(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search");
120
+ }
121
+
122
+ /**
123
+ * True when the outbound body declares a hosted search tool, either top-level or inside a Lite
124
+ * `additional_tools` input wrapper. Read on the final body, after unsupported hosted tools were
125
+ * stripped, so a declaration the destination never sees does not count.
126
+ */
127
+ export function declaresHostedWebSearch(body: Record<string, unknown>): boolean {
128
+ if (Array.isArray(body.tools) && body.tools.some(isHostedSearchTool)) return true;
129
+ return Array.isArray(body.input) && body.input.some(item => isPlainObject(item)
130
+ && item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.some(isHostedSearchTool));
131
+ }
132
+
133
+ /**
134
+ * Project unreferenced hosted `web_search_call` history into reference notes. A same-input
135
+ * `item_reference` to a hosted cell keeps that cell structural. Returns the original body when
136
+ * there is nothing to project.
137
+ */
138
+ export function projectHostedSearchHistory(body: Record<string, unknown>): Record<string, unknown> {
139
+ if (!Array.isArray(body.input) || !body.input.some(item => isPlainObject(item) && item.type === "web_search_call")) return body;
140
+ const referencedIds = new Set(body.input.flatMap(item => isPlainObject(item)
141
+ && item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
142
+ return { ...body, input: renderCompactionSearchHistoryItems(body.input, referencedIds) };
143
+ }
@@ -53,7 +53,7 @@ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchField
53
53
  import { observeOutbound } from "../../usage/cache-diagnostic";
54
54
 
55
55
  import { normalizeMuseToolChoice } from "./muse-tool-choice";
56
- import { renderCompactionSearchHistoryItems } from "./compaction-search-history";
56
+ import { declaresHostedWebSearch, projectHostedSearchHistory, renderCompactionSearchHistoryItems } from "./compaction-search-history";
57
57
  import { stripKimiEmptyAssistantMessages } from "./empty-assistant";
58
58
 
59
59
  /**
@@ -135,7 +135,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
135
135
  const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
136
136
  const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
137
137
  || (isPlainObject(usage.input_tokens_details)
138
- && Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"))
138
+ && Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
139
+ && key !== "cache_write_1h_tokens"))
139
140
  || (isPlainObject(usage.output_tokens_details)
140
141
  && Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
141
142
  if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
@@ -147,6 +148,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
147
148
  ...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
148
149
  ...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
149
150
  ...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
151
+ ...(typeof inputDetails?.cache_write_1h_tokens === "number"
152
+ ? { cacheCreation1hInputTokens: inputDetails.cache_write_1h_tokens } : {}),
150
153
  ...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
151
154
  ...(hasExtras ? { rawUsage: { ...usage } } : {}),
152
155
  };
@@ -492,16 +495,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
492
495
  // here, on the serialized body, not on the parsed selector. One place covers both the
493
496
  // HTTP and the WebSocket outbound, because the WS path transports this same request
494
497
  // instead of rebuilding it.
495
- // Opt-in Codex Lite history policy: replay remaining hosted actions as untrusted metadata,
496
- // after bridge restoration/tool validation. Tool declarations and other input stay intact.
497
- if (provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
498
- && !parsed._compactionRequest && !parsed._portableCompaction
499
- && codexResponsesLiteSelected(headers, finalBody)
500
- && isPlainObject(finalBody) && Array.isArray(finalBody.input)
501
- && finalBody.input.some(item => isPlainObject(item) && item.type === "web_search_call")) {
502
- const referencedIds = new Set(finalBody.input.flatMap(item => isPlainObject(item)
503
- && item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
504
- finalBody = { ...finalBody, input: renderCompactionSearchHistoryItems(finalBody.input, referencedIds) };
498
+ // Hosted-search history projection, after bridge restoration and tool validation. Two
499
+ // triggers share one bounded renderer, and tool declarations and other input stay intact:
500
+ // 1. The opt-in Codex Lite policy covers ordinary turns and native v2 compaction triggers
501
+ // (#7268). Portable compaction was already projected by the summary builder above.
502
+ // 2. Any request whose final tool surface declares no hosted search. Native Sol answers a
503
+ // replayed web_search_call without a matching declaration with a generic server_error
504
+ // (#7250), so the cells become untrusted reference notes instead of a doomed request.
505
+ if (isPlainObject(finalBody)
506
+ && ((provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
507
+ && !parsed._portableCompaction && codexResponsesLiteSelected(headers, finalBody))
508
+ || !declaresHostedWebSearch(finalBody))) {
509
+ finalBody = projectHostedSearchHistory(finalBody);
505
510
  }
506
511
  observeOutbound(parsed._rawBody, finalBody, headers);
507
512
  if (!isCanonicalOpenAiForwardProvider(provider)) {
@@ -163,7 +163,74 @@ export function sanitizeReasoningInputContent(
163
163
  });
164
164
 
165
165
  if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
166
- return changed ? { ...raw, input } : body;
166
+ // Runs after the per-item pass so an item it emptied has already been backfilled, and a
167
+ // placeholder is only invented for a turn that carries no reasoning item at all.
168
+ const withTurnReasoning = opts?.requirePlaintextReasoning === true
169
+ ? backfillBareToolCallTurnReasoning(input)
170
+ : input;
171
+ if (withTurnReasoning !== input) changed = true;
172
+ return changed ? { ...raw, input: withTurnReasoning } : body;
173
+ }
174
+
175
+ /** Assistant-authored history items: what DeepSeek merges into one assistant message. */
176
+ function isAssistantTurnItem(item: unknown): boolean {
177
+ if (!isPlainObject(item) || typeof item.type !== "string") return false;
178
+ if (item.type === "reasoning") return true;
179
+ if (item.type === "message") return item.role === "assistant";
180
+ return item.type.endsWith("_call");
181
+ }
182
+
183
+ function isClientToolCallItem(item: unknown): boolean {
184
+ return isPlainObject(item)
185
+ && (item.type === "function_call" || item.type === "custom_tool_call" || item.type === "local_shell_call");
186
+ }
187
+
188
+ /**
189
+ * Give every assistant tool-call turn that has NO reasoning item a minimal plaintext one (#7298).
190
+ *
191
+ * A Combo can serve the turn that issued a tool call on one model and the continuation on
192
+ * another. When the issuing model was a GPT turn that produced no reasoning item (or the item
193
+ * was dropped upstream), the strict destination sees a bare `function_call` and DeepSeek's
194
+ * thinking mode refuses the whole request with `The reasoning_text in the thinking mode must be
195
+ * passed back to the API`. The per-item backfill above cannot help, because there is no item to
196
+ * backfill. This is the Responses-wire twin of the Chat adapter's `reasoning_content: " "`
197
+ * placeholder (#5421): the same single-space text, never fabricated reasoning, inserted at the
198
+ * head of the turn so it precedes the call exactly where a native turn's reasoning would be.
199
+ *
200
+ * A turn is a maximal run of assistant-authored items; any user message or tool output ends it.
201
+ * Turns that already carry a reasoning item, and turns with no client tool call, are untouched.
202
+ */
203
+ export function backfillBareToolCallTurnReasoning(input: unknown[]): unknown[] {
204
+ const bareTurnStarts = new Set<number>();
205
+ let turnStart = -1;
206
+ let turnHasReasoning = false;
207
+ let turnHasToolCall = false;
208
+ const closeTurn = (): void => {
209
+ if (turnStart >= 0 && turnHasToolCall && !turnHasReasoning) bareTurnStarts.add(turnStart);
210
+ turnStart = -1;
211
+ turnHasReasoning = false;
212
+ turnHasToolCall = false;
213
+ };
214
+ for (let index = 0; index < input.length; index++) {
215
+ const item = input[index];
216
+ if (!isAssistantTurnItem(item)) {
217
+ closeTurn();
218
+ continue;
219
+ }
220
+ if (turnStart < 0) turnStart = index;
221
+ if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true;
222
+ if (isClientToolCallItem(item)) turnHasToolCall = true;
223
+ }
224
+ closeTurn();
225
+ if (bareTurnStarts.size === 0) return input;
226
+ const result: unknown[] = [];
227
+ for (let index = 0; index < input.length; index++) {
228
+ if (bareTurnStarts.has(index)) {
229
+ result.push({ type: "reasoning", summary: [], content: [{ type: "reasoning_text", text: " " }] });
230
+ }
231
+ result.push(input[index]);
232
+ }
233
+ return result;
167
234
  }
168
235
 
169
236
  /**
@@ -138,7 +138,7 @@ export const ADAPTER_REGISTRY = {
138
138
  zed: {
139
139
  wire: "zed",
140
140
  mutation: "codex-owned",
141
- create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createZedAdapter(provider),
141
+ create: (provider: OcxProviderConfig, context: AdapterFactoryContext) => createZedAdapter(provider, context.cacheRetention),
142
142
  },
143
143
  "mimo-free": {
144
144
  contractParent: "openai-chat",
@@ -1,3 +1,4 @@
1
+ import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
1
2
  import { readBoundedResponseBody } from "../lib/bounded-body";
2
3
  import { redactSecretString } from "../lib/redact";
3
4
 
@@ -40,9 +41,9 @@ export async function normalizeUpstreamHttpErrorResponse(
40
41
  const headers = new Headers(res.headers);
41
42
  headers.delete("content-encoding");
42
43
  headers.delete("content-length");
43
- return new Response(await opts.formatMessage(payloadText), {
44
+ return inheritAccountAutoPauseResponse(res, new Response(await opts.formatMessage(payloadText), {
44
45
  status: res.status,
45
46
  statusText: res.statusText,
46
47
  headers,
47
- });
48
+ }));
48
49
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
3
3
  import { createAnthropicAdapter } from "./anthropic";
4
+ import type { AnthropicCacheRetention } from "./anthropic/cache-retention";
4
5
  import { createGoogleAdapter } from "./google";
5
6
  import { createOpenAIChatAdapter } from "./openai-chat";
6
7
  import { createResponsesPassthroughAdapter } from "./openai-responses";
@@ -218,15 +219,17 @@ export function zedEventStream(
218
219
  return body.pipeThrough(output);
219
220
  }
220
221
 
221
- function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider): ProviderAdapter {
222
+ function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
222
223
  const config = delegateProvider(provider, zedProvider);
223
- if (zedProvider === "anthropic") return createAnthropicAdapter(config);
224
+ // The delegate only borrows the api.anthropic.com body builder; the wire goes to Zed's
225
+ // gateway, which does not document the 1h TTL, so an omitted retention stays 5m here.
226
+ if (zedProvider === "anthropic") return createAnthropicAdapter(config, cacheRetention ?? "short");
224
227
  if (zedProvider === "google") return createGoogleAdapter(config);
225
228
  if (zedProvider === "open_ai") return createResponsesPassthroughAdapter(config);
226
229
  return createOpenAIChatAdapter(config);
227
230
  }
228
231
 
229
- export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
232
+ export function createZedAdapter(provider: OcxProviderConfig, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
230
233
  let delegate: ZedDelegate | undefined;
231
234
  let credentials: ZedCredentials | undefined;
232
235
 
@@ -246,7 +249,7 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
246
249
  /* Model inference fallback below; a transient catalog outage must not block passthrough. */
247
250
  }
248
251
  const zedProvider = providerFromCatalog(catalog, parsed.modelId);
249
- const selected = createDelegate(provider, zedProvider);
252
+ const selected = createDelegate(provider, zedProvider, cacheRetention);
250
253
  const safeIncoming: IncomingMeta = {
251
254
  headers: incoming?.headers ?? new Headers(),
252
255
  translatorBudget: incoming?.translatorBudget ?? createTranslatorBudget(),
@@ -23,6 +23,11 @@ export function uuid(): string {
23
23
  return crypto.randomUUID().replace(/-/g, "");
24
24
  }
25
25
 
26
+ /** The client-visible id of a hosted search cell: the producer's `ws_` id when well-formed, else a fresh one. */
27
+ export function webSearchCellItemId(candidate: string | undefined): string {
28
+ return typeof candidate === "string" && /^ws_[A-Za-z0-9_-]{1,128}$/.test(candidate) ? candidate : `ws_${uuid()}`;
29
+ }
30
+
26
31
  /** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
27
32
  export let ownedBudgetAbandonedMs = 10 * 60 * 1000;
28
33
  const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
@@ -64,7 +69,7 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
64
69
  // normalized contract).
65
70
  const rawInputDetails = isRecord(raw.input_tokens_details)
66
71
  ? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
67
- .filter(([key]) => key !== "cache_write_tokens"))
72
+ .filter(([key]) => key !== "cache_write_tokens" && key !== "cache_write_1h_tokens"))
68
73
  : {} as Record<string, unknown>;
69
74
  const rawOutputDetails = isRecord(raw.output_tokens_details)
70
75
  ? raw.output_tokens_details as Record<string, unknown>
@@ -92,6 +97,11 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
92
97
  usage.cacheCreationInputTokens,
93
98
  Math.max(0, inputTokens - cacheRead),
94
99
  );
100
+ // The 1-hour-TTL share of the write (Anthropic cache_creation split), only when reported,
101
+ // so a translated Messages client can price it at the 1h rate.
102
+ if (usage.cacheCreation1hInputTokens !== undefined) {
103
+ inputDetails.cache_write_1h_tokens = Math.min(usage.cacheCreation1hInputTokens, inputDetails.cache_write_tokens as number);
104
+ }
95
105
  }
96
106
  out.input_tokens_details = inputDetails;
97
107
  out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
@@ -42,7 +42,7 @@ import {
42
42
  type TranslatorBudget,
43
43
  type TranslatorBufferKind,
44
44
  } from "../lib/translator-budget";
45
- import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
45
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
46
46
  import type { OutputItem, StringChunks } from "./internal";
47
47
  import { bridgeToResponsesSSE } from "./sse";
48
48
 
@@ -546,7 +546,7 @@ function buildResponseJSONWithBudget(
546
546
  flushToolCall();
547
547
  const safeSources = safeWebSearchSources(e.sources);
548
548
  pushOutput({
549
- type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
549
+ type: "web_search_call", id: webSearchCellItemId(e.itemId), status: e.status ?? "completed",
550
550
  action: webSearchAction(e.queries, e.anthropic),
551
551
  ...(safeSources.length > 0 ? { sources: safeSources } : {}),
552
552
  ...(e.anthropic ? { anthropic_blocks: e.anthropic } : {}),
package/src/bridge/sse.ts CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  type TranslatorBudget,
51
51
  type TranslatorBufferKind,
52
52
  } from "../lib/translator-budget";
53
- import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
53
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
54
54
  import type { OutputItem, StringChunks } from "./internal";
55
55
  import { terminalRefusalFailure } from "./terminal-refusal";
56
56
 
@@ -770,9 +770,14 @@ export function bridgeToResponsesSSE(
770
770
  returnIterator();
771
771
  };
772
772
  let handlingTranslatorOverflow = false;
773
- terminateForTranslatorOverflow = _error => {
773
+ terminateForTranslatorOverflow = error => {
774
774
  if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
775
775
  handlingTranslatorOverflow = true;
776
+ // An adapter-raised overflow event carries the usage the turn already consumed; the
777
+ // provider billed it, so it must not be logged as zero.
778
+ const usage = (error as AdapterEvent | null | undefined)?.type === "error"
779
+ ? (error as Extract<AdapterEvent, { type: "error" }>).usage : undefined;
780
+ if (usage) options?.onUsage?.(usage);
776
781
  abortCurrentToolCallForTranslatorOverflow();
777
782
  currentWebSearch = null;
778
783
  releasePendingWebSources();
@@ -788,6 +793,7 @@ export function bridgeToResponsesSSE(
788
793
  sequence_number: seq++,
789
794
  response: {
790
795
  ...responseSnapshot("failed", finishedItems),
796
+ ...(usage ? { usage: responsesUsage(usage) } : {}),
791
797
  error: failure,
792
798
  last_error: failure,
793
799
  },
@@ -1211,7 +1217,7 @@ export function bridgeToResponsesSSE(
1211
1217
  flushHiddenRawReasoning();
1212
1218
  if (currentToolCall) closeCurrentToolCall();
1213
1219
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
1214
- const wsItemId = `ws_${uuid()}`;
1220
+ const wsItemId = webSearchCellItemId(event.itemId);
1215
1221
  emit("response.output_item.added", {
1216
1222
  output_index: outputIndex,
1217
1223
  item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
@@ -1224,7 +1230,7 @@ export function bridgeToResponsesSSE(
1224
1230
  // (defensive), synthesize the added frame first so the done has a matching item.
1225
1231
  if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
1226
1232
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
1227
- const wsItemId2 = `ws_${uuid()}`;
1233
+ const wsItemId2 = webSearchCellItemId(event.itemId);
1228
1234
  emit("response.output_item.added", {
1229
1235
  output_index: outputIndex,
1230
1236
  item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
@@ -21,6 +21,7 @@ import { stabilizeClaudeInstructionsForPromptCache } from "./inbound-cache-stabi
21
21
  import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
22
22
  import { inlineDocumentMarker } from "../responses/inline-document";
23
23
  import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
24
+ import { collectAnthropicCacheTtls } from "../adapters/anthropic/cache-retention";
24
25
 
25
26
 
26
27
 
@@ -390,34 +391,19 @@ export type ClaudeCacheKeySource = "metadata" | "system" | null;
390
391
  /**
391
392
  * The translated lane places its own prompt-cache breakpoints, so the caller's are dropped;
392
393
  * their TTL is the one thing worth keeping. When every caller breakpoint asks for `ttl: "1h"`
393
- * (Claude Code on a subscription marks them so) this maps to `cacheRetention: "long"`. A mixed
394
- * caller (1h prefix, 5m tail) stays on the default: "long" also makes the moving per-turn
395
- * breakpoint 1h, which bills writes the caller asked to keep at the 5m rate at the 1h rate.
396
- * Read out of body: the Responses body has no field for it.
394
+ * (Claude Code on a subscription marks them so) this maps to `cacheRetention: "long"`. A caller
395
+ * that pins any breakpoint to an explicit non-1h TTL ("5m") maps to "short": "long" would also
396
+ * make the moving per-turn breakpoint 1h and bill writes the caller asked to keep at 5m at the
397
+ * 1h rate. Breakpoints without a TTL express no preference, so they fall through to the
398
+ * destination default (1h on first-party Anthropic). Read out of body: the Responses body has
399
+ * no field for it.
397
400
  */
398
- export function callerCacheRetention(raw: unknown): "long" | undefined {
399
- if (!isRec(raw)) return undefined;
401
+ export function callerCacheRetention(raw: unknown): "long" | "short" | undefined {
400
402
  // Top-level automatic caching is a breakpoint too, and content blocks nest (tool_result,
401
- // search_result, document sources), so every block container is walked. Tool inputs and
402
- // schemas are caller data, not blocks: a `cache_control` key there is not a breakpoint.
403
- const ttls: unknown[] = [];
404
- const visit = (node: unknown, depth: number): void => {
405
- if (depth > 16) return;
406
- if (Array.isArray(node)) { for (const item of node) visit(item, depth + 1); return; }
407
- if (!isRec(node)) return;
408
- if (isRec(node.cache_control)) ttls.push(node.cache_control.ttl);
409
- for (const [key, value] of Object.entries(node)) {
410
- if (key !== "cache_control" && key !== "input" && key !== "input_schema"
411
- && (Array.isArray(value) || isRec(value))) visit(value, depth + 1);
412
- }
413
- };
414
- if (isRec(raw.cache_control)) ttls.push(raw.cache_control.ttl);
415
- visit(raw.system, 0);
416
- visit(raw.tools, 0);
417
- visit(raw.messages, 0);
418
- return ttls.length > 0 && ttls.every(ttl => ttl === "1h")
419
- ? "long"
420
- : undefined;
403
+ // search_result, document sources), so every block container is walked.
404
+ const ttls = collectAnthropicCacheTtls(raw);
405
+ if (ttls.some(ttl => ttl !== undefined && ttl !== "1h")) return "short";
406
+ return ttls.length > 0 && ttls.every(ttl => ttl === "1h") ? "long" : undefined;
421
407
  }
422
408
 
423
409
  export interface ClaudeInboundTranslation {
@@ -156,6 +156,8 @@ export function anthropicUsage(usage: unknown, webSearchRequests = 0): Rec {
156
156
  const details = isRec(u.input_tokens_details) ? u.input_tokens_details : {};
157
157
  const cached = typeof details.cached_tokens === "number" ? details.cached_tokens : 0;
158
158
  const cacheWrite = typeof details.cache_write_tokens === "number" ? details.cache_write_tokens : 0;
159
+ const cacheWrite1h = typeof details.cache_write_1h_tokens === "number"
160
+ ? Math.max(0, Math.min(details.cache_write_1h_tokens, cacheWrite)) : undefined;
159
161
  const input = typeof u.input_tokens === "number" ? u.input_tokens : 0;
160
162
  const output = typeof u.output_tokens === "number" ? u.output_tokens : 0;
161
163
  return {
@@ -163,6 +165,9 @@ export function anthropicUsage(usage: unknown, webSearchRequests = 0): Rec {
163
165
  output_tokens: output,
164
166
  cache_read_input_tokens: cached,
165
167
  cache_creation_input_tokens: cacheWrite,
168
+ ...(cacheWrite1h !== undefined
169
+ ? { cache_creation: { ephemeral_5m_input_tokens: cacheWrite - cacheWrite1h, ephemeral_1h_input_tokens: cacheWrite1h } }
170
+ : {}),
166
171
  // Only successful searches are billed/counted (Anthropic contract; Claude Code cost accounting).
167
172
  ...(webSearchRequests > 0 ? { server_tool_use: { web_search_requests: webSearchRequests } } : {}),
168
173
  };
@@ -16,6 +16,7 @@ import { projectApiKeyQuotaRows } from "./account-key-quota";
16
16
  import { projectAccountHealth } from "./account-next-actions";
17
17
  import type { OAuthHealthLabel } from "../oauth/health";
18
18
 
19
+ import { accountPauseDetailsSchema } from "../config/schema/account-auto-pause";
19
20
  export type AccountType = "codex" | "oauth" | "api-key";
20
21
 
21
22
  export interface AccountRow {
@@ -61,6 +62,7 @@ export interface AccountRow {
61
62
  * human listing (#2703).
62
63
  */
63
64
  paused?: boolean;
65
+ pauseDetails?: import("../types/account-auto-pause").AccountPauseDetails;
64
66
  }
65
67
 
66
68
  export type ClassifyResult = { type: AccountType } | { error: string };
@@ -342,6 +344,7 @@ interface CodexAccountDto {
342
344
  quota?: CodexQuotaDto | null;
343
345
  quotaRefresh?: unknown;
344
346
  paused?: boolean;
347
+ pauseDetails?: import("../types/account-auto-pause").AccountPauseDetails;
345
348
  }
346
349
 
347
350
  function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
@@ -415,6 +418,8 @@ export async function fetchCodexRows(
415
418
  ? a.autoSwitchThresholdOverride
416
419
  : null,
417
420
  paused: a.paused === true,
421
+ // Unknown display details are dropped defensively; the pause state above is retained.
422
+ pauseDetails: accountPauseDetailsSchema.safeParse(a.pauseDetails).data,
418
423
  ...(includeQuota ? {
419
424
  quota: projectQuota(a.quota),
420
425
  quotaRefresh: projectCodexQuotaRefreshOutcome(a.quotaRefresh),
@@ -432,6 +437,7 @@ interface OAuthAccountDto {
432
437
  healthLabel?: unknown;
433
438
  /** Present only for providers that support operator pause (generic OAuth pools). */
434
439
  paused?: boolean;
440
+ pauseDetails?: import("../types/account-auto-pause").AccountPauseDetails;
435
441
  autoSwitchThresholdOverride?: number | null;
436
442
  needsReauthReason?: "verify_account";
437
443
  autoSelectable?: boolean;
@@ -475,7 +481,8 @@ async function fetchOAuthRows(
475
481
  active: a.active ?? a.id === activeId,
476
482
  needsReauth: a.needsReauth,
477
483
  ...projectAccountHealth(a, name, a.id),
478
- ...(a.paused === true ? { paused: true } : {}),
484
+ // Unknown display details are dropped defensively; paused remains authoritative.
485
+ ...(a.paused === true ? { paused: true, pauseDetails: accountPauseDetailsSchema.safeParse(a.pauseDetails).data } : {}),
479
486
  ...(isAnthropicOAuthInstance(name) && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
480
487
  ...(a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
481
488
  ...(name === "kiro" && typeof a.autoSelectable === "boolean"
@@ -495,6 +502,8 @@ async function fetchOAuthRows(
495
502
  }
496
503
 
497
504
  interface ApiKeyDto {
505
+ disabled?: boolean;
506
+ pauseDetails?: import("../types/account-auto-pause").AccountPauseDetails;
498
507
  id: string;
499
508
  label?: string;
500
509
  masked?: string;
@@ -523,6 +532,9 @@ async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string, qu
523
532
  label: k.label ?? k.masked,
524
533
  masked: k.masked,
525
534
  active: k.active ?? k.id === activeId,
535
+ paused: k.disabled === true,
536
+ // Unknown display details are dropped defensively; disabled still keeps the key paused.
537
+ pauseDetails: accountPauseDetailsSchema.safeParse(k.pauseDetails).data,
526
538
  }));
527
539
  return { rows, activeId, status: 200 };
528
540
  }