@bitkyc08/opencodex 2.52.0 → 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-CWXut3rG.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
@@ -0,0 +1,153 @@
1
+ import { formatErrorResponse } from "../bridge";
2
+ import {
3
+ CodexAccountCooldownError,
4
+ CodexMainProfileDrainingError,
5
+ codexMainProfileDrainingResponse,
6
+ cooldownErrorResponse,
7
+ isCodexAuthContextUsable,
8
+ materializeCodexUpstreamAuth,
9
+ releaseCodexAuthContextProbeLease,
10
+ resolveCodexAuthContext,
11
+ type CodexAuthContext,
12
+ } from "../codex/auth-context";
13
+ import { formatCodexProviderForLog } from "../codex/routing";
14
+ import type { AdmissionLease } from "../lib/admission";
15
+ import { captureExplicitOpenAiCallerAuth, resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
16
+ import type { OcxConfig } from "../types";
17
+ import {
18
+ isProxyAdmissionSecret,
19
+ resolveDataPlaneAdmissionSecret,
20
+ validateForwardAdmissionCredential,
21
+ type DataPlaneAdmission,
22
+ } from "./auth-cors";
23
+ import { codexAccountSelectionForTurn } from "./lifecycle";
24
+ import type { RequestLogContext } from "./request-log";
25
+
26
+ export const TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
27
+ export const LIVE_AUDIO_MODEL = "gpt-live-1-codex";
28
+
29
+ /** Audio keys remain identifiable even on the otherwise unauthenticated local listener. */
30
+ export function resolveAudioAdmission(headers: Headers, config: OcxConfig): DataPlaneAdmission | null {
31
+ const dedicated = headers.get("x-opencodex-api-key")?.trim();
32
+ if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
33
+ const authorization = headers.get("authorization")?.trim();
34
+ if (authorization) {
35
+ const token = /^Bearer\s+([^\s,]+)$/i.exec(authorization)?.[1];
36
+ return token ? resolveDataPlaneAdmissionSecret(token, config, "bearer") : null;
37
+ }
38
+ const key = headers.get("x-api-key")?.trim();
39
+ return key ? resolveDataPlaneAdmissionSecret(key, config, "x-api-key") : null;
40
+ }
41
+
42
+ export interface AudioUpstream {
43
+ providerName: string;
44
+ providerBaseUrl: string;
45
+ headers: Record<string, string>;
46
+ keyed: boolean;
47
+ authContext?: CodexAuthContext;
48
+ recordOutcome?: (status: number | "timeout" | "connect_error") => void;
49
+ release: () => void;
50
+ }
51
+
52
+ export interface AudioUpstreamOptions {
53
+ admission: DataPlaneAdmission;
54
+ model: string;
55
+ lease?: AdmissionLease;
56
+ exactAccountId?: string;
57
+ providerName?: string;
58
+ signal?: AbortSignal;
59
+ }
60
+
61
+ /** Keep client admission, account selection, and outbound credentials as separate values. */
62
+ export async function resolveAudioUpstream(
63
+ incoming: Headers,
64
+ config: OcxConfig,
65
+ log: RequestLogContext,
66
+ options: AudioUpstreamOptions,
67
+ ): Promise<AudioUpstream | Response> {
68
+ const candidates = selectOpenAiImagesProvider(config);
69
+ if (options.providerName) {
70
+ candidates.forwardCandidates = candidates.forwardCandidates.filter(candidate => candidate.providerName === options.providerName);
71
+ if (candidates.keyed?.providerName !== options.providerName) delete candidates.keyed;
72
+ }
73
+ const headers = new Headers(incoming);
74
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
75
+ if (bearer && isProxyAdmissionSecret(bearer, config)) {
76
+ headers.delete("authorization");
77
+ headers.delete("chatgpt-account-id");
78
+ }
79
+ headers.delete("x-opencodex-api-key");
80
+ headers.delete("x-api-key");
81
+ headers.delete("sec-websocket-protocol");
82
+ let context: CodexAuthContext | undefined;
83
+ try {
84
+ options.signal?.throwIfAborted();
85
+ const candidate = candidates.forwardCandidates[0];
86
+ if (candidate) {
87
+ let selected: Headers;
88
+ let recordOutcome: AudioUpstream["recordOutcome"];
89
+ const beginCodexAccountSelection = codexAccountSelectionForTurn(options.lease);
90
+ if (candidate.accountMode === "direct" && options.exactAccountId === undefined
91
+ && !captureExplicitOpenAiCallerAuth(incoming, config)) {
92
+ context = await resolveCodexAuthContext(headers, config, "direct", {
93
+ admission: options.admission,
94
+ substituteMainCredentialForDirect: true,
95
+ beginCodexAccountSelection,
96
+ signal: options.signal,
97
+ });
98
+ options.signal?.throwIfAborted();
99
+ selected = materializeCodexUpstreamAuth(headers, context, {
100
+ config,
101
+ admission: options.admission,
102
+ substituteMainCredential: true,
103
+ });
104
+ } else {
105
+ const resolved = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, headers, config, {
106
+ admission: options.admission,
107
+ beginCodexAccountSelection,
108
+ signal: options.signal,
109
+ ...(options.exactAccountId ? { exactAccount: { accountId: options.exactAccountId, modelId: options.model } } : {}),
110
+ });
111
+ if (!resolved) return formatErrorResponse(401, "authentication_error", "Connect a ChatGPT account to use audio");
112
+ context = resolved.authContext;
113
+ selected = resolved.headers;
114
+ recordOutcome = resolved.recordOutcome;
115
+ }
116
+ options.signal?.throwIfAborted();
117
+ if (!isCodexAuthContextUsable(context, config)) {
118
+ releaseCodexAuthContextProbeLease(context);
119
+ return formatErrorResponse(401, "authentication_error", "Selected audio account is unavailable");
120
+ }
121
+ validateForwardAdmissionCredential(selected, config);
122
+ log.provider = formatCodexProviderForLog(candidate.providerName, context.accountId, config);
123
+ log.model = options.model;
124
+ return {
125
+ providerName: candidate.providerName,
126
+ providerBaseUrl: candidate.provider.baseUrl,
127
+ headers: Object.fromEntries(selected),
128
+ keyed: false,
129
+ authContext: context,
130
+ recordOutcome,
131
+ release: () => releaseCodexAuthContextProbeLease(context),
132
+ };
133
+ }
134
+ if (candidates.keyed && options.exactAccountId === undefined) {
135
+ const { providerName, provider, apiKey } = candidates.keyed;
136
+ const selected = new Headers(provider.headers);
137
+ selected.set("authorization", `Bearer ${apiKey}`);
138
+ validateForwardAdmissionCredential(selected, config);
139
+ log.provider = providerName;
140
+ log.model = options.model;
141
+ return {
142
+ providerName, providerBaseUrl: provider.baseUrl, headers: Object.fromEntries(selected),
143
+ keyed: true, release: () => {},
144
+ };
145
+ }
146
+ return formatErrorResponse(400, "invalid_request_error", "Audio requires a connected ChatGPT account or OpenAI API provider");
147
+ } catch (error) {
148
+ releaseCodexAuthContextProbeLease(context);
149
+ if (error instanceof CodexAccountCooldownError) return cooldownErrorResponse(error);
150
+ if (error instanceof CodexMainProfileDrainingError) return codexMainProfileDrainingResponse();
151
+ return formatErrorResponse(401, "authentication_error", "Audio account authentication unavailable; reconnect the selected OpenAI account");
152
+ }
153
+ }
@@ -1,3 +1,4 @@
1
+ import { modelCapabilitiesConfigError } from "../config/provider-validation";
1
2
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
3
  import { initialModelSelection } from "../providers/initial-model-selection";
3
4
  import { extractAccountId } from "../oauth/chatgpt";
@@ -13,6 +14,8 @@ import {
13
14
  } from "../config";
14
15
  import {
15
16
  apiKeyTransportConfigError,
17
+ autoReviewModelOverridesConfigError,
18
+ autoReviewModelTargetConfigError,
16
19
  booleanRecordConfigError,
17
20
  providerReasoningPinsConfigError,
18
21
  modelAdapterRecordConfigError,
@@ -482,6 +485,9 @@ export interface ApiAuthMatrixRow {
482
485
  * against every cell rather than reading the table back to itself.
483
486
  */
484
487
  export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
488
+ { endpoint: "/v1/audio/transcriptions", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
489
+ { endpoint: "/v1/live", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
490
+ { endpoint: "/v1/realtime/calls", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
485
491
  // #1686: a bearer that is one of OUR admission secrets is now accepted here. It is safe
486
492
  // because materializeCodexUpstreamAuth substitutes the stored main credential rather than
487
493
  // forwarding it; a bearer that is NOT our secret stays unadmitted and remains Codex Direct
@@ -499,6 +505,8 @@ export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
499
505
  // it forwards no caller credential upstream and its body is booleans plus model ids — and it
500
506
  // 404s on any host whose runtimeRole is not "hub", so no standalone install gains a surface.
501
507
  { endpoint: "/v1/hub-state", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
508
+ // Usage additionally requires a configured key identity; unscoped environment keys are refused.
509
+ { endpoint: "/v1/usage", bearer: "rejected", dedicated: "accepted", xApiKey: "rejected" },
502
510
  ];
503
511
 
504
512
  /** Whether `token` is the environment-provided management secret. */
@@ -602,6 +610,23 @@ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: Oc
602
610
  return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
603
611
  }
604
612
 
613
+ /**
614
+ * Operator-overlay tolerant variant of the canonical seed check: every key the registry
615
+ * seed defines must still match the submitted provider verbatim, but keys the seed never
616
+ * defines are ignored instead of failing the comparison. Field-masked writes (PATCH,
617
+ * the provider editor, reload) merge onto the persisted row, so the submitted candidate
618
+ * legitimately carries stored operator overlays like `selectedModels` or `disabled`.
619
+ * Those fields are validated by their own write boundaries and cannot widen what the
620
+ * forward proxy claims. Full-object writes (POST) keep the strict exact-key comparison
621
+ * so a forged overlay cannot ride in on a canonical transport seed.
622
+ */
623
+ function matchesCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
624
+ return Object.keys(expected).every(
625
+ key => Object.hasOwn(actual, key)
626
+ && JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]),
627
+ );
628
+ }
629
+
605
630
  function positiveWindowValue(value: unknown): boolean {
606
631
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
607
632
  }
@@ -638,13 +663,41 @@ function nativeContextOverlayError(raw: Record<string, unknown>): string | null
638
663
  * string, or null when the provider may be persisted. Caller-controlled names/fields are
639
664
  * redacted and JSON-escaped so secrets never reach the response.
640
665
  */
641
- export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
666
+ export function providerManagementConfigError(
667
+ name: unknown,
668
+ provider: unknown,
669
+ options?: { allowOperatorOverlays?: boolean },
670
+ ): string | null {
642
671
  if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
643
672
  return "provider must be a plain object";
644
673
  }
645
674
  const raw = provider as Record<string, unknown>;
675
+ const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
676
+ if (capabilitiesError) return capabilitiesError;
646
677
  const pinsError = providerReasoningPinsConfigError(raw);
647
678
  if (pinsError) return pinsError;
679
+ if (name === "openai" && (Object.hasOwn(raw, "autoReviewModel") || Object.hasOwn(raw, "autoReviewModelOverrides"))) {
680
+ return "provider openai must not include autoReviewModel or autoReviewModelOverrides";
681
+ }
682
+ // Canonical OpenAI is the ChatGPT forward seed. allowPrivateNetwork is the explicit
683
+ // opt-in that skips destination DNS classification (loopback, RFC1918, metadata).
684
+ // Overlay-tolerant comparison would otherwise treat it as an extra key and persist it.
685
+ if (name === "openai" && Object.hasOwn(raw, "allowPrivateNetwork")) {
686
+ return "provider openai must not include allowPrivateNetwork";
687
+ }
688
+ // The same reasoning applies to `headers`, and it is not hypothetical. Canonical OpenAI
689
+ // has no registry `staticHeaders`, so any header block on this row is operator-authored,
690
+ // and the forward adapter copies it onto the ChatGPT request BEFORE the incoming forward
691
+ // headers — a persisted value therefore wins whenever the caller omits that header. The
692
+ // exact-key comparison rejected it as an extra key; overlay tolerance would silently admit
693
+ // it on every merge-based write path while POST still refused it.
694
+ if (name === "openai" && Object.hasOwn(raw, "headers")) {
695
+ return "provider openai must not include headers";
696
+ }
697
+ const autoReviewTargetError = autoReviewModelTargetConfigError(raw.autoReviewModel, "autoReviewModel", true);
698
+ if (autoReviewTargetError) return autoReviewTargetError;
699
+ const autoReviewMapError = autoReviewModelOverridesConfigError(raw.autoReviewModelOverrides, "autoReviewModelOverrides", true);
700
+ if (autoReviewMapError) return autoReviewMapError;
648
701
  for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
649
702
  if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
650
703
  }
@@ -683,7 +736,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
683
736
  // validation and then rejected by the seed comparison, so canonical OpenAI could never
684
737
  // set OR clear it — the value was admitted and then refused in the same request.
685
738
  delete canonicalCandidate.annotateEmptyToolOutputs;
686
- const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
739
+ const canonical = seed && (options?.allowOperatorOverlays
740
+ ? matchesCanonicalProviderSeed(canonicalCandidate, seed)
741
+ : sameCanonicalProviderSeed(canonicalCandidate, seed));
687
742
  if (!canonical) {
688
743
  return `provider ${name} must equal the canonical built-in provider seed`;
689
744
  }
@@ -888,6 +943,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
888
943
  contextWindow: "editor",
889
944
  modelContextWindows: "editor",
890
945
  modelInputModalities: "editor",
946
+ modelCapabilities: "editor",
891
947
  modelMaxInputTokens: "runtime",
892
948
  modelAutoCompactTokenLimits: "editor",
893
949
  defaultMaxOutputTokens: "editor",
@@ -910,6 +966,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
910
966
  modelDefaultReasoningEfforts: "editor",
911
967
  pinnedReasoningEffort: "editor",
912
968
  modelPinnedReasoningEfforts: "editor",
969
+ autoReviewModel: "editor",
970
+ autoReviewModelOverrides: "editor",
913
971
  modelSupportsReasoningSummaries: "editor",
914
972
  modelSupportsVerbosity: "editor",
915
973
  supportsVerbosity: "editor",
@@ -942,6 +1000,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
942
1000
  autoToolChoiceOnlyModels: "editor",
943
1001
  preserveReasoningContentModels: "editor",
944
1002
  requiresReasoningPlaceholderModels: "editor",
1003
+ showThinkingSummary: "editor",
945
1004
  retryOn429: "editor",
946
1005
  transientRetryOn5xx: "editor",
947
1006
  reasoningSplitModels: "editor",
@@ -288,7 +288,7 @@ async function handleChatCompletionsWithBudget(
288
288
  try {
289
289
  internalBodyJson = JSON.stringify(internalBody);
290
290
  translatorBudget.chargeRetained(
291
- new TextEncoder().encode(internalBodyJson).byteLength,
291
+ Buffer.byteLength(internalBodyJson, "utf8"),
292
292
  { kind: "request_copies" },
293
293
  );
294
294
  } catch (err) {
@@ -7,7 +7,8 @@ import {
7
7
  type TranslatorBudget,
8
8
  } from "../lib/translator-budget";
9
9
  import type { OcxUsage } from "../types";
10
- import { nextSseBlock, replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
10
+ import { resolveStallTimeoutSec } from "../stall-timeout";
11
+ import { createSseBlockBuffer, replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
11
12
 
12
13
  type Rec = Record<string, unknown>;
13
14
 
@@ -65,6 +66,10 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
65
66
  const id = typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
66
67
  const created = typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000);
67
68
  const model = requestedModel;
69
+ // Relay the upstream service-tier echo on every chunk, matching how OpenAI/xAI
70
+ // annotate streamed chat chunks, so streaming callers see the same confirmation
71
+ // the non-streaming body carries.
72
+ const serviceTier = typeof value.service_tier === "string" ? { service_tier: value.service_tier } : {};
68
73
  const choices = Array.isArray(value.choices) ? value.choices : [];
69
74
  const choice = isRec(choices[0]) ? choices[0] : {};
70
75
  const message = isRec(choice.message) ? choice.message : {};
@@ -73,6 +78,7 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
73
78
  object: "chat.completion.chunk",
74
79
  created,
75
80
  model,
81
+ ...serviceTier,
76
82
  choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
77
83
  }];
78
84
  const delta: Rec = {};
@@ -85,13 +91,14 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
85
91
  delta.tool_calls = message.tool_calls.filter(isRec).map((tool, index) => ({ ...tool, index }));
86
92
  }
87
93
  if (Object.keys(delta).length > 0) {
88
- frames.push({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] });
94
+ frames.push({ id, object: "chat.completion.chunk", created, model, ...serviceTier, choices: [{ index: 0, delta, finish_reason: null }] });
89
95
  }
90
96
  frames.push({
91
97
  id,
92
98
  object: "chat.completion.chunk",
93
99
  created,
94
100
  model,
101
+ ...serviceTier,
95
102
  choices: [{ index: 0, delta: {}, finish_reason: typeof choice.finish_reason === "string" ? choice.finish_reason : "stop" }],
96
103
  ...(value.usage !== undefined ? { usage: value.usage } : {}),
97
104
  });
@@ -121,6 +128,7 @@ interface NativeChatSseOptions {
121
128
  requestedModel: string;
122
129
  translatorBudget: TranslatorBudget;
123
130
  signal: AbortSignal;
131
+ stallTimeoutSec?: number;
124
132
  onFirstOutput?: () => void;
125
133
  onUsage: (usage: OcxUsage) => void;
126
134
  onTerminal?: (status: number, message?: string) => void;
@@ -135,8 +143,11 @@ export function nativeChatSse(
135
143
  const decoder = new TextDecoder();
136
144
  const encoder = new TextEncoder();
137
145
  const scope = { kind: "live_transient" as const };
138
- let buffer = "";
139
- let bufferBytes = 0;
146
+ const buffer = createSseBlockBuffer(options.translatorBudget, nextBytes => {
147
+ if (nextBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
148
+ throw new Error("upstream SSE event exceeded the safe limit", { cause: { code: "translation_buffer_limit" } });
149
+ }
150
+ });
140
151
  let queuedBytes = 0;
141
152
  let sawFinish = false;
142
153
  let sawDone = false;
@@ -144,50 +155,31 @@ export function nativeChatSse(
144
155
  let settled = false;
145
156
  let cancelled = false;
146
157
  let cancelledBySignal = false;
158
+ const stallMs = resolveStallTimeoutSec(options.stallTimeoutSec) * 1_000;
159
+ // Count only active upstream consumption; downstream backpressure must not
160
+ // consume the provider's inactivity allowance. Comments and empty deltas do.
161
+ let remainingStallMs = stallMs;
162
+ let pullDeadline = 0;
163
+ const stalled = new Error("upstream stream stalled without meaningful progress");
147
164
 
148
165
  const releaseQueued = () => {
149
166
  if (queuedBytes === 0) return;
150
167
  options.translatorBudget.releaseRetained(queuedBytes, scope);
151
168
  queuedBytes = 0;
152
169
  };
153
- const replaceBuffer = (next: string) => {
154
- const nextBytes = encoder.encode(next).byteLength;
155
- const reservation = options.translatorBudget.reserveTransient(nextBytes, scope);
156
- reservation.commitRetained();
157
- options.translatorBudget.releaseRetained(bufferBytes, scope);
158
- buffer = next;
159
- bufferBytes = nextBytes;
160
- };
161
- const appendBuffer = (fragment: string) => {
162
- if (!fragment) return;
163
- const fragmentBytes = encoder.encode(fragment).byteLength;
164
- const nextBytes = bufferBytes + fragmentBytes;
165
- if (nextBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
166
- throw new Error("upstream SSE event exceeded the safe limit", { cause: { code: "translation_buffer_limit" } });
167
- }
168
- const reservation = options.translatorBudget.reserveTransient(nextBytes, scope);
170
+ const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
171
+ const byteLength = Buffer.byteLength(text);
172
+ const reservation = options.translatorBudget.reserveTransient(byteLength, scope);
169
173
  try {
170
- buffer += fragment;
174
+ controller.enqueue(encoder.encode(text));
171
175
  reservation.commitRetained();
172
- options.translatorBudget.releaseRetained(bufferBytes, scope);
173
- bufferBytes = nextBytes;
176
+ queuedBytes += byteLength;
174
177
  } catch (error) {
175
178
  reservation.release();
176
179
  throw error;
177
180
  }
178
181
  };
179
- const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
180
- const bytes = encoder.encode(text);
181
- const reservation = options.translatorBudget.reserveTransient(bytes.byteLength, scope);
182
- controller.enqueue(bytes);
183
- reservation.commitRetained();
184
- queuedBytes += bytes.byteLength;
185
- };
186
- const releaseBuffer = () => {
187
- options.translatorBudget.releaseRetained(bufferBytes, scope);
188
- buffer = "";
189
- bufferBytes = 0;
190
- };
182
+ const releaseBuffer = () => buffer.clear();
191
183
  const settle = (status: number, message?: string) => {
192
184
  if (settled) return;
193
185
  settled = true;
@@ -219,13 +211,20 @@ export function nativeChatSse(
219
211
  ): void => {
220
212
  const payload = sseDataPayload(block);
221
213
  if (payload === null) {
214
+ if (performance.now() >= pullDeadline) throw stalled;
222
215
  enqueue(controller, block + delimiter);
223
216
  return;
224
217
  }
225
218
  const trimmed = payload.trim();
219
+ if (trimmed === "") {
220
+ if (performance.now() >= pullDeadline) throw stalled;
221
+ enqueue(controller, block + delimiter);
222
+ return;
223
+ }
226
224
  if (trimmed === "[DONE]") {
227
225
  sawDone = true;
228
226
  enqueue(controller, replaceSseDataPayload(block, "[DONE]") + delimiter);
227
+ releaseBuffer();
229
228
  settle(200);
230
229
  try { void reader.cancel().catch(() => {}); } catch { /* already closed */ }
231
230
  controller.close();
@@ -254,6 +253,7 @@ export function nativeChatSse(
254
253
  }
255
254
  const safe = chatCompletionsErrorBody(status, classified.message, classified.type, classified.code);
256
255
  enqueue(controller, replaceSseDataPayload(block, JSON.stringify(safe)) + delimiter);
256
+ releaseBuffer();
257
257
  settle(isCyberPolicyCode(classified.code) ? 400 : status, classified.message);
258
258
  try { void reader.cancel(new Error(classified.message)).catch(() => {}); } catch { /* already closed */ }
259
259
  controller.close();
@@ -262,16 +262,28 @@ export function nativeChatSse(
262
262
  const usage = usageFromChat(parsed.usage);
263
263
  if (usage) options.onUsage(usage);
264
264
  const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
265
- if (choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0)) {
266
- sawFinish = true;
267
- }
268
- if (!firstOutput && choices.some(choice => {
265
+ const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
266
+ if (finished) sawFinish = true;
267
+ const hasProgress = choices.some(choice => {
269
268
  if (!isRec(choice) || !isRec(choice.delta)) return false;
270
269
  const delta = choice.delta;
271
270
  return (typeof delta.content === "string" && delta.content.length > 0)
272
271
  || (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0)
273
- || (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0);
274
- })) {
272
+ || (typeof delta.reasoning === "string" && delta.reasoning.length > 0)
273
+ || (Array.isArray(delta.reasoning_details) && delta.reasoning_details.some(detail =>
274
+ isRec(detail) && typeof detail.text === "string" && detail.text.length > 0))
275
+ || (typeof delta.refusal === "string" && delta.refusal.length > 0)
276
+ || (Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
277
+ if (!isRec(tool)) return false;
278
+ const fn = isRec(tool.function) ? tool.function : {};
279
+ return (typeof tool.id === "string" && tool.id.length > 0)
280
+ || (typeof fn.name === "string" && fn.name.length > 0)
281
+ || (typeof fn.arguments === "string" && fn.arguments.length > 0);
282
+ }));
283
+ });
284
+ if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
285
+ else if (performance.now() >= pullDeadline) throw stalled;
286
+ if (!firstOutput && hasProgress) {
275
287
  firstOutput = true;
276
288
  options.onFirstOutput?.();
277
289
  }
@@ -280,6 +292,7 @@ export function nativeChatSse(
280
292
 
281
293
  function onAbort() {
282
294
  cancelledBySignal = true;
295
+ releaseBuffer();
283
296
  if (!settled) {
284
297
  settled = true;
285
298
  options.onCancel?.();
@@ -292,15 +305,39 @@ export function nativeChatSse(
292
305
  return new ReadableStream<Uint8Array>({
293
306
  async pull(controller) {
294
307
  releaseQueued();
308
+ pullDeadline = performance.now() + remainingStallMs;
295
309
  try {
296
310
  for (;;) {
297
- const next = nextSseBlock(buffer);
311
+ if (cancelledBySignal) {
312
+ releaseBuffer();
313
+ controller.close();
314
+ return;
315
+ }
316
+ const next = buffer.next();
298
317
  if (next) {
299
- replaceBuffer(next.rest);
318
+ buffer.compact();
300
319
  processBlock(controller, next.block, next.delimiter);
301
320
  return;
302
321
  }
303
- const { done, value } = await reader.read();
322
+ if (performance.now() >= pullDeadline) throw stalled;
323
+ let timeout: ReturnType<typeof setTimeout> | undefined;
324
+ let read: Awaited<ReturnType<typeof reader.read>>;
325
+ try {
326
+ read = await Promise.race([
327
+ reader.read(),
328
+ new Promise<never>((_, reject) => {
329
+ const check = () => {
330
+ const remaining = pullDeadline - performance.now();
331
+ if (remaining <= 0) reject(stalled);
332
+ else timeout = setTimeout(check, Math.min(remaining, 2_147_483_647));
333
+ };
334
+ check();
335
+ }),
336
+ ]);
337
+ } finally {
338
+ if (timeout !== undefined) clearTimeout(timeout);
339
+ }
340
+ const { done, value } = read;
304
341
  if (cancelled) return;
305
342
  if (cancelledBySignal) {
306
343
  releaseBuffer();
@@ -308,11 +345,11 @@ export function nativeChatSse(
308
345
  return;
309
346
  }
310
347
  if (!done) {
311
- appendBuffer(decoder.decode(value, { stream: true }));
348
+ buffer.append(decoder.decode(value, { stream: true }));
312
349
  continue;
313
350
  }
314
- appendBuffer(decoder.decode());
315
- if (buffer.trim().length > 0) {
351
+ buffer.append(decoder.decode());
352
+ if (buffer.tail().trim().length > 0) {
316
353
  fail(controller, "upstream SSE ended with an unterminated event", "upstream_sse_unterminated");
317
354
  return;
318
355
  }
@@ -327,13 +364,20 @@ export function nativeChatSse(
327
364
  return;
328
365
  }
329
366
  } catch (error) {
367
+ if (cancelled || cancelledBySignal) {
368
+ releaseBuffer();
369
+ try { controller.close(); } catch { /* already closed */ }
370
+ return;
371
+ }
330
372
  const overflow = isTranslatorBudgetExceededError(error)
331
373
  || (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit");
332
374
  fail(
333
375
  controller,
334
376
  overflow ? "upstream SSE event exceeded the safe limit" : error instanceof Error ? error.message : String(error),
335
- overflow ? "translation_buffer_limit" : "upstream_sse_error",
377
+ overflow ? "translation_buffer_limit" : error === stalled ? "upstream_stall_timeout" : "upstream_sse_error",
336
378
  );
379
+ } finally {
380
+ remainingStallMs = Math.max(0, pullDeadline - performance.now());
337
381
  }
338
382
  },
339
383
  cancel(reason) {