@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-D_t6sCWs.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -7,7 +7,7 @@
7
7
  */
8
8
  type Rec = Record<string, unknown>;
9
9
 
10
- import { decodeServerSentEvents, sseFieldValue } from "../lib/sse-decoder";
10
+ import { decodeServerSentEvents } from "../lib/sse-decoder";
11
11
  import {
12
12
  isTranslatorBudgetExceededError,
13
13
  type TranslatorBudget,
@@ -21,6 +21,7 @@ import {
21
21
  isCyberPolicyMessage,
22
22
  } from "../lib/errors";
23
23
  import { redactSecretString } from "../lib/redact";
24
+ import { createSseBlockBuffer, sseDataPayload } from "../server/sse-payload-rewrite";
24
25
 
25
26
  function isRec(v: unknown): v is Rec {
26
27
  return !!v && typeof v === "object" && !Array.isArray(v);
@@ -182,6 +183,10 @@ export function responsesSseToChatCompletionsSse(
182
183
  let cancelled = false;
183
184
  let started = false;
184
185
  let sawToolUse = false;
186
+ // The upstream response-level service-tier echo (xAI Priority Processing, OpenAI
187
+ // fast tier), captured from any response event and stamped on every emitted chunk,
188
+ // matching how the source annotates its own streamed chat chunks.
189
+ let serviceTier: string | undefined;
185
190
  const id = completionId();
186
191
  const created = Math.floor(Date.now() / 1000);
187
192
  // tool call_id -> streaming index (OpenAI requires stable indices per tool call)
@@ -298,9 +303,18 @@ export function responsesSseToChatCompletionsSse(
298
303
  if (existing && existing.parts.size > 0 && item.type !== undefined) throw refusalTranslationError();
299
304
  return;
300
305
  }
301
- // Unrelated sparse text messages historically need no position metadata.
302
- if (outputIndex === undefined && (!Array.isArray(item.content)
303
- || !item.content.some(part => isRec(part) && part.type === "refusal"))) return;
306
+ // Only an item that actually carries refusal content belongs in the refusal ledger.
307
+ // The exit used to be the position guard alone, so every message that had an index
308
+ // was enrolled — ordinary assistant text included — and the id/index consistency
309
+ // checks inside refusalItem() then governed streams containing no refusal at all.
310
+ // One index legitimately carrying two item ids was then fatal: a bridge that flushes
311
+ // a hidden reasoning envelope at the index of a still-open message failed a healthy
312
+ // turn with invalid_refusal. `existing` keeps a ledger already opened by real refusal
313
+ // evidence under the same scrutiny as before. The non-streaming collector below has
314
+ // always scoped itself this way; this is the streaming path catching up.
315
+ const hasRefusalPart = Array.isArray(item.content)
316
+ && item.content.some(part => isRec(part) && part.type === "refusal");
317
+ if (!existing && !hasRefusalPart) return;
304
318
  const known = refusalItem(outputIndex, item, "id");
305
319
  if (!Array.isArray(item.content)) return;
306
320
  item.content.forEach((part: unknown, contentIndex: number) => {
@@ -366,6 +380,9 @@ export function responsesSseToChatCompletionsSse(
366
380
  };
367
381
  const emit = (payload: Rec | "[DONE]") => {
368
382
  if (failed) return;
383
+ if (serviceTier !== undefined && isRec(payload) && Array.isArray(payload.choices)) {
384
+ payload.service_tier = serviceTier;
385
+ }
369
386
  if (terminalBatch) {
370
387
  const serialized = dataFrame(payload);
371
388
  const stringReservation = translatorBudget.reserveTransient(Buffer.byteLength(serialized), { kind: "live_transient" });
@@ -530,6 +547,9 @@ export function responsesSseToChatCompletionsSse(
530
547
  };
531
548
 
532
549
  const handleFrame = (eventName: string, data: Rec) => {
550
+ if (isRec(data.response) && typeof data.response.service_tier === "string") {
551
+ serviceTier = data.response.service_tier;
552
+ }
533
553
  switch (eventName) {
534
554
  case "response.created":
535
555
  case "response.heartbeat":
@@ -565,7 +585,15 @@ export function responsesSseToChatCompletionsSse(
565
585
  const item = isRec(data.item) ? data.item : null;
566
586
  if (item?.type === "message") {
567
587
  snapshotRefusalItem(data.output_index, item);
568
- if (Object.hasOwn(data, "item_id")) refusalItem(data.output_index, data, "item_id");
588
+ // Bind the event-level id only for an index the refusal ledger already
589
+ // tracks. Testing membership directly rather than through position()
590
+ // matters: position() throws on a malformed index, which would add a
591
+ // failure to refusal-free streams instead of removing one.
592
+ if (Object.hasOwn(data, "item_id")
593
+ && typeof data.output_index === "number"
594
+ && refusalItems.has(data.output_index)) {
595
+ refusalItem(data.output_index, data, "item_id");
596
+ }
569
597
  }
570
598
  if (!item || item.type !== "function_call") break;
571
599
  ensureRole();
@@ -611,7 +639,11 @@ export function responsesSseToChatCompletionsSse(
611
639
  const item = isRec(data.item) ? data.item : null;
612
640
  if (!item) break;
613
641
  snapshotRefusalItem(data.output_index, item);
614
- if (item.type === "message" && Object.hasOwn(data, "item_id")) refusalItem(data.output_index, data, "item_id");
642
+ if (item.type === "message" && Object.hasOwn(data, "item_id")
643
+ && typeof data.output_index === "number"
644
+ && refusalItems.has(data.output_index)) {
645
+ refusalItem(data.output_index, data, "item_id");
646
+ }
615
647
  if (item.type === "function_call") {
616
648
  sawToolUse = true;
617
649
  const callId = typeof item.call_id === "string" ? item.call_id : "";
@@ -871,6 +903,10 @@ export function responsesJsonToChatCompletion(json: unknown, model: string, tran
871
903
  logprobs: null,
872
904
  }],
873
905
  usage: chatCompletionsUsage(body.usage),
906
+ // Relay the upstream service-tier echo (xAI Priority Processing, OpenAI fast tier)
907
+ // so a Chat Completions caller can confirm the tier the turn actually used, the
908
+ // same field the Responses lane already relays for responses-wire upstreams.
909
+ ...(typeof body.service_tier === "string" ? { service_tier: body.service_tier } : {}),
874
910
  };
875
911
  }
876
912
 
@@ -880,129 +916,125 @@ export async function collectChatCompletion(
880
916
  model: string,
881
917
  translatorBudget: TranslatorBudget,
882
918
  ): Promise<Rec> {
919
+ const reader = stream.getReader();
883
920
  const decoder = new TextDecoder();
884
- let buffer = "";
921
+ const buffer = createSseBlockBuffer(translatorBudget);
885
922
  let content = "";
886
923
  let refusal: string | null = null;
887
924
  let reasoning = "";
925
+ const retainedBytes = { content: 0, refusal: 0, reasoning: 0 };
888
926
  const toolCalls = new Map<number, { id: string; name: string; arguments: string; argumentBytes: number }>();
889
927
  // Per-call budget scopes (2 MiB/call enforced by the budget): the map key is the
890
928
  // wire index, which is stable across deltas and present before the call id.
891
929
  const callScope = (index: number) => `chat_collect_${index}`;
892
930
  let finishReason = "stop";
893
931
  let usage: unknown;
894
- const replaceRetained = (previous: string, next: string, kind: "live_transient" | "retained_collectors") => {
895
- const reservation = translatorBudget.reserveTransient(Buffer.byteLength(next), { kind });
896
- reservation.commitRetained();
897
- translatorBudget.releaseRetained(Buffer.byteLength(previous), { kind });
898
- return next;
932
+ let serviceTier: unknown;
933
+ const appendRetained = (key: keyof typeof retainedBytes, previous: string, fragment: string): string => {
934
+ const previousBytes = retainedBytes[key];
935
+ const nextBytes = appendedUtf8Bytes(previous, previousBytes, fragment);
936
+ const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "retained_collectors" });
937
+ try {
938
+ const next = previous + fragment;
939
+ reservation.commitRetained();
940
+ translatorBudget.releaseRetained(previousBytes, { kind: "retained_collectors" });
941
+ retainedBytes[key] = nextBytes;
942
+ return next;
943
+ } catch (error) {
944
+ reservation.release();
945
+ throw error;
946
+ }
947
+ };
948
+ const releaseCollectors = () => {
949
+ translatorBudget.releaseRetained(retainedBytes.content + retainedBytes.refusal + retainedBytes.reasoning,
950
+ { kind: "retained_collectors" });
899
951
  };
900
- const reader = stream.getReader();
901
952
  try {
953
+ // Share native relay framing, while retaining the collector's existing admission
954
+ // order: release each consumed input frame before accumulating its output fields.
955
+ // CRLF and multiline data obey the same contract as the streaming response.
902
956
  for (;;) {
903
- let done = false;
904
- let value: Uint8Array | undefined;
905
- try {
906
- ({ done, value } = await reader.read());
907
- } catch (err) {
908
- if (isChatCompletionsStreamError(err)) throw err;
909
- if (isTranslatorBudgetExceededError(err)) {
910
- // Provider-controlled overflow is an upstream failure, not a client
911
- // request error: match the adapter/bridge contract (502 upstream_error).
912
- throw new ChatCompletionsStreamError(err.message, {
913
- status: 502,
914
- type: "upstream_error",
915
- code: err.code,
916
- });
917
- }
918
- throw new ChatCompletionsStreamError(err instanceof Error ? err.message : String(err));
919
- }
957
+ const { done, value } = await reader.read();
920
958
  if (done) break;
921
959
  if (!value) continue;
922
- buffer = replaceRetained(buffer, buffer + decoder.decode(value, { stream: true }), "live_transient");
923
- let sep: number;
924
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
925
- const rawFrame = buffer.slice(0, sep);
926
- buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
927
- for (const line of rawFrame.split("\n")) {
928
- const rawData = sseFieldValue(line, "data");
929
- if (rawData === null) continue;
930
- const data = rawData.trim();
931
- if (!data || data === "[DONE]") continue;
932
- let parsed: unknown;
933
- try { parsed = JSON.parse(data); } catch { continue; }
934
- if (!isRec(parsed)) continue;
935
- if (isRec(parsed.error)) {
936
- const message = typeof parsed.error.message === "string"
937
- ? parsed.error.message
938
- : "upstream request failed";
939
- const type = typeof parsed.error.type === "string" ? parsed.error.type : "server_error";
940
- const code = typeof parsed.error.code === "string" ? parsed.error.code : null;
941
- const status = code === "translation_buffer_limit"
942
- ? 502
943
- : code === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(message)
944
- ? 400
945
- : streamErrorStatus(message);
946
- const streamError = new ChatCompletionsStreamError(message, {
947
- status,
948
- type: code === "translation_buffer_limit" ? "upstream_error" : type,
949
- code,
950
- });
951
- throw streamError;
952
- }
953
- if (parsed.usage) usage = parsed.usage;
954
- const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
955
- const choice = isRec(choices[0]) ? choices[0] : null;
956
- if (!choice) continue;
957
- if (typeof choice.finish_reason === "string") finishReason = choice.finish_reason;
958
- const delta = isRec(choice.delta) ? choice.delta : null;
959
- if (!delta) continue;
960
- if (typeof delta.content === "string") content = replaceRetained(content, content + delta.content, "retained_collectors");
961
- if (delta.refusal !== undefined && delta.refusal !== null) {
962
- if (typeof delta.refusal !== "string") throw refusalTranslationError();
963
- refusal = replaceRetained(refusal ?? "", (refusal ?? "") + delta.refusal, "retained_collectors");
964
- }
965
- if (typeof delta.reasoning_content === "string") reasoning = replaceRetained(reasoning, reasoning + delta.reasoning_content, "retained_collectors");
966
- if (Array.isArray(delta.tool_calls)) {
967
- for (const tc of delta.tool_calls) {
968
- if (!isRec(tc)) continue;
969
- const index = typeof tc.index === "number" ? tc.index : 0;
970
- let current = toolCalls.get(index);
971
- if (!current) {
972
- current = { id: "", name: "", arguments: "", argumentBytes: 0 };
973
- toolCalls.set(index, current);
974
- translatorBudget.openCall(callScope(index));
975
- }
976
- if (typeof tc.id === "string") current.id = tc.id;
977
- const fn = isRec(tc.function) ? tc.function : {};
978
- // Done-frame final arguments are authoritative last-write-wins snapshots.
979
- if (typeof fn.name === "string" && fn.name.length > 0) current.name = fn.name;
980
- if (typeof fn.arguments === "string") {
981
- const replace = fn.arguments.startsWith("{") || fn.arguments.startsWith("[") || current.arguments.length === 0;
982
- const nextBytes = replace
983
- ? Buffer.byteLength(fn.arguments)
984
- : appendedUtf8Bytes(current.arguments, current.argumentBytes, fn.arguments);
985
- const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "tool_args", callId: callScope(index) });
986
- try {
987
- current.arguments = replace ? fn.arguments : current.arguments + fn.arguments;
988
- reservation.commitRetained();
989
- translatorBudget.releaseRetained(current.argumentBytes, { kind: "tool_args", callId: callScope(index) });
990
- current.argumentBytes = nextBytes;
991
- } catch (error) {
992
- reservation.release();
993
- throw error;
994
- }
960
+ buffer.append(decoder.decode(value, { stream: true }));
961
+ for (let frame = buffer.next(); frame; frame = buffer.next()) {
962
+ const data = sseDataPayload(frame.block)?.trim();
963
+ if (!data || data === "[DONE]") continue;
964
+ let parsed: unknown;
965
+ try { parsed = JSON.parse(data); } catch { continue; }
966
+ if (!isRec(parsed)) continue;
967
+ if (isRec(parsed.error)) {
968
+ const message = typeof parsed.error.message === "string"
969
+ ? parsed.error.message
970
+ : "upstream request failed";
971
+ const type = typeof parsed.error.type === "string" ? parsed.error.type : "server_error";
972
+ const code = typeof parsed.error.code === "string" ? parsed.error.code : null;
973
+ const status = code === "translation_buffer_limit"
974
+ ? 502
975
+ : code === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(message)
976
+ ? 400
977
+ : streamErrorStatus(message);
978
+ const streamError = new ChatCompletionsStreamError(message, {
979
+ status,
980
+ type: code === "translation_buffer_limit" ? "upstream_error" : type,
981
+ code,
982
+ });
983
+ throw streamError;
984
+ }
985
+ if (parsed.usage) usage = parsed.usage;
986
+ if (typeof parsed.service_tier === "string") serviceTier = parsed.service_tier;
987
+ const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
988
+ const choice = isRec(choices[0]) ? choices[0] : null;
989
+ if (!choice) continue;
990
+ if (typeof choice.finish_reason === "string") finishReason = choice.finish_reason;
991
+ const delta = isRec(choice.delta) ? choice.delta : null;
992
+ if (!delta) continue;
993
+ if (typeof delta.content === "string") content = appendRetained("content", content, delta.content);
994
+ if (delta.refusal !== undefined && delta.refusal !== null) {
995
+ if (typeof delta.refusal !== "string") throw refusalTranslationError();
996
+ refusal = appendRetained("refusal", refusal ?? "", delta.refusal);
997
+ }
998
+ if (typeof delta.reasoning_content === "string") reasoning = appendRetained("reasoning", reasoning, delta.reasoning_content);
999
+ if (Array.isArray(delta.tool_calls)) {
1000
+ for (const tc of delta.tool_calls) {
1001
+ if (!isRec(tc)) continue;
1002
+ const index = typeof tc.index === "number" ? tc.index : 0;
1003
+ let current = toolCalls.get(index);
1004
+ if (!current) {
1005
+ current = { id: "", name: "", arguments: "", argumentBytes: 0 };
1006
+ toolCalls.set(index, current);
1007
+ translatorBudget.openCall(callScope(index));
1008
+ }
1009
+ if (typeof tc.id === "string") current.id = tc.id;
1010
+ const fn = isRec(tc.function) ? tc.function : {};
1011
+ // Done-frame final arguments are authoritative last-write-wins snapshots.
1012
+ if (typeof fn.name === "string" && fn.name.length > 0) current.name = fn.name;
1013
+ if (typeof fn.arguments === "string") {
1014
+ const replace = fn.arguments.startsWith("{") || fn.arguments.startsWith("[") || current.arguments.length === 0;
1015
+ const nextBytes = replace
1016
+ ? Buffer.byteLength(fn.arguments)
1017
+ : appendedUtf8Bytes(current.arguments, current.argumentBytes, fn.arguments);
1018
+ const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "tool_args", callId: callScope(index) });
1019
+ try {
1020
+ current.arguments = replace ? fn.arguments : current.arguments + fn.arguments;
1021
+ reservation.commitRetained();
1022
+ translatorBudget.releaseRetained(current.argumentBytes, { kind: "tool_args", callId: callScope(index) });
1023
+ current.argumentBytes = nextBytes;
1024
+ } catch (error) {
1025
+ reservation.release();
1026
+ throw error;
995
1027
  }
996
1028
  }
997
1029
  }
998
1030
  }
999
1031
  }
1032
+ buffer.compact();
1000
1033
  }
1001
1034
  } catch (error) {
1002
- // Processing may fail between reads; cancel while we still own the lock so the
1003
- // upstream translator releases its maps and stops any pending provider read.
1035
+ // Cancel while we still own the reader so a failed collection releases its upstream.
1004
1036
  try { await reader.cancel(error); } catch { /* preserve the original failure */ }
1005
- translatorBudget.releaseRetained(Buffer.byteLength(refusal ?? ""), { kind: "retained_collectors" });
1037
+ releaseCollectors();
1006
1038
  // Never leak an open call scope on the error path; the turn budget's
1007
1039
  // dispose is a backstop, not the owner of this transfer.
1008
1040
  for (const index of toolCalls.keys()) translatorBudget.closeCall(callScope(index));
@@ -1015,8 +1047,12 @@ export async function collectChatCompletion(
1015
1047
  code: error.code,
1016
1048
  });
1017
1049
  }
1018
- throw error;
1050
+ if (isChatCompletionsStreamError(error)) throw error;
1051
+ throw new ChatCompletionsStreamError(error instanceof Error ? error.message : String(error));
1019
1052
  } finally {
1053
+ // Preserve the previous EOF contract: only delimiter-terminated events are collected.
1054
+ // A partial final frame is discarded, with its retained input ownership released.
1055
+ buffer.clear();
1020
1056
  reader.releaseLock();
1021
1057
  }
1022
1058
 
@@ -1048,6 +1084,7 @@ export async function collectChatCompletion(
1048
1084
  return copy;
1049
1085
  });
1050
1086
  } catch (error) {
1087
+ releaseCollectors();
1051
1088
  for (const copyBytes of chargedCopies) {
1052
1089
  translatorBudget.releaseRetained(copyBytes, { kind: "retained_collectors" });
1053
1090
  }
@@ -1076,5 +1113,6 @@ export async function collectChatCompletion(
1076
1113
  logprobs: null,
1077
1114
  }],
1078
1115
  usage: usage && isRec(usage) ? usage : chatCompletionsUsage(undefined),
1116
+ ...(typeof serviceTier === "string" ? { service_tier: serviceTier } : {}),
1079
1117
  };
1080
1118
  }
@@ -103,12 +103,10 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
103
103
  if (!isPlainObject(value.assignments)) throw new DesktopProfileError("must be an object", "profile.assignments");
104
104
  if (!isPlainObject(value.defaults)) throw new DesktopProfileError("must be an object", "profile.defaults");
105
105
  assertExactKeys(value.defaults, DESKTOP_FAMILIES, "profile.defaults");
106
- if (value.appliedFingerprint !== undefined && typeof value.appliedFingerprint !== "string") {
107
- throw new DesktopProfileError("must be a string", "profile.appliedFingerprint");
108
- }
109
- if (value.appliedAt !== undefined && typeof value.appliedAt !== "string") {
110
- throw new DesktopProfileError("must be a string", "profile.appliedAt");
111
- }
106
+ // JSON null (and any other non-string) is unset, not fatal. Older builds and several
107
+ // writers persisted appliedFingerprint/appliedAt as JSON null; treating those as a
108
+ // document-level parse failure made loadConfig replace the whole operator config with
109
+ // defaults (#4430). appliedMarkers() already drops non-strings when copying.
112
110
 
113
111
  const assignments: Record<string, OcxClaudeDesktopAssignment> = {};
114
112
  const aliases = new Set<string>();
@@ -1,3 +1,4 @@
1
+ import { parseQuotaFailureCode, type QuotaFailureCode } from "../providers/quota-types";
1
2
  /**
2
3
  * Data-access layer for `ocx account` (issue #180) — live-proxy HTTP client and
3
4
  * per-family account readers. Kept separate from account.ts (command handlers)
@@ -26,12 +27,16 @@ export interface AccountRow {
26
27
  masked?: string;
27
28
  active: boolean;
28
29
  needsReauth?: boolean;
30
+ selectionExcludedReason?: "plan_excluded";
31
+ selectionExcludedPlan?: string;
29
32
  /** Registered credential that is still excluded from routing until validation completes. */
30
33
  validationPending?: boolean;
31
34
  /** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
32
35
  priority?: number;
33
36
  quota?: CodexQuotaDto | null;
34
37
  quotaRefresh?: CodexQuotaRefreshOutcome;
38
+ quotaUnavailable?: boolean;
39
+ quotaFailure?: QuotaFailureCode;
35
40
  /**
36
41
  * Whether the pool is holding this account out of rotation.
37
42
  *
@@ -243,6 +248,8 @@ interface CodexAccountDto {
243
248
  plan?: string;
244
249
  isMain?: boolean;
245
250
  needsReauth?: boolean;
251
+ selectionExcludedReason?: "plan_excluded";
252
+ selectionExcludedPlan?: string;
246
253
  health?: { reason?: string };
247
254
  priority?: number;
248
255
  quota?: CodexQuotaDto | null;
@@ -309,6 +316,10 @@ export async function fetchCodexRows(
309
316
  plan: a.plan,
310
317
  active: a.id === activeId,
311
318
  needsReauth: a.needsReauth,
319
+ ...(a.selectionExcludedReason === "plan_excluded" ? {
320
+ selectionExcludedReason: "plan_excluded" as const,
321
+ ...(typeof a.selectionExcludedPlan === "string" ? { selectionExcludedPlan: a.selectionExcludedPlan } : {}),
322
+ } : {}),
312
323
  ...(a.health?.reason === "validation_pending" ? { validationPending: true } : {}),
313
324
  priority: typeof a.priority === "number" ? a.priority : 0,
314
325
  paused: a.paused === true,
@@ -330,6 +341,7 @@ interface OAuthAccountDto {
330
341
  plan?: string | null;
331
342
  quota?: CodexQuotaDto | null;
332
343
  quotaUnavailable?: boolean;
344
+ quotaFailure?: unknown;
333
345
  }
334
346
 
335
347
  async function fetchOAuthRows(
@@ -363,6 +375,8 @@ async function fetchOAuthRows(
363
375
  plan: a.plan ?? null,
364
376
  ...(a.quota !== undefined ? { quota: a.quota } : {}),
365
377
  ...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
378
+ ...(a.quotaUnavailable === true && parseQuotaFailureCode(a.quotaFailure)
379
+ ? { quotaFailure: parseQuotaFailureCode(a.quotaFailure) } : {}),
366
380
  }));
367
381
  return { rows, activeId, status: 200 };
368
382
  }
@@ -44,7 +44,7 @@ const EXTENDED_USAGE = `Usage:
44
44
  ocx account pause <provider> <id|main> [--json]
45
45
  ocx account resume <provider> <id|main> [--json]
46
46
  ocx account pause-exhausted <provider> [--json]
47
- ocx account strategy <provider> [<quota|round-robin|fill-first>] [--json]
47
+ ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
48
48
  ocx account sticky <provider> [<1-100>] [--json]
49
49
  ocx account remove <provider> <id|main> --yes [--json]
50
50
  ocx account clear-cooldown <provider> <id|main> [--json]
@@ -0,0 +1,60 @@
1
+ import { parseCapacityReason } from "../codex/quota-capacity";
2
+ import { isValidCodexAccountId } from "../codex/account-id";
3
+ import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api";
4
+
5
+ function historyDate(value: unknown): string {
6
+ if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
7
+ const date = new Date(value);
8
+ return Number.isFinite(date.getTime()) ? date.toISOString() : "unknown";
9
+ }
10
+
11
+ /** Read cached pool observations without refreshing credentials or spending quota. */
12
+ export async function cmdAccountHistory(args: string[], deps: AccountDeps): Promise<number> {
13
+ const [provider, accountId, ...flags] = args;
14
+ let json = false;
15
+ let limit = 200;
16
+ let hasLimit = false;
17
+ let valid = provider === "openai" && isValidCodexAccountId(accountId);
18
+ for (let index = 0; index < flags.length; index++) {
19
+ if (flags[index] === "--json" && !json) json = true;
20
+ else if (flags[index] === "--limit" && !hasLimit && /^(?:[1-9]|[1-9][0-9]|1[0-9]{2}|200)$/.test(flags[index + 1] ?? "")) {
21
+ limit = Number(flags[++index]); hasLimit = true;
22
+ } else valid = false;
23
+ }
24
+ if (!valid) {
25
+ console.error("Usage: ocx account history openai <pool-account-id> [--limit <1-200>] [--json]");
26
+ return 1;
27
+ }
28
+ const baseUrl = await resolveBaseUrl(deps);
29
+ if (!baseUrl) return proxyUnreachable();
30
+ const result = await apiJson(deps, baseUrl, "GET", `/api/codex-auth/quota/history?accountId=${encodeURIComponent(accountId)}&limit=${limit}`);
31
+ if (result.status === 0) return proxyUnreachable(result.transportError);
32
+ if (result.status !== 200) return apiError(result.json, "Quota history unavailable", result.status);
33
+ if (json) { console.log(JSON.stringify(result.json, null, 2)); return 0; }
34
+ const observations = result.json.observations;
35
+ if (!Array.isArray(observations)) return apiError({}, "Invalid quota history response", 502);
36
+ console.log("OBSERVED\tSOURCE\tWINDOW\tUSED\tRESET");
37
+ if (!observations.length) console.log("No quota observations for this credential publication.");
38
+ for (const observation of observations) {
39
+ if (!observation || typeof observation !== "object" || !Array.isArray(observation.windows)
40
+ || !Number.isFinite(observation.observedAt)) return apiError({}, "Invalid quota history response", 502);
41
+ for (const window of observation.windows) {
42
+ console.log(`${historyDate(observation.observedAt)}\t${observation.source}\t${window.family}/${window.window}\t${window.usedPercent}%\t${historyDate(window.resetAtMs)}`);
43
+ }
44
+ }
45
+ const capacity = result.json.capacity;
46
+ if (capacity && typeof capacity === "object" && "status" in capacity && capacity.status === "estimated"
47
+ && "estimates" in capacity && Array.isArray(capacity.estimates)) {
48
+ console.log("Effective capacity estimate (low confidence; not a provider token limit):");
49
+ for (const estimate of capacity.estimates) {
50
+ if (estimate && Number.isFinite(estimate.estimatedTokens) && Number.isSafeInteger(estimate.sampleCount)) {
51
+ console.log(`${estimate.window}\t~${estimate.estimatedTokens} reported tokens / 100%\t${estimate.sampleCount} samples`);
52
+ }
53
+ }
54
+ }
55
+ if (capacity && typeof capacity === "object" && "status" in capacity && capacity.status === "insufficient-evidence") {
56
+ const reason = "reason" in capacity ? parseCapacityReason(capacity.reason) : undefined;
57
+ console.log(`Effective capacity: insufficient evidence${reason ? ` (${reason})` : ""}.`);
58
+ }
59
+ return 0;
60
+ }
@@ -17,6 +17,9 @@ const USAGE = `Usage:
17
17
  ocx account main list [--json]
18
18
  ocx account main register <label> [--json]
19
19
  ocx account main add <label>
20
+ ocx account main reauth --device [--no-wait] [--json]
21
+ ocx account main reauth status --flow <id> [--json]
22
+ ocx account main reauth cancel --flow <id> [--json]
20
23
  ocx account main switch <profile-id-or-label> --yes [--json]
21
24
  ocx account main recover [--rollback --yes] [--json]
22
25
 
@@ -186,6 +189,83 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
186
189
  const baseUrl = await resolveBaseUrl(deps);
187
190
  if (!baseUrl) return proxyUnreachable();
188
191
 
192
+ if (sub === "reauth") {
193
+ // #3898: same-identity device reauth of the native __main__ slot via the
194
+ // dedicated management namespace; /api/codex-auth/login stays pool-only.
195
+ if (confirmed || rollback) return reject(args);
196
+ const noWait = flag(args, "--no-wait");
197
+ const device = flag(args, "--device");
198
+ const flowIndex = args.indexOf("--flow");
199
+ const flowId = flowIndex >= 0 ? args.splice(flowIndex, 2)[1] : undefined;
200
+ const action = args.shift();
201
+ if (args.length > 0) return reject(args);
202
+
203
+ const printStatus = (status: Record<string, unknown>): void => {
204
+ if (wantsJson) { console.log(JSON.stringify(status, null, 2)); return; }
205
+ console.log("status: " + String(status.status ?? "unknown"));
206
+ if (typeof status.verificationUrl === "string" && status.verificationUrl) {
207
+ console.log("url: " + status.verificationUrl);
208
+ }
209
+ if (typeof status.deviceCode === "string" && status.deviceCode) {
210
+ console.log("deviceCode: " + status.deviceCode);
211
+ }
212
+ if (typeof status.code === "string" && status.code) {
213
+ console.log("failure: " + status.code);
214
+ }
215
+ };
216
+
217
+ if (action === "status" || action === "cancel") {
218
+ if (!flowId || device || noWait) return reject(args);
219
+ const query = "/api/codex-auth/main/reauth-device?flowId=" + encodeURIComponent(flowId);
220
+ const result = action === "status"
221
+ ? await apiJson(deps, baseUrl, "GET", query)
222
+ : await apiJson(deps, baseUrl, "DELETE", query);
223
+ if (result.status === 0) return proxyUnreachable(result.transportError);
224
+ if (result.status !== 200) return apiError(result.json, "failed to " + action + " the native main reauth", result.status);
225
+ printStatus(result.json);
226
+ return 0;
227
+ }
228
+
229
+ if (action !== undefined || !device) return reject(args);
230
+ const started = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/main/reauth-device");
231
+ if (started.status === 0) return proxyUnreachable(started.transportError);
232
+ if (started.status !== 200) return apiError(started.json, "failed to start the native main device reauth", started.status);
233
+ const startFlowId = typeof started.json.flowId === "string" ? started.json.flowId : "";
234
+ if (!startFlowId) {
235
+ console.error("The proxy returned an invalid reauth flow.");
236
+ return 1;
237
+ }
238
+ // The URL and human code arrive with the usercode response, a beat after start.
239
+ let pending = started.json;
240
+ const statusQuery = "/api/codex-auth/main/reauth-device?flowId=" + encodeURIComponent(startFlowId);
241
+ for (let attempt = 0; attempt < 50 && !pending.deviceCode; attempt += 1) {
242
+ await Bun.sleep(200);
243
+ const polled = await apiJson(deps, baseUrl, "GET", statusQuery);
244
+ if (polled.status === 200) pending = polled.json;
245
+ }
246
+ if (noWait) {
247
+ printStatus({ flowId: startFlowId, ...pending });
248
+ console.log("follow up: ocx account main reauth status --flow " + startFlowId);
249
+ return 0;
250
+ }
251
+ // Blocking wait bounded by the service flow expiry (15-minute grant + margin).
252
+ const deadline = Date.now() + 16 * 60_000;
253
+ for (;;) {
254
+ if (pending.status !== "pending" && pending.status !== "committing") break;
255
+ if (Date.now() >= deadline) {
256
+ console.error("The reauth flow did not finish within the device grant window; check status with --flow " + startFlowId + ".");
257
+ return 1;
258
+ }
259
+ await Bun.sleep(2_000);
260
+ const polled = await apiJson(deps, baseUrl, "GET", statusQuery);
261
+ if (polled.status === 0) return proxyUnreachable(polled.transportError);
262
+ if (polled.status !== 200) return apiError(polled.json, "failed to poll the native main reauth", polled.status);
263
+ pending = polled.json;
264
+ }
265
+ printStatus({ flowId: startFlowId, ...pending });
266
+ return pending.status === "succeeded" ? 0 : 1;
267
+ }
268
+
189
269
  if (sub === "doctor" || sub === "list") {
190
270
  if (args.length > 0 || confirmed || rollback) return reject(args);
191
271
  const path = sub === "doctor" ? "/api/native-main-profiles/doctor" : "/api/native-main-profiles";